Build 0.20.7.0

This commit is contained in:
Markus Isberg
2022-11-18 18:13:38 +02:00
parent 8c8fd865c5
commit ecb6d40b4b
111 changed files with 1346 additions and 701 deletions
@@ -657,12 +657,9 @@ namespace Barotrauma
{ {
if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f) if (Controlled == null || Controlled == this || (Controlled.CharacterHealth.GetAffliction("psychosis")?.Strength ?? 0.0f) <= 0.0f)
{ {
InvisibleTimer = 0.0f; InvisibleTimer = Math.Min(InvisibleTimer, 1.0f);
}
else
{
InvisibleTimer -= deltaTime;
} }
InvisibleTimer -= deltaTime;
} }
foreach (GUIMessage message in guiMessages) foreach (GUIMessage message in guiMessages)
@@ -394,6 +394,7 @@ namespace Barotrauma
showHiddenAfflictionsButton = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: "GUIButtonCircular") showHiddenAfflictionsButton = new GUIButton(new RectTransform(new Point(afflictionIconContainer.Rect.Height), afflictionIconContainer.RectTransform), style: "GUIButtonCircular")
{ {
Visible = false,
CanBeFocused = false CanBeFocused = false
}; };
@@ -2219,6 +2219,15 @@ namespace Barotrauma
} }
})); }));
commands.Add(new Command("spawnallitems", "", (string[] args) =>
{
var cursorPos = Screen.Selected.Cam?.ScreenToWorld(PlayerInput.MousePosition) ?? Vector2.Zero;
foreach (ItemPrefab itemPrefab in ItemPrefab.Prefabs)
{
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, cursorPos);
}
}));
commands.Add(new Command("camerasettings", "camerasettings [defaultzoom] [zoomsmoothness] [movesmoothness] [minzoom] [maxzoom]: debug command for testing camera settings. The values default to 1.1, 8.0, 8.0, 0.1 and 2.0.", (string[] args) => commands.Add(new Command("camerasettings", "camerasettings [defaultzoom] [zoomsmoothness] [movesmoothness] [minzoom] [maxzoom]: debug command for testing camera settings. The values default to 1.1, 8.0, 8.0, 0.1 and 2.0.", (string[] args) =>
{ {
float defaultZoom = Screen.Selected.Cam.DefaultZoom; float defaultZoom = Screen.Selected.Cam.DefaultZoom;
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System;
using System.Linq; using System.Linq;
namespace Barotrauma; namespace Barotrauma;
@@ -10,37 +11,66 @@ partial class UIHighlightAction : EventAction
partial void UpdateProjSpecific() partial void UpdateProjSpecific()
{ {
bool useCircularFlash = false; bool useCircularFlash = false;
GUIComponent component = null;
if (Id != ElementId.None) if (Id != ElementId.None)
{ {
component = GUI.GetAdditions().FirstOrDefault(c => Equals(Id, c.UserData)); FindAndFlashComponents(c => Equals(Id, c.UserData));
} }
else if (!EntityIdentifier.IsEmpty) else if (!EntityIdentifier.IsEmpty)
{ {
component = GUI.GetAdditions().FirstOrDefault(c => FindAndFlashComponents(c =>
c.UserData is MapEntityPrefab mep && mep.Identifier == EntityIdentifier || c.UserData is MapEntity me && me.Prefab.Identifier == EntityIdentifier); c.UserData is MapEntityPrefab mep && mep.Identifier == EntityIdentifier || c.UserData is MapEntity me && me.Prefab.Identifier == EntityIdentifier);
} }
else if (!OrderIdentifier.IsEmpty) else if (!OrderIdentifier.IsEmpty)
{ {
useCircularFlash = true; useCircularFlash = true;
bool foundMinimapNode = false;
if (!OrderTargetTag.IsEmpty) if (!OrderTargetTag.IsEmpty)
{ {
component = foundMinimapNode = FindAndFlashComponents(c =>
GUI.GetAdditions().FirstOrDefault(c => c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order &&
c.UserData is CrewManager.MinimapNodeData nodeData && nodeData.Order is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag));
order.Identifier == OrderIdentifier && order.Option == OrderOption && order.TargetEntity is Item item && item.HasTag(OrderTargetTag)); }
if (!foundMinimapNode)
{
FindAndFlashComponents(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption,
c => c.UserData is Order order && order.Identifier == OrderIdentifier,
c => Equals(OrderCategory, c.UserData));
} }
component ??=
GUI.GetAdditions().FirstOrDefault(c => c.UserData is Order order && order.Identifier == OrderIdentifier && order.Option == OrderOption) ??
GUI.GetAdditions().FirstOrDefault(c => c.UserData is Order order && order.Identifier == OrderIdentifier) ??
GUI.GetAdditions().FirstOrDefault(c => Equals(OrderCategory, c.UserData));
} }
if (component != null && component.FlashTimer <= 0.0f) bool FindAndFlashComponents(params Func<GUIComponent, bool>[] predicates)
{ {
component.Flash(highlightColor, useCircularFlash: useCircularFlash); foreach (var predicate in predicates)
component.Bounce |= Bounce; {
if (HighlightMultiple)
{
bool found = false;
foreach (var component in GUI.GetAdditions())
{
if (predicate(component))
{
Flash(component);
found = true;
}
};
return found;
}
else if (GUI.GetAdditions().FirstOrDefault(predicate) is GUIComponent component)
{
Flash(component);
return true;
}
}
return false;
}
void Flash(GUIComponent component)
{
if (component.FlashTimer <= 0.0f)
{
component.Flash(highlightColor, useCircularFlash: useCircularFlash);
component.Bounce |= Bounce;
}
} }
} }
} }
@@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Xml.Linq;
using Barotrauma.Threading; using Barotrauma.Threading;
namespace Barotrauma namespace Barotrauma
@@ -37,7 +38,7 @@ namespace Barotrauma
private set; private set;
} }
public bool IsCJK public TextManager.SpeciallyHandledCharCategory SpeciallyHandledCharCategory
{ {
get; get;
private set; private set;
@@ -84,17 +85,35 @@ namespace Barotrauma
} }
} }
public static TextManager.SpeciallyHandledCharCategory ExtractShccFromXElement(XElement element)
=> TextManager.SpeciallyHandledCharCategories
.Where(category => element.GetAttributeBool($"is{category}", category switch {
// CJK isn't supported by default
TextManager.SpeciallyHandledCharCategory.CJK => false,
// For backwards compatibility, we assume that Cyrillic is supported by default
TextManager.SpeciallyHandledCharCategory.Cyrillic => true,
_ => throw new Exception("unreachable")
}))
.Aggregate(TextManager.SpeciallyHandledCharCategory.None, (current, category) => current | category);
public ScalableFont(ContentXElement element, GraphicsDevice gd = null) public ScalableFont(ContentXElement element, GraphicsDevice gd = null)
: this( : this(
element.GetAttributeContentPath("file")?.Value, element.GetAttributeContentPath("file")?.Value,
(uint)element.GetAttributeInt("size", 14), (uint)element.GetAttributeInt("size", 14),
gd, gd,
element.GetAttributeBool("dynamicloading", false), element.GetAttributeBool("dynamicloading", false),
element.GetAttributeBool("iscjk", false)) ExtractShccFromXElement(element))
{ {
} }
public ScalableFont(string filename, uint size, GraphicsDevice gd = null, bool dynamicLoading = false, bool isCJK = false) public ScalableFont(
string filename,
uint size,
GraphicsDevice gd = null,
bool dynamicLoading = false,
TextManager.SpeciallyHandledCharCategory speciallyHandledCharCategory = TextManager.SpeciallyHandledCharCategory.None)
{ {
lock (globalMutex) lock (globalMutex)
{ {
@@ -120,7 +139,7 @@ namespace Barotrauma
this.textures = new List<Texture2D>(); this.textures = new List<Texture2D>();
this.texCoords = new Dictionary<uint, GlyphData>(); this.texCoords = new Dictionary<uint, GlyphData>();
this.DynamicLoading = dynamicLoading; this.DynamicLoading = dynamicLoading;
this.IsCJK = isCJK; this.SpeciallyHandledCharCategory = speciallyHandledCharCategory;
this.graphicsDevice = gd; this.graphicsDevice = gd;
if (gd != null && !dynamicLoading) if (gd != null && !dynamicLoading)
@@ -150,7 +150,6 @@ namespace Barotrauma
} }
} }
// TODO: fix implicit hiding
public override bool Selected public override bool Selected
{ {
get { return isSelected; } get { return isSelected; }
@@ -179,6 +179,11 @@ namespace Barotrauma
private set; private set;
} }
/// <summary>
/// If enabled, the value wraps around to Max when you go below Min, and vice versa
/// </summary>
public bool WrapAround;
public float valueStep; public float valueStep;
private float pressedTimer; private float pressedTimer;
@@ -403,13 +408,19 @@ namespace Barotrauma
{ {
if (MinValueFloat != null) if (MinValueFloat != null)
{ {
floatValue = Math.Max(floatValue, MinValueFloat.Value); floatValue =
MinusButton.Enabled = floatValue > MinValueFloat; WrapAround && MinValueFloat.HasValue && floatValue < MinValueFloat.Value ?
MaxValueFloat.Value :
Math.Max(floatValue, MinValueFloat.Value);
MinusButton.Enabled = WrapAround || floatValue > MinValueFloat;
} }
if (MaxValueFloat != null) if (MaxValueFloat != null)
{ {
floatValue = Math.Min(floatValue, MaxValueFloat.Value); floatValue =
PlusButton.Enabled = floatValue < MaxValueFloat; WrapAround && MaxValueFloat.HasValue && floatValue > MaxValueFloat.Value ?
MinValueFloat.Value :
Math.Min(floatValue, MaxValueFloat.Value);
PlusButton.Enabled = WrapAround || floatValue < MaxValueFloat;
} }
} }
@@ -417,16 +428,16 @@ namespace Barotrauma
{ {
if (MinValueInt != null && intValue < MinValueInt.Value) if (MinValueInt != null && intValue < MinValueInt.Value)
{ {
intValue = Math.Max(intValue, MinValueInt.Value); intValue = WrapAround && MaxValueInt.HasValue ? MaxValueInt.Value : Math.Max(intValue, MinValueInt.Value);
UpdateText(); UpdateText();
} }
if (MaxValueInt != null && intValue > MaxValueInt.Value) if (MaxValueInt != null && intValue > MaxValueInt.Value)
{ {
intValue = Math.Min(intValue, MaxValueInt.Value); intValue = WrapAround && MinValueInt.HasValue ? MinValueInt.Value : Math.Min(intValue, MaxValueInt.Value);
UpdateText(); UpdateText();
} }
PlusButton.Enabled = MaxValueInt == null || intValue < MaxValueInt; PlusButton.Enabled = WrapAround || MaxValueInt == null || intValue < MaxValueInt;
MinusButton.Enabled = MinValueInt == null || intValue > MinValueInt; MinusButton.Enabled = WrapAround || MinValueInt == null || intValue > MinValueInt;
} }
private void UpdateText() private void UpdateText()
@@ -8,6 +8,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Xml.Linq; using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma namespace Barotrauma
{ {
@@ -45,16 +46,17 @@ namespace Barotrauma
} }
} }
private ScalableFont cjkFont; private ImmutableDictionary<TextManager.SpeciallyHandledCharCategory, ScalableFont> specialHandlingFonts;
public ScalableFont CjkFont public ScalableFont GetFontForCategory(TextManager.SpeciallyHandledCharCategory category)
{ {
get if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); }
{ if (font.SpeciallyHandledCharCategory.HasFlag(category)) { return font; }
if (Language != GameSettings.CurrentConfig.Language) { LoadFont(); } return specialHandlingFonts.TryGetValue(category, out var resultFont)
if (font.IsCJK) { return font; } ? resultFont
return cjkFont; : specialHandlingFonts.TryGetValue(TextManager.SpeciallyHandledCharCategory.CJK, out resultFont)
} ? resultFont
: font;
} }
public LanguageIdentifier Language { get; private set; } public LanguageIdentifier Language { get; private set; }
@@ -70,40 +72,68 @@ namespace Barotrauma
string fontPath = GetFontFilePath(element); string fontPath = GetFontFilePath(element);
uint size = GetFontSize(element); uint size = GetFontSize(element);
bool dynamicLoading = GetFontDynamicLoading(element); bool dynamicLoading = GetFontDynamicLoading(element);
bool isCJK = GetIsCJK(element); var shcc = GetShcc(element);
font?.Dispose(); font?.Dispose();
cjkFont?.Dispose(); specialHandlingFonts?.Values.ForEach(f => f.Dispose());
font = new ScalableFont(fontPath, size, GameMain.Instance.GraphicsDevice, dynamicLoading, isCJK) font = new ScalableFont(
fontPath,
size,
GameMain.Instance.GraphicsDevice,
dynamicLoading,
shcc)
{ {
ForceUpperCase = element.GetAttributeBool("forceuppercase", false) ForceUpperCase = element.GetAttributeBool("forceuppercase", false)
}; };
if (!isCJK)
var fallbackFonts = new Dictionary<TextManager.SpeciallyHandledCharCategory, ScalableFont>();
foreach (var flag in TextManager.SpeciallyHandledCharCategories)
{ {
cjkFont = ExtractCjkFont(element) if (shcc.HasFlag(flag)) { continue; }
?? new ScalableFont("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf",
font.Size, GameMain.Instance.GraphicsDevice, dynamicLoading: true, isCJK: true); var extractedFont = ExtractFont(flag, element);
cjkFont.ForceUpperCase = font.ForceUpperCase; if (extractedFont is null) { continue; }
fallbackFonts.Add(flag, extractedFont);
} }
fallbackFonts.Values.ForEach(ff => ff.ForceUpperCase = font.ForceUpperCase);
specialHandlingFonts = fallbackFonts.ToImmutableDictionary();
Language = GameSettings.CurrentConfig.Language; Language = GameSettings.CurrentConfig.Language;
} }
public override void Dispose() public override void Dispose()
{ {
font?.Dispose(); font = null; font?.Dispose();
cjkFont?.Dispose(); cjkFont = null; font = null;
specialHandlingFonts?.Values.ForEach(f => f.Dispose());
specialHandlingFonts = null;
} }
private ScalableFont ExtractCjkFont(ContentXElement element) private ScalableFont ExtractFont(TextManager.SpeciallyHandledCharCategory flag, ContentXElement element)
{ {
foreach (var subElement in element.Elements().Reverse()) foreach (var subElement in element.Elements().Reverse())
{ {
if (subElement.NameAsIdentifier() != "override") { continue; } if (subElement.NameAsIdentifier() != "override") { continue; }
if (subElement.GetAttributeBool("iscjk", false)) if (ScalableFont.ExtractShccFromXElement(subElement).HasFlag(flag))
{ {
return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice); return new ScalableFont(subElement, GameMain.Instance.GraphicsDevice);
} }
} }
return null;
ScalableFont hardcodedFallback(string path)
=> new ScalableFont(
path,
font.Size,
GameMain.Instance.GraphicsDevice,
dynamicLoading: true,
speciallyHandledCharCategory: flag);
return flag switch
{
TextManager.SpeciallyHandledCharCategory.CJK
=> hardcodedFallback("Content/Fonts/NotoSans/NotoSansCJKsc-Bold.otf"),
TextManager.SpeciallyHandledCharCategory.Cyrillic
=> hardcodedFallback("Content/Fonts/Oswald-Bold.ttf"),
_ => null
};
} }
private string GetFontFilePath(ContentXElement element) private string GetFontFilePath(ContentXElement element)
@@ -154,21 +184,21 @@ namespace Barotrauma
return element.GetAttributeBool("dynamicloading", false); return element.GetAttributeBool("dynamicloading", false);
} }
private bool GetIsCJK(XElement element) private TextManager.SpeciallyHandledCharCategory GetShcc(XElement element)
{ {
foreach (var subElement in element.Elements()) foreach (var subElement in element.Elements())
{ {
if (IsValidOverride(subElement)) if (IsValidOverride(subElement))
{ {
return subElement.GetAttributeBool("iscjk", false); return ScalableFont.ExtractShccFromXElement(subElement);
} }
} }
return element.GetAttributeBool("iscjk", false); return ScalableFont.ExtractShccFromXElement(element);
} }
private bool IsValidOverride(XElement element) private bool IsValidOverride(XElement element)
{ {
if (!element.Name.ToString().Equals("override", StringComparison.OrdinalIgnoreCase)) { return false; } if (!element.IsOverride()) { return false; }
var languages = element.GetAttributeIdentifierArray("language", Array.Empty<Identifier>()); var languages = element.GetAttributeIdentifierArray("language", Array.Empty<Identifier>());
return languages.Any(l => l.ToLanguageIdentifier() == GameSettings.CurrentConfig.Language); return languages.Any(l => l.ToLanguageIdentifier() == GameSettings.CurrentConfig.Language);
} }
@@ -191,7 +221,7 @@ namespace Barotrauma
private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value); private ScalableFont GetFontForStr(LocalizedString str) => GetFontForStr(str.Value);
public ScalableFont GetFontForStr(string str) => public ScalableFont GetFontForStr(string str) =>
TextManager.IsCJK(str) ? Prefabs.ActivePrefab.CjkFont : Prefabs.ActivePrefab.Font; Prefabs.ActivePrefab.GetFontForCategory(TextManager.GetSpeciallyHandledCategories(str));
public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth) public void DrawString(SpriteBatch sb, LocalizedString text, Vector2 position, Color color, float rotation, Vector2 origin, Vector2 scale, SpriteEffects se, float layerDepth)
{ {
@@ -617,9 +617,9 @@ namespace Barotrauma
Stretch = true Stretch = true
}; };
var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), shoppingCrateInventoryContainer.RectTransform), style: null); var shoppingCrateListContainer = new GUIFrame(new RectTransform(new Vector2(1.0f, 0.8f), shoppingCrateInventoryContainer.RectTransform), style: null);
shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; shoppingCrateBuyList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; shoppingCrateSellList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false }; shoppingCrateSellFromSubList = new GUIListBox(new RectTransform(Vector2.One, shoppingCrateListContainer.RectTransform)) { Visible = false, KeepSpaceForScrollBar = true };
var relevantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true) var relevantBalanceContainer = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.05f), shoppingCrateInventoryContainer.RectTransform), isHorizontal: true)
{ {
@@ -325,7 +325,7 @@ namespace Barotrauma
} }
var specializationList = GetSpecializationList(); var specializationList = GetSpecializationList();
GetSpecializationList().RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height)); GetSpecializationList().Content.RectTransform.Resize(new Point(specializationList.Content.Children.Sum(static c => c.Rect.Width), specializationList.Rect.Height), resizeChildren: false);
GUITextBlock.AutoScaleAndNormalize(subTreeNames); GUITextBlock.AutoScaleAndNormalize(subTreeNames);
@@ -439,7 +439,8 @@ namespace Barotrauma
} }
if (character is null) { return false; } if (character is null) { return false; }
Identifier talentIdentifier = (Identifier)userData;
if (talentOption.MaxChosenTalents is 1) if (talentOption.MaxChosenTalents is 1)
{ {
// deselect other buttons in tier by removing their selected talents from pool // deselect other buttons in tier by removing their selected talents from pool
@@ -454,9 +455,11 @@ namespace Barotrauma
} }
} }
Identifier talentIdentifier = (Identifier)userData; if (character.HasTalent(talentIdentifier))
{
if (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents)) return true;
}
else if (IsViableTalentForCharacter(info.Character, talentIdentifier, selectedTalents))
{ {
if (!selectedTalents.Contains(talentIdentifier)) if (!selectedTalents.Contains(talentIdentifier))
{ {
@@ -467,7 +470,7 @@ namespace Barotrauma
selectedTalents.Remove(talentIdentifier); selectedTalents.Remove(talentIdentifier);
} }
} }
else if (!character.HasTalent(talentIdentifier)) else
{ {
selectedTalents.Remove(talentIdentifier); selectedTalents.Remove(talentIdentifier);
} }
@@ -314,7 +314,7 @@ namespace Barotrauma
GraphicsDeviceManager.SynchronizeWithVerticalRetrace = GameSettings.CurrentConfig.Graphics.VSync; GraphicsDeviceManager.SynchronizeWithVerticalRetrace = GameSettings.CurrentConfig.Graphics.VSync;
SetWindowMode(GameSettings.CurrentConfig.Graphics.DisplayMode); SetWindowMode(GameSettings.CurrentConfig.Graphics.DisplayMode);
defaultViewport = GraphicsDevice.Viewport; defaultViewport = new Viewport(0, 0, GraphicsWidth, GraphicsHeight);
if (recalculateFontsAndStyles) if (recalculateFontsAndStyles)
{ {
@@ -353,6 +353,7 @@ namespace Barotrauma
public void ResetViewPort() public void ResetViewPort()
{ {
GraphicsDevice.Viewport = defaultViewport; GraphicsDevice.Viewport = defaultViewport;
GraphicsDevice.ScissorRectangle = defaultViewport.Bounds;
} }
/// <summary> /// <summary>
@@ -28,18 +28,27 @@ namespace Barotrauma.Items.Components
if (node.ParentIndex > -1) if (node.ParentIndex > -1)
{ {
Vector2 diff = nodes[node.ParentIndex].WorldPosition - node.WorldPosition; CreateParticlesBetween(nodes[node.ParentIndex].WorldPosition, node.WorldPosition);
float dist = diff.Length(); }
Vector2 normalizedDiff = diff / dist; }
for (float x = 0.0f; x < dist; x += 50.0f) foreach (var character in charactersInRange)
{ {
var spark = GameMain.ParticleManager.CreateParticle("ElectricShock", node.WorldPosition + normalizedDiff * x, Vector2.Zero); CreateParticlesBetween(character.character.WorldPosition, character.node.WorldPosition);
if (spark != null) }
{
spark.Size *= 0.3f;
}
}
static void CreateParticlesBetween(Vector2 start, Vector2 end)
{
const float ParticleInterval = 50.0f;
Vector2 diff = end - start;
float dist = diff.Length();
Vector2 normalizedDiff = MathUtils.NearlyEqual(dist, 0.0f) ? Vector2.Zero : diff / dist;
for (float x = 0.0f; x < dist; x += ParticleInterval)
{
var spark = GameMain.ParticleManager.CreateParticle("ElectricShock", start + normalizedDiff * x, Vector2.Zero);
if (spark != null)
{
spark.Size *= 0.3f;
}
} }
} }
} }
@@ -14,7 +14,10 @@ namespace Barotrauma.Items.Components
private GUIFrame selectedItemFrame; private GUIFrame selectedItemFrame;
private GUIFrame selectedItemReqsFrame; private GUIFrame selectedItemReqsFrame;
private GUITextBlock amountTextMin, amountTextMax;
private GUIScrollBar amountInput;
public GUIButton ActivateButton public GUIButton ActivateButton
{ {
get { return activateButton; } get { return activateButton; }
@@ -160,14 +163,46 @@ namespace Barotrauma.Items.Components
new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false }; new GUICustomComponent(new RectTransform(Vector2.One, inputInventoryHolder.RectTransform), DrawInputOverLay) { CanBeFocused = false };
// === ACTIVATE BUTTON === // // === ACTIVATE BUTTON === //
var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.8f), inputArea.RectTransform), childAnchor: Anchor.CenterRight); var buttonFrame = new GUILayoutGroup(new RectTransform(new Vector2(0.3f, 0.9f), inputArea.RectTransform))
activateButton = new GUIButton(new RectTransform(new Vector2(1f, 0.6f), buttonFrame.RectTransform), {
TextManager.Get(CreateButtonText), style: "DeviceButtonFixedSize") Stretch = true,
RelativeSpacing = 0.05f
};
var amountInputHolder = new GUILayoutGroup(new RectTransform(new Vector2(1.0f, 0.4f), buttonFrame.RectTransform), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
Stretch = true
};
amountTextMin = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center);
amountInput = new GUIScrollBar(new RectTransform(new Vector2(0.7f, 1.0f), amountInputHolder.RectTransform), barSize: 0.1f, style: "GUISlider")
{
OnMoved = (GUIScrollBar scrollBar, float barScroll) =>
{
scrollBar.Step = 1.0f / Math.Max(scrollBar.Range.Y - 1, 1);
AmountToFabricate = (int)MathF.Round(scrollBar.BarScrollValue);
RefreshActivateButtonText();
if (GameMain.Client != null)
{
item.CreateClientEvent(this);
}
return true;
}
};
amountTextMax = new GUITextBlock(new RectTransform(new Vector2(0.15f, 1.0f), amountInputHolder.RectTransform), "1", textAlignment: Alignment.Center);
activateButton = new GUIButton(new RectTransform(new Vector2(1.0f, 0.6f), buttonFrame.RectTransform),
TextManager.Get(CreateButtonText), style: "DeviceButton")
{ {
OnClicked = StartButtonClicked, OnClicked = StartButtonClicked,
UserData = selectedItem, UserData = selectedItem,
Enabled = false Enabled = false
}; };
//spacing
new GUIFrame(new RectTransform(new Vector2(1.0f, 0.01f), buttonFrame.RectTransform), style: null);
} }
else else
{ {
@@ -192,6 +227,21 @@ namespace Barotrauma.Items.Components
CreateRecipes(); CreateRecipes();
} }
private void RefreshActivateButtonText()
{
if (amountInput == null)
{
activateButton.Text = TextManager.Get(IsActive ? "FabricatorCancel" : CreateButtonText);
}
else
{
activateButton.Text =
IsActive ?
$"{TextManager.Get("FabricatorCancel")} ({amountRemaining})" :
$"{TextManager.Get(CreateButtonText)} ({AmountToFabricate})";
}
}
partial void CreateRecipes() partial void CreateRecipes()
{ {
itemList.Content.RectTransform.ClearChildren(); itemList.Content.RectTransform.ClearChildren();
@@ -247,7 +297,7 @@ namespace Barotrauma.Items.Components
outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform; outputContainer.Inventory.RectTransform = outputInventoryHolder.RectTransform;
} }
private LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe) private static LocalizedString GetRecipeNameAndAmount(FabricationRecipe fabricationRecipe)
{ {
if (fabricationRecipe == null) { return ""; } if (fabricationRecipe == null) { return ""; }
if (fabricationRecipe.Amount > 1) if (fabricationRecipe.Amount > 1)
@@ -269,7 +319,7 @@ namespace Barotrauma.Items.Components
partial void SelectProjSpecific(Character character) partial void SelectProjSpecific(Character character)
{ {
var nonItems = itemList.Content.Children.Where(c => !(c.UserData is FabricationRecipe)).ToList(); var nonItems = itemList.Content.Children.Where(c => c.UserData is not FabricationRecipe).ToList();
nonItems.ForEach(i => itemList.Content.RemoveChild(i)); nonItems.ForEach(i => itemList.Content.RemoveChild(i));
itemList.Content.RectTransform.SortChildren((c1, c2) => itemList.Content.RectTransform.SortChildren((c1, c2) =>
@@ -567,7 +617,7 @@ namespace Barotrauma.Items.Components
bool recipeVisible = false; bool recipeVisible = false;
foreach (GUIComponent child in itemList.Content.Children.Reverse()) foreach (GUIComponent child in itemList.Content.Children.Reverse())
{ {
if (!(child.UserData is FabricationRecipe recipe)) if (child.UserData is not FabricationRecipe recipe)
{ {
if (child.Enabled) if (child.Enabled)
{ {
@@ -598,9 +648,23 @@ namespace Barotrauma.Items.Components
{ {
this.selectedItem = selectedItem; this.selectedItem = selectedItem;
int max = Math.Max(selectedItem.TargetItem.MaxStackSize / selectedItem.Amount, 1);
if (amountInput != null)
{
float prevBarScroll = amountInput.BarScroll;
amountInput.Range = new Vector2(1, max);
amountInput.BarScroll = prevBarScroll;
amountTextMax.Text = max.ToString();
amountInput.Enabled = amountTextMax.Enabled = max > 1;
AmountToFabricate = Math.Min((int)amountInput.BarScrollValue, max);
}
RefreshActivateButtonText();
selectedItemFrame.ClearChildren(); selectedItemFrame.ClearChildren();
selectedItemReqsFrame.ClearChildren(); selectedItemReqsFrame.ClearChildren();
var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; var paddedFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f };
var paddedReqFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemReqsFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f }; var paddedReqFrame = new GUILayoutGroup(new RectTransform(new Vector2(1f, 0.9f), selectedItemReqsFrame.RectTransform, Anchor.Center)) { RelativeSpacing = 0.03f };
@@ -734,7 +798,9 @@ namespace Barotrauma.Items.Components
outputSlot.Flash(GUIStyle.Red); outputSlot.Flash(GUIStyle.Red);
return false; return false;
} }
amountRemaining = AmountToFabricate;
if (GameMain.Client != null) if (GameMain.Client != null)
{ {
pendingFabricatedItem = fabricatedItem != null ? null : selectedItem; pendingFabricatedItem = fabricatedItem != null ? null : selectedItem;
@@ -777,7 +843,7 @@ namespace Barotrauma.Items.Components
{ {
foreach (GUIComponent child in itemList.Content.Children) foreach (GUIComponent child in itemList.Content.Children)
{ {
if (!(child.UserData is FabricationRecipe recipe)) { continue; } if (child.UserData is not FabricationRecipe recipe) { continue; }
if (recipe != selectedItem && if (recipe != selectedItem &&
(child.Rect.Y > itemList.Rect.Bottom || child.Rect.Bottom < itemList.Rect.Y)) (child.Rect.Y > itemList.Rect.Bottom || child.Rect.Bottom < itemList.Rect.Y))
@@ -811,11 +877,14 @@ namespace Barotrauma.Items.Components
{ {
uint recipeHash = pendingFabricatedItem?.RecipeHash ?? 0; uint recipeHash = pendingFabricatedItem?.RecipeHash ?? 0;
msg.WriteUInt32(recipeHash); msg.WriteUInt32(recipeHash);
msg.WriteRangedInteger(AmountToFabricate, 1, MaxAmountToFabricate);
} }
public void ClientEventRead(IReadMessage msg, float sendingTime) public void ClientEventRead(IReadMessage msg, float sendingTime)
{ {
FabricatorState newState = (FabricatorState)msg.ReadByte(); FabricatorState newState = (FabricatorState)msg.ReadByte();
int amountToFabricate = msg.ReadRangedInteger(0, MaxAmountToFabricate);
int amountRemaining = msg.ReadRangedInteger(0, MaxAmountToFabricate);
float newTimeUntilReady = msg.ReadSingle(); float newTimeUntilReady = msg.ReadSingle();
uint recipeHash = msg.ReadUInt32(); uint recipeHash = msg.ReadUInt32();
UInt16 userID = msg.ReadUInt16(); UInt16 userID = msg.ReadUInt16();
@@ -828,6 +897,8 @@ namespace Barotrauma.Items.Components
} }
State = newState; State = newState;
this.amountToFabricate = amountToFabricate;
this.amountRemaining = amountRemaining;
if (newState == FabricatorState.Stopped || recipeHash == 0) if (newState == FabricatorState.Stopped || recipeHash == 0)
{ {
CancelFabricating(); CancelFabricating();
@@ -802,21 +802,34 @@ namespace Barotrauma.Items.Components
if (passivePingRadius > 0.0f) if (passivePingRadius > 0.0f)
{ {
if (activePingsCount == 0) { disruptedDirections.Clear(); } if (activePingsCount == 0) { disruptedDirections.Clear(); }
//emit "pings" from nearby sound-emitting AITargets to reveal what's around them
foreach (AITarget t in AITarget.List) foreach (AITarget t in AITarget.List)
{ {
if (t.Entity is Character c && !c.IsUnconscious && c.Params.HideInSonar) { continue; } if (t.Entity is Character c && !c.IsUnconscious && c.Params.HideInSonar) { continue; }
if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; } if (t.SoundRange <= 0.0f || float.IsNaN(t.SoundRange) || float.IsInfinity(t.SoundRange)) { continue; }
float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter); float distSqr = Vector2.DistanceSquared(t.WorldPosition, transducerCenter);
if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; } if (distSqr > t.SoundRange * t.SoundRange * 2) { continue; }
float dist = (float)Math.Sqrt(distSqr); float dist = (float)Math.Sqrt(distSqr);
if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500 && t.IsWithinSector(transducerCenter)) if (dist > prevPassivePingRadius * Range && dist <= passivePingRadius * Range && Rand.Int(sonarBlips.Count) < 500)
{ {
int prevBlipCount = sonarBlips.Count;
Ping(t.WorldPosition, transducerCenter, Ping(t.WorldPosition, transducerCenter,
Math.Min(t.SoundRange, range * 0.5f) * displayScale, 0, displayScale, Math.Min(t.SoundRange, range * 0.5f), Math.Min(t.SoundRange, range * 0.5f) * displayScale, 0, displayScale, Math.Min(t.SoundRange, range * 0.5f),
passive: true, pingStrength: 0.5f); passive: true, pingStrength: 0.5f);
sonarBlips.Add(new SonarBlip(t.WorldPosition, 1.0f, 1.0f)); sonarBlips.Add(new SonarBlip(t.WorldPosition, 1.0f, 1.0f));
//remove blips that weren't in the AITarget's sector
if (t.HasSector())
{
for (int i = sonarBlips.Count - 1; i >= prevBlipCount; i--)
{
if (!t.IsWithinSector(sonarBlips[i].Position))
{
sonarBlips.RemoveAt(i);
}
}
}
} }
} }
} }
@@ -411,6 +411,14 @@ namespace Barotrauma.Items.Components
SpriteEffects.None, newDepth); SpriteEffects.None, newDepth);
} }
if (GameMain.DebugDraw)
{
Vector2 firingPos = GetRelativeFiringPosition();
firingPos.Y = -firingPos.Y;
GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitX * 5, firingPos + Vector2.UnitX * 5, Color.Red);
GUI.DrawLine(spriteBatch, firingPos - Vector2.UnitY * 5, firingPos + Vector2.UnitY * 5, Color.Red);
}
if (!editing || GUI.DisableHUD || !item.IsSelected) { return; } if (!editing || GUI.DisableHUD || !item.IsSelected) { return; }
const float widgetRadius = 60.0f; const float widgetRadius = 60.0f;
@@ -343,38 +343,31 @@ namespace Barotrauma.Lights
{ {
SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidVertexColor"]; SolidColorEffect.CurrentTechnique = SolidColorEffect.Techniques["SolidVertexColor"];
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: SolidColorEffect, transformMatrix: spriteBatchTransform); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, effect: SolidColorEffect, transformMatrix: spriteBatchTransform);
foreach (Character character in Character.CharacterList) DrawCharacters(spriteBatch, cam, drawDeformSprites: false);
{
if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; }
if (Character.Controlled?.FocusedCharacter == character) { continue; }
Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ?
Color.Black :
character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque();
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.DeformSprite != null) { continue; }
limb.Draw(spriteBatch, cam, lightColor);
}
}
spriteBatch.End(); spriteBatch.End();
DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"]; DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShaderSolidVertexColor"];
DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply(); DeformableSprite.Effect.CurrentTechnique.Passes[0].Apply();
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform); spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, transformMatrix: spriteBatchTransform);
DrawCharacters(spriteBatch, cam, drawDeformSprites: true);
spriteBatch.End();
}
static void DrawCharacters(SpriteBatch spriteBatch, Camera cam, bool drawDeformSprites)
{
foreach (Character character in Character.CharacterList) foreach (Character character in Character.CharacterList)
{ {
if (character.CurrentHull == null || !character.Enabled || !character.IsVisible) { continue; } if (character.CurrentHull == null || !character.Enabled || !character.IsVisible || character.InvisibleTimer > 0.0f) { continue; }
if (Character.Controlled?.FocusedCharacter == character) { continue; } if (Character.Controlled?.FocusedCharacter == character) { continue; }
Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ? Color lightColor = character.CurrentHull.AmbientLight == Color.TransparentBlack ?
Color.Black : Color.Black :
character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque(); character.CurrentHull.AmbientLight.Multiply(character.CurrentHull.AmbientLight.A / 255.0f).Opaque();
foreach (Limb limb in character.AnimController.Limbs) foreach (Limb limb in character.AnimController.Limbs)
{ {
if (limb.DeformSprite == null) { continue; } if (drawDeformSprites == (limb.DeformSprite == null)) { continue; }
limb.Draw(spriteBatch, cam, lightColor); limb.Draw(spriteBatch, cam, lightColor);
} }
} }
spriteBatch.End();
} }
DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShader"]; DeformableSprite.Effect.CurrentTechnique = DeformableSprite.Effect.Techniques["DeformShader"];
@@ -697,17 +697,22 @@ namespace Barotrauma
Vector2 size = new Vector2(Math.Max(nameSize.X, Math.Max(typeSize.X, descSize.X)), nameSize.Y + typeSize.Y + descSize.Y); Vector2 size = new Vector2(Math.Max(nameSize.X, Math.Max(typeSize.X, descSize.X)), nameSize.Y + typeSize.Y + descSize.Y);
int highestSubTier = HighlightedLocation.HighestSubmarineTierAvailable(); int highestSubTier = HighlightedLocation.HighestSubmarineTierAvailable();
var overrideTiers = new List<(SubmarineClass subClass, int tier)>(); List<(SubmarineClass subClass, int tier)> overrideTiers = null;
foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass))) if (HighlightedLocation.CanHaveSubsForSale())
{ {
if (subClass == SubmarineClass.Undefined) { continue; } overrideTiers = new List<(SubmarineClass subClass, int tier)>();
int highestClassTier = HighlightedLocation.HighestSubmarineTierAvailable(subClass); foreach (SubmarineClass subClass in Enum.GetValues(typeof(SubmarineClass)))
if (highestClassTier > 0 && highestClassTier > highestSubTier)
{ {
overrideTiers.Add((subClass, highestClassTier)); if (subClass == SubmarineClass.Undefined) { continue; }
int highestClassTier = HighlightedLocation.HighestSubmarineTierAvailable(subClass);
if (highestClassTier > 0 && highestClassTier > highestSubTier)
{
overrideTiers.Add((subClass, highestClassTier));
}
} }
} }
size.Y += ((highestSubTier > 0 ? 1 : 0) + overrideTiers.Count) * GUIStyle.SmallFont.MeasureString(TextManager.Get("advancedsub.all")).Y; int subAvailabilityTextCount = (highestSubTier > 0 ? 1 : 0) + (overrideTiers?.Count ?? 0);
size.Y += subAvailabilityTextCount * GUIStyle.SmallFont.MeasureString(TextManager.Get("advancedsub.all")).Y;
bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null; bool showReputation = hudVisibility > 0.0f && HighlightedLocation.Discovered && HighlightedLocation.Type.HasOutpost && HighlightedLocation.Reputation != null;
LocalizedString repLabelText = null, repValueText = null; LocalizedString repLabelText = null, repValueText = null;
@@ -745,9 +750,12 @@ namespace Barotrauma
{ {
DrawSubAvailabilityText("advancedsub.all", highestSubTier); DrawSubAvailabilityText("advancedsub.all", highestSubTier);
} }
foreach (var (subClass, tier) in overrideTiers) if (overrideTiers != null)
{ {
DrawSubAvailabilityText($"advancedsub.{subClass}", tier); foreach (var (subClass, tier) in overrideTiers)
{
DrawSubAvailabilityText($"advancedsub.{subClass}", tier);
}
} }
void DrawSubAvailabilityText(string tag, int tier) void DrawSubAvailabilityText(string tag, int tier)
{ {
@@ -14,6 +14,16 @@ namespace Barotrauma.Networking
set; set;
} }
private SoundChannel radioNoiseChannel;
private float radioNoise;
public float RadioNoise
{
get { return radioNoise; }
set { radioNoise = MathHelper.Clamp(value, 0.0f, 1.0f); }
}
private bool mutedLocally; private bool mutedLocally;
public bool MutedLocally public bool MutedLocally
{ {
@@ -42,35 +52,64 @@ namespace Barotrauma.Networking
!HasPermission(ClientPermissions.Kick) && !HasPermission(ClientPermissions.Kick) &&
!HasPermission(ClientPermissions.Unban); !HasPermission(ClientPermissions.Unban);
public void UpdateSoundPosition() public void UpdateVoipSound()
{ {
if (VoipSound == null) { return; } if (VoipSound == null || !VoipSound.IsPlaying)
if (!VoipSound.IsPlaying)
{ {
DebugConsole.Log("Destroying voipsound"); radioNoiseChannel?.Dispose();
VoipSound.Dispose(); radioNoiseChannel = null;
if (VoipSound != null)
{
DebugConsole.Log("Destroying voipsound");
VoipSound.Dispose();
}
VoipSound = null; VoipSound = null;
return; return;
} }
if (Screen.Selected is ModDownloadScreen)
{
VoipSound.Gain = 0.0f;
}
float gain = 1.0f;
float noiseGain = 0.0f;
Vector3? position = null;
if (character != null) if (character != null)
{ {
if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat) if (GameSettings.CurrentConfig.Audio.UseDirectionalVoiceChat)
{ {
VoipSound.SetPosition(new Vector3(character.WorldPosition.X, character.WorldPosition.Y, 0.0f)); position = new Vector3(character.WorldPosition.X, character.WorldPosition.Y, 0.0f);
} }
else else
{ {
VoipSound.SetPosition(null);
float dist = Vector3.Distance(new Vector3(character.WorldPosition, 0.0f), GameMain.SoundManager.ListenerPosition); float dist = Vector3.Distance(new Vector3(character.WorldPosition, 0.0f), GameMain.SoundManager.ListenerPosition);
VoipSound.Gain = 1.0f - MathUtils.InverseLerp(VoipSound.Near, VoipSound.Far, dist); gain = 1.0f - MathUtils.InverseLerp(VoipSound.Near, VoipSound.Far, dist);
}
if (RadioNoise > 0.0f)
{
noiseGain = gain * RadioNoise;
gain *= 1.0f - RadioNoise;
} }
} }
else VoipSound.SetPosition(position);
VoipSound.Gain = gain;
if (noiseGain > 0.0f)
{ {
VoipSound.SetPosition(null); if (radioNoiseChannel == null || !radioNoiseChannel.IsPlaying)
VoipSound.Gain = 1.0f; {
radioNoiseChannel = SoundPlayer.PlaySound("radiostatic");
radioNoiseChannel.Category = "voip";
radioNoiseChannel.Looping = true;
}
radioNoiseChannel.Near = VoipSound.Near;
radioNoiseChannel.Far = VoipSound.Far;
radioNoiseChannel.Position = position;
radioNoiseChannel.Gain = noiseGain;
}
else if (radioNoiseChannel != null)
{
radioNoiseChannel.Gain = 0.0f;
} }
} }
@@ -158,6 +197,11 @@ namespace Barotrauma.Networking
VoipSound.Dispose(); VoipSound.Dispose();
VoipSound = null; VoipSound = null;
} }
if (radioNoiseChannel != null)
{
radioNoiseChannel.Dispose();
radioNoiseChannel = null;
}
} }
} }
} }
@@ -449,7 +449,7 @@ namespace Barotrauma.Networking
foreach (Client c in ConnectedClients) foreach (Client c in ConnectedClients)
{ {
if (c.Character != null && c.Character.Removed) { c.Character = null; } if (c.Character != null && c.Character.Removed) { c.Character = null; }
c.UpdateSoundPosition(); c.UpdateVoipSound();
} }
if (VoipCapture.Instance != null) if (VoipCapture.Instance != null)
@@ -111,7 +111,7 @@ namespace Barotrauma.Networking
foreach (NetIncomingMessage inc in incomingLidgrenMessages) foreach (NetIncomingMessage inc in incomingLidgrenMessages)
{ {
if (!inc.SenderConnection.RemoteEndPoint.Equals(lidgrenEndpoint.NetEndpoint)) if (!inc.SenderConnection.RemoteEndPoint.EquivalentTo(lidgrenEndpoint.NetEndpoint))
{ {
DebugConsole.AddWarning($"Mismatched endpoint: expected {lidgrenEndpoint.NetEndpoint}, got {inc.SenderConnection.RemoteEndPoint}"); DebugConsole.AddWarning($"Mismatched endpoint: expected {lidgrenEndpoint.NetEndpoint}, got {inc.SenderConnection.RemoteEndPoint}");
continue; continue;
@@ -260,6 +260,13 @@ namespace Barotrauma.Networking
} }
} }
} }
if (Screen.Selected is ModDownloadScreen)
{
allowEnqueue = false;
captureTimer = 0;
}
if (allowEnqueue || captureTimer > 0) if (allowEnqueue || captureTimer > 0)
{ {
LastEnqueueAudio = DateTime.Now; LastEnqueueAudio = DateTime.Now;
@@ -1,20 +1,23 @@
using Barotrauma.Sounds; using Barotrauma.Items.Components;
using Barotrauma.Sounds;
using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Barotrauma.Items.Components;
namespace Barotrauma.Networking namespace Barotrauma.Networking
{ {
class VoipClient : IDisposable class VoipClient : IDisposable
{ {
private GameClient gameClient; /// <summary>
private ClientPeer netClient; /// The "near" range of the voice chat (a percentage of either SpeakRange or radio range), further than this the volume starts to diminish
/// </summary>
const float RangeNear = 0.4f;
private readonly GameClient gameClient;
private readonly ClientPeer netClient;
private DateTime lastSendTime; private DateTime lastSendTime;
private List<VoipQueue> queues; private readonly List<VoipQueue> queues;
private UInt16 storedBufferID = 0; private UInt16 storedBufferID = 0;
@@ -32,13 +35,13 @@ namespace Barotrauma.Networking
public void RegisterQueue(VoipQueue queue) public void RegisterQueue(VoipQueue queue)
{ {
if (queue == VoipCapture.Instance) return; if (queue == VoipCapture.Instance) { return; }
if (!queues.Contains(queue)) queues.Add(queue); if (!queues.Contains(queue)) { queues.Add(queue); }
} }
public void UnregisterQueue(VoipQueue queue) public void UnregisterQueue(VoipQueue queue)
{ {
if (queues.Contains(queue)) queues.Remove(queue); if (queues.Contains(queue)) { queues.Remove(queue); }
} }
public void SendToServer() public void SendToServer()
@@ -85,6 +88,7 @@ namespace Barotrauma.Networking
public void Read(IReadMessage msg) public void Read(IReadMessage msg)
{ {
byte queueId = msg.ReadByte(); byte queueId = msg.ReadByte();
float distanceFactor = msg.ReadRangedSingle(0.0f, 1.0f, 8);
VoipQueue queue = queues.Find(q => q.QueueID == queueId); VoipQueue queue = queues.Find(q => q.QueueID == queueId);
if (queue == null) if (queue == null)
@@ -105,9 +109,12 @@ namespace Barotrauma.Networking
client.VoipSound = new VoipSound(client.Name, GameMain.SoundManager, client.VoipQueue); client.VoipSound = new VoipSound(client.Name, GameMain.SoundManager, client.VoipQueue);
} }
GameMain.SoundManager.ForceStreamUpdate(); GameMain.SoundManager.ForceStreamUpdate();
client.RadioNoise = 0.0f;
if (client.Character != null && !client.Character.IsDead && !client.Character.Removed && client.Character.SpeechImpediment <= 100.0f) if (client.Character != null && !client.Character.IsDead && !client.Character.Removed && client.Character.SpeechImpediment <= 100.0f)
{ {
float speechImpedimentMultiplier = 1.0f - client.Character.SpeechImpediment / 100.0f;
bool spectating = Character.Controlled == null;
float rangeMultiplier = spectating ? 2.0f : 1.0f;
WifiComponent radio = null; WifiComponent radio = null;
var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default; var messageType = !client.VoipQueue.ForceLocal && ChatMessage.CanUseRadio(client.Character, out radio) ? ChatMessageType.Radio : ChatMessageType.Default;
client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]); client.Character.ShowSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
@@ -115,11 +122,17 @@ namespace Barotrauma.Networking
client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters; client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
if (messageType == ChatMessageType.Radio) if (messageType == ChatMessageType.Radio)
{ {
client.VoipSound.SetRange(radio.Range * 0.8f, radio.Range); client.VoipSound.SetRange(radio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, radio.Range * speechImpedimentMultiplier * rangeMultiplier);
if (distanceFactor > RangeNear && !spectating)
{
//noise starts increasing exponentially after 40% range
client.RadioNoise = MathF.Pow(MathUtils.InverseLerp(RangeNear, 1.0f, distanceFactor), 2);
}
} }
else else
{ {
client.VoipSound.SetRange(ChatMessage.SpeakRange * 0.4f, ChatMessage.SpeakRange);
client.VoipSound.SetRange(ChatMessage.SpeakRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, ChatMessage.SpeakRange * speechImpedimentMultiplier * rangeMultiplier);
} }
client.VoipSound.UseMuffleFilter = client.VoipSound.UseMuffleFilter =
messageType != ChatMessageType.Radio && Character.Controlled != null && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters && messageType != ChatMessageType.Radio && Character.Controlled != null && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters &&
@@ -35,14 +35,14 @@ namespace Barotrauma
}; };
// New game // New game
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("SaveName"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft);
saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, string.Empty) saveNameBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform), string.Empty)
{ {
textFilterFunction = ToolBox.RemoveInvalidFileNameChars textFilterFunction = ToolBox.RemoveInvalidFileNameChars
}; };
new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft); new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, GUI.IntScale(24)) }, TextManager.Get("MapSeed"), font: GUIStyle.SubHeadingFont, textAlignment: Alignment.BottomLeft);
seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform) { MinSize = new Point(0, 20) }, ToolBox.RandomSeed(8)); seedBox = new GUITextBox(new RectTransform(new Vector2(1.0f, 0.03f), nameSeedLayout.RectTransform), ToolBox.RandomSeed(8));
nameSeedLayout.RectTransform.MinSize = new Point(0, nameSeedLayout.Children.Sum(c => c.RectTransform.MinSize.Y)); nameSeedLayout.RectTransform.MinSize = new Point(0, nameSeedLayout.Children.Sum(c => c.RectTransform.MinSize.Y));
@@ -11,7 +11,7 @@ namespace Barotrauma.Sounds
private VorbisReader reader; private VorbisReader reader;
//key = sample rate, value = filter //key = sample rate, value = filter
private static Dictionary<int, BiQuad> muffleFilters = new Dictionary<int, BiQuad>(); private static readonly Dictionary<int, BiQuad> muffleFilters = new Dictionary<int, BiQuad>();
private static List<float> playbackAmplitude; private static 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
@@ -29,8 +29,8 @@ namespace Barotrauma.Sounds
public override int FillStreamBuffer(int samplePos, short[] buffer) public override int FillStreamBuffer(int samplePos, short[] buffer)
{ {
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 (reader == null) throw new Exception("Called FillStreamBuffer when the reader is null!"); if (reader == null) { throw new Exception("Called FillStreamBuffer when the reader is null!"); }
if (samplePos >= reader.TotalSamples * reader.Channels * 2) return 0; if (samplePos >= reader.TotalSamples * reader.Channels * 2) return 0;
@@ -24,11 +24,12 @@ namespace Barotrauma.Sounds
public readonly bool StreamsReliably; public readonly bool StreamsReliably;
private readonly SoundManager.SourcePoolIndex sourcePoolIndex = SoundManager.SourcePoolIndex.Default;
public virtual SoundManager.SourcePoolIndex SourcePoolIndex public virtual SoundManager.SourcePoolIndex SourcePoolIndex
{ {
get get
{ {
return SoundManager.SourcePoolIndex.Default; return sourcePoolIndex;
} }
} }
@@ -59,13 +60,14 @@ namespace Barotrauma.Sounds
public float BaseNear; public float BaseNear;
public float BaseFar; public float BaseFar;
public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, XElement xElement=null, bool getFullPath=true) public Sound(SoundManager owner, string filename, bool stream, bool streamsReliably, XElement xElement = null, bool getFullPath = true)
{ {
Owner = owner; Owner = owner;
Filename = getFullPath ? Path.GetFullPath(filename.CleanUpPath()).CleanUpPath() : filename; Filename = getFullPath ? Path.GetFullPath(filename.CleanUpPath()).CleanUpPath() : filename;
Stream = stream; Stream = stream;
StreamsReliably = streamsReliably; StreamsReliably = streamsReliably;
XElement = xElement; XElement = xElement;
sourcePoolIndex = XElement.GetAttributeEnum("sourcepool", SoundManager.SourcePoolIndex.Default);
BaseGain = 1.0f; BaseGain = 1.0f;
BaseNear = 100.0f; BaseNear = 100.0f;
@@ -1,9 +1,7 @@
using System; using Microsoft.Xna.Framework;
using OpenAL; using OpenAL;
using Microsoft.Xna.Framework; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Diagnostics;
namespace Barotrauma.Sounds namespace Barotrauma.Sounds
{ {
@@ -17,7 +15,7 @@ namespace Barotrauma.Sounds
public SoundSourcePool(int sourceCount = SoundManager.SOURCE_COUNT) public SoundSourcePool(int sourceCount = SoundManager.SOURCE_COUNT)
{ {
int alError = Al.NoError; int alError;
ALSources = new uint[sourceCount]; ALSources = new uint[sourceCount];
for (int i = 0; i < sourceCount; i++) for (int i = 0; i < sourceCount; i++)
@@ -83,7 +81,7 @@ namespace Barotrauma.Sounds
class SoundChannel : IDisposable class SoundChannel : IDisposable
{ {
private const int STREAM_BUFFER_SIZE = 8820; private const int STREAM_BUFFER_SIZE = 8820;
private short[] streamShortBuffer; private readonly short[] streamShortBuffer;
private string debugName = "SoundChannel"; private string debugName = "SoundChannel";
@@ -312,12 +310,12 @@ namespace Barotrauma.Sounds
if (ALSourceIndex < 0) { return; } if (ALSourceIndex < 0) { return; }
if (!IsPlaying) return; if (!IsPlaying) { return; }
if (!IsStream) if (!IsStream)
{ {
uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex);
int playbackPos; Al.GetSourcei(alSource, Al.SampleOffset, out playbackPos); Al.GetSourcei(alSource, Al.SampleOffset, out int playbackPos);
int alError = Al.GetError(); int alError = Al.GetError();
if (alError != Al.NoError) if (alError != Al.NoError)
{ {
@@ -379,7 +377,7 @@ namespace Barotrauma.Sounds
if (!IsStream) if (!IsStream)
{ {
int playbackPos; Al.GetSourcei(alSource, Al.SampleOffset, out playbackPos); Al.GetSourcei(alSource, Al.SampleOffset, out int playbackPos);
int alError = Al.GetError(); int alError = Al.GetError();
if (alError != Al.NoError) if (alError != Al.NoError)
{ {
@@ -390,7 +388,7 @@ namespace Barotrauma.Sounds
} }
else else
{ {
float retVal = -1.0f; float retVal;
Monitor.Enter(mutex); Monitor.Enter(mutex);
retVal = streamAmplitude; retVal = streamAmplitude;
Monitor.Exit(mutex); Monitor.Exit(mutex);
@@ -432,8 +430,8 @@ namespace Barotrauma.Sounds
private bool reachedEndSample; private bool reachedEndSample;
private int queueStartIndex; private int queueStartIndex;
private readonly uint[] streamBuffers; private readonly uint[] streamBuffers;
private uint[] unqueuedBuffers; private readonly uint[] unqueuedBuffers;
private float[] streamBufferAmplitudes; private readonly float[] streamBufferAmplitudes;
public int StreamSeekPos public int StreamSeekPos
{ {
@@ -448,18 +446,17 @@ namespace Barotrauma.Sounds
} }
} }
private object mutex; private readonly object mutex;
public bool IsPlaying public bool IsPlaying
{ {
get get
{ {
if (ALSourceIndex < 0) return false; if (ALSourceIndex < 0) { return false; }
if (IsStream && !reachedEndSample) return true; if (IsStream && !reachedEndSample) { return true; }
int state;
uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex);
if (!Al.IsSource(alSource)) return false; if (!Al.IsSource(alSource)) { return false; }
Al.GetSourcei(alSource, Al.SourceState, out state); Al.GetSourcei(alSource, Al.SourceState, out int state);
int alError = Al.GetError(); int alError = Al.GetError();
if (alError != Al.NoError) if (alError != Al.NoError)
{ {
@@ -710,8 +707,7 @@ namespace Barotrauma.Sounds
{ {
uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex); uint alSource = Sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex);
int state; Al.GetSourcei(alSource, Al.SourceState, out int state);
Al.GetSourcei(alSource, Al.SourceState, out state);
bool playing = state == Al.Playing; bool playing = state == Al.Playing;
int alError = Al.GetError(); int alError = Al.GetError();
if (alError != Al.NoError) if (alError != Al.NoError)
@@ -719,8 +715,7 @@ namespace Barotrauma.Sounds
throw new Exception("Failed to determine playing state from streamed source: " + debugName + ", " + Al.GetErrorString(alError)); throw new Exception("Failed to determine playing state from streamed source: " + debugName + ", " + Al.GetErrorString(alError));
} }
int unqueuedBufferCount; Al.GetSourcei(alSource, Al.BuffersProcessed, out int unqueuedBufferCount);
Al.GetSourcei(alSource, Al.BuffersProcessed, out unqueuedBufferCount);
alError = Al.GetError(); alError = Al.GetError();
if (alError != Al.NoError) if (alError != Al.NoError)
{ {
@@ -1,10 +1,8 @@
using Barotrauma.IO; using Barotrauma.Networking;
using Barotrauma.Networking;
using Concentus.Structs; using Concentus.Structs;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using OpenAL; using OpenAL;
using System; using System;
using System.Collections.Generic;
namespace Barotrauma.Sounds namespace Barotrauma.Sounds
{ {
@@ -26,12 +24,12 @@ namespace Barotrauma.Sounds
} }
} }
private VoipQueue queue; private readonly VoipQueue queue;
private int bufferID = 0; private int bufferID = 0;
private SoundChannel soundChannel; private SoundChannel soundChannel;
private OpusDecoder decoder; private readonly OpusDecoder decoder;
public bool UseRadioFilter; public bool UseRadioFilter;
public bool UseMuffleFilter; public bool UseMuffleFilter;
@@ -39,11 +37,11 @@ namespace Barotrauma.Sounds
public float Near { get; private set; } public float Near { get; private set; }
public float Far { get; private set; } public float Far { get; private set; }
private BiQuad[] muffleFilters = new BiQuad[] private readonly BiQuad[] muffleFilters = new BiQuad[]
{ {
new LowpassFilter(VoipConfig.FREQUENCY, 800) new LowpassFilter(VoipConfig.FREQUENCY, 800)
}; };
private BiQuad[] radioFilters = new BiQuad[] private readonly BiQuad[] radioFilters = new BiQuad[]
{ {
new BandpassFilter(VoipConfig.FREQUENCY, 2000) new BandpassFilter(VoipConfig.FREQUENCY, 2000)
}; };
@@ -101,13 +99,14 @@ namespace Barotrauma.Sounds
public void ApplyFilters(short[] buffer, int readSamples) public void ApplyFilters(short[] buffer, int readSamples)
{ {
float finalGain = gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume;
for (int i = 0; i < readSamples; i++) for (int i = 0; i < readSamples; i++)
{ {
float fVal = ShortToFloat(buffer[i]); float fVal = ShortToFloat(buffer[i]);
if (gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume > 1.0f) //TODO: take distance into account? if (finalGain > 1.0f) //TODO: take distance into account?
{ {
fVal = Math.Clamp(fVal * gain * GameSettings.CurrentConfig.Audio.VoiceChatVolume, -1f, 1f); fVal = Math.Clamp(fVal * finalGain, -1f, 1f);
} }
if (UseMuffleFilter) if (UseMuffleFilter)
@@ -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>0.20.6.0</Version> <Version>0.20.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</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>0.20.6.0</Version> <Version>0.20.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</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>0.20.6.0</Version> <Version>0.20.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</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>0.20.6.0</Version> <Version>0.20.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</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>0.20.6.0</Version> <Version>0.20.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -9,11 +9,12 @@ namespace Barotrauma.Items.Components
public void ServerEventRead(IReadMessage msg, Client c) public void ServerEventRead(IReadMessage msg, Client c)
{ {
uint recipeHash = msg.ReadUInt32(); uint recipeHash = msg.ReadUInt32();
int amountToFabricate = msg.ReadRangedInteger(1, MaxAmountToFabricate);
item.CreateServerEvent(this); item.CreateServerEvent(this);
if (!item.CanClientAccess(c)) { return; } if (!item.CanClientAccess(c)) { return; }
AmountToFabricate = amountToFabricate;
if (recipeHash == 0) if (recipeHash == 0)
{ {
CancelFabricating(c.Character); CancelFabricating(c.Character);
@@ -24,6 +25,8 @@ namespace Barotrauma.Items.Components
if (fabricatedItem != null && fabricatedItem.RecipeHash == recipeHash) { return; } if (fabricatedItem != null && fabricatedItem.RecipeHash == recipeHash) { return; }
if (recipeHash == 0) { return; } if (recipeHash == 0) { return; }
amountRemaining = AmountToFabricate;
StartFabricating(fabricationRecipes[recipeHash], c.Character); StartFabricating(fabricationRecipes[recipeHash], c.Character);
} }
} }
@@ -56,6 +59,8 @@ namespace Barotrauma.Items.Components
{ {
var componentData = ExtractEventData<EventData>(extraData); var componentData = ExtractEventData<EventData>(extraData);
msg.WriteByte((byte)componentData.State); msg.WriteByte((byte)componentData.State);
msg.WriteRangedInteger(AmountToFabricate, 0, MaxAmountToFabricate);
msg.WriteRangedInteger(amountRemaining, 0, MaxAmountToFabricate);
msg.WriteSingle(timeUntilReady); msg.WriteSingle(timeUntilReady);
uint recipeHash = fabricatedItem?.RecipeHash ?? 0; uint recipeHash = fabricatedItem?.RecipeHash ?? 0;
msg.WriteUInt32(recipeHash); msg.WriteUInt32(recipeHash);
@@ -2,19 +2,18 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text;
namespace Barotrauma.Networking namespace Barotrauma.Networking
{ {
class VoipServer class VoipServer
{ {
private ServerPeer netServer; private readonly ServerPeer netServer;
private List<VoipQueue> queues; private readonly List<VoipQueue> queues;
private Dictionary<VoipQueue,DateTime> lastSendTime; private readonly Dictionary<VoipQueue,DateTime> lastSendTime;
public VoipServer(ServerPeer server) public VoipServer(ServerPeer server)
{ {
this.netServer = server; netServer = server;
queues = new List<VoipQueue>(); queues = new List<VoipQueue>();
lastSendTime = new Dictionary<VoipQueue, DateTime>(); lastSendTime = new Dictionary<VoipQueue, DateTime>();
} }
@@ -46,17 +45,19 @@ namespace Barotrauma.Networking
} }
Client sender = clients.Find(c => c.VoipQueue == queue); Client sender = clients.Find(c => c.VoipQueue == queue);
if (sender == null) { return; }
foreach (Client recipient in clients) foreach (Client recipient in clients)
{ {
if (recipient == sender) { continue; } if (recipient == sender) { continue; }
if (!CanReceive(sender, recipient)) { continue; } if (!CanReceive(sender, recipient, out float distanceFactor)) { continue; }
IWriteMessage msg = new WriteOnlyMessage(); IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.VOICE); msg.WriteByte((byte)ServerPacketHeader.VOICE);
msg.WriteByte((byte)queue.QueueID); msg.WriteByte((byte)queue.QueueID);
msg.WriteRangedSingle(distanceFactor, 0.0f, 1.0f, 8);
queue.Write(msg); queue.Write(msg);
netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable); netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable);
@@ -64,9 +65,15 @@ namespace Barotrauma.Networking
} }
} }
private bool CanReceive(Client sender, Client recipient) private static bool CanReceive(Client sender, Client recipient, out float distanceFactor)
{ {
if (Screen.Selected != GameMain.GameScreen) { return true; } if (Screen.Selected != GameMain.GameScreen)
{
distanceFactor = 0.0f;
return true;
}
distanceFactor = 0.0f;
//no-one can hear muted players //no-one can hear muted players
if (sender.Muted) { return false; } if (sender.Muted) { return false; }
@@ -74,30 +81,47 @@ namespace Barotrauma.Networking
bool recipientSpectating = recipient.Character == null || recipient.Character.IsDead; bool recipientSpectating = recipient.Character == null || recipient.Character.IsDead;
bool senderSpectating = sender.Character == null || sender.Character.IsDead; bool senderSpectating = sender.Character == null || sender.Character.IsDead;
//TODO: only allow spectators to hear the voice chat if close enough to the speaker? //non-spectators cannot hear spectators, and spectators can always hear spectators
if (senderSpectating)
//non-spectators cannot hear spectators {
if (senderSpectating && !recipientSpectating) { return false; } return recipientSpectating;
}
//both spectating, no need to do radio/distance checks
if (recipientSpectating && senderSpectating) { return true; }
//spectators can hear non-spectators
if (!senderSpectating && recipientSpectating) { return true; }
//sender can't speak //sender can't speak
if (sender.Character != null && sender.Character.SpeechImpediment >= 100.0f) { return false; } if (sender.Character != null && sender.Character.SpeechImpediment >= 100.0f) { return false; }
//check if the message can be sent via radio //check if the message can be sent via radio
WifiComponent recipientRadio = null;
if (!sender.VoipQueue.ForceLocal && if (!sender.VoipQueue.ForceLocal &&
ChatMessage.CanUseRadio(sender.Character, out WifiComponent senderRadio) && ChatMessage.CanUseRadio(sender.Character, out WifiComponent senderRadio) &&
ChatMessage.CanUseRadio(recipient.Character, out WifiComponent recipientRadio)) (recipientSpectating || ChatMessage.CanUseRadio(recipient.Character, out recipientRadio)))
{ {
if (recipientRadio.CanReceive(senderRadio)) { return true; } if (recipientSpectating)
{
if (recipient.SpectatePos == null) { return true; }
distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / senderRadio.Range, 0.0f, 1.0f);
return distanceFactor < 1.0f;
}
else if (recipientRadio != null && recipientRadio.CanReceive(senderRadio))
{
distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.Character.WorldPosition) / senderRadio.Range, 0.0f, 1.0f);
return true;
}
} }
//otherwise do a distance check if (recipientSpectating)
return ChatMessage.GetGarbleAmount(recipient.Character, sender.Character, ChatMessage.SpeakRange) < 1.0f; {
if (recipient.SpectatePos == null) { return true; }
distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.SpectatePos.Value) / ChatMessage.SpeakRange, 0.0f, 1.0f);
return distanceFactor < 1.0f;
}
else
{
//otherwise do a distance check
float garbleAmount = ChatMessage.GetGarbleAmount(recipient.Character, sender.Character, ChatMessage.SpeakRange);
distanceFactor = garbleAmount;
return garbleAmount < 1.0f;
}
} }
} }
} }
@@ -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>0.20.6.0</Version> <Version>0.20.7.0</Version>
<Copyright>Copyright © FakeFish 2018-2022</Copyright> <Copyright>Copyright © FakeFish 2018-2022</Copyright>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName> <AssemblyName>DedicatedServer</AssemblyName>
@@ -102,6 +102,8 @@ namespace Barotrauma
private bool inDetectable; private bool inDetectable;
public double InDetectableSetTime;
/// <summary> /// <summary>
/// Should be reset to false each frame and kept indetectable by e.g. a status effect. /// Should be reset to false each frame and kept indetectable by e.g. a status effect.
/// </summary> /// </summary>
@@ -115,7 +117,8 @@ namespace Barotrauma
{ {
inDetectable = value; inDetectable = value;
if (inDetectable) if (inDetectable)
{ {
InDetectableSetTime = Timing.TotalTime;
NeedsUpdate = true; NeedsUpdate = true;
} }
} }
@@ -257,9 +260,14 @@ namespace Barotrauma
SightRange -= speed * deltaTime * (MaxSightRange / FadeOutTime); SightRange -= speed * deltaTime * (MaxSightRange / FadeOutTime);
} }
public bool HasSector()
{
return sectorRad < MathHelper.TwoPi;
}
public bool IsWithinSector(Vector2 worldPosition) public bool IsWithinSector(Vector2 worldPosition)
{ {
if (sectorRad >= MathHelper.TwoPi) { return true; } if (!HasSector()) { return true; }
Vector2 diff = worldPosition - WorldPosition; Vector2 diff = worldPosition - WorldPosition;
return Math.Abs(MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir))) <= sectorRad * 0.5f; return Math.Abs(MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir))) <= sectorRad * 0.5f;
} }
@@ -1083,7 +1083,7 @@ namespace Barotrauma
State = AIState.Idle; State = AIState.Idle;
return; return;
} }
else else if (!owner.HasAbilityFlag(AbilityFlags.IgnoredByEnemyAI))
{ {
SelectedAiTarget = owner.AiTarget; SelectedAiTarget = owner.AiTarget;
} }
@@ -2147,7 +2147,6 @@ namespace Barotrauma
if (SelectedAiTarget?.Entity == null) { return false; } if (SelectedAiTarget?.Entity == null) { return false; }
if (AttackLimb?.attack == null) { return false; } if (AttackLimb?.attack == null) { return false; }
if (damageTarget == null) { return false; } if (damageTarget == null) { return false; }
ActiveAttack = AttackLimb.attack;
if (wallTarget != null) if (wallTarget != null)
{ {
// If the selected target is not the wall target, make the wall target the selected target. // If the selected target is not the wall target, make the wall target the selected target.
@@ -2156,9 +2155,10 @@ namespace Barotrauma
{ {
SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority); SelectTarget(aiTarget, GetTargetMemory(SelectedAiTarget, addIfNotFound: true).Priority);
State = AIState.Attack; State = AIState.Attack;
return true;
} }
} }
if (damageTarget == null) { return false; } ActiveAttack = AttackLimb.attack;
if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0) if (ActiveAttack.Ranged && ActiveAttack.RequiredAngleToShoot > 0)
{ {
Limb referenceLimb = GetLimbToRotate(ActiveAttack); Limb referenceLimb = GetLimbToRotate(ActiveAttack);
@@ -3810,6 +3810,7 @@ namespace Barotrauma
{ {
SelectTarget(door.Item.AiTarget, SelectedTargetMemory.Priority); SelectTarget(door.Item.AiTarget, SelectedTargetMemory.Priority);
State = AIState.Attack; State = AIState.Attack;
return false;
} }
} }
} }
@@ -3881,9 +3882,8 @@ namespace Barotrauma
return targetLimb; return targetLimb;
} }
private Character GetOwner(Item item) private static Character GetOwner(Item item)
{ {
// If the item is held by a character, attack the character instead.
var pickable = item.GetComponent<Pickable>(); var pickable = item.GetComponent<Pickable>();
if (pickable != null) if (pickable != null)
{ {
@@ -413,28 +413,10 @@ namespace Barotrauma
} }
} }
} }
private void ApplyTreatment(Affliction affliction, Item item) private void ApplyTreatment(Affliction affliction, Item item)
{ {
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction); item.ApplyTreatment(character, targetCharacter, targetCharacter.CharacterHealth.GetAfflictionLimb(affliction));
bool remove = false;
foreach (ItemComponent ic in item.Components)
{
if (!ic.HasRequiredContainedItems(user: character, addMessage: false)) { continue; }
#if CLIENT
ic.PlaySound(ActionType.OnUse, character);
#endif
ic.WasUsed = true;
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, user: character);
if (ic.DeleteOnUse)
{
remove = true;
}
}
if (remove)
{
Entity.Spawner?.AddItemToRemoveQueue(item);
}
} }
protected override bool CheckObjectiveSpecific() protected override bool CheckObjectiveSpecific()
@@ -1371,7 +1371,7 @@ namespace Barotrauma
Limb head = GetLimb(LimbType.Head); Limb head = GetLimb(LimbType.Head);
Limb torso = GetLimb(LimbType.Torso); Limb torso = GetLimb(LimbType.Torso);
Vector2 headDiff = targetHead == null ? diff : targetHead.SimPosition - character.SimPosition; Vector2 headDiff = targetHead == null ? diff : targetHead.SimPosition - character.SimPosition;
targetMovement = new Vector2(diff.X, 0.0f); targetMovement = new Vector2(diff.X, 0.0f);
TargetDir = headDiff.X > 0.0f ? Direction.Right : Direction.Left; TargetDir = headDiff.X > 0.0f ? Direction.Right : Direction.Left;
@@ -1386,15 +1386,25 @@ namespace Barotrauma
float prevVitality = target.Vitality; float prevVitality = target.Vitality;
bool wasCritical = prevVitality < 0.0f; bool wasCritical = prevVitality < 0.0f;
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) //Serverside code
{ {
target.Oxygen += deltaTime * 0.5f; //Stabilize them target.Oxygen += deltaTime * 0.5f; //Stabilize them
} }
float cprBoost = character.GetStatValue(StatTypes.CPRBoost); float cprBoost = character.GetStatValue(StatTypes.CPRBoost);
int skill = (int)character.GetSkillLevel("medical"); int skill = (int)character.GetSkillLevel("medical");
if (GameMain.NetworkMember is not { IsClient: true })
{
if (cprBoost >= 1f)
{
//prevent the patient from suffocating no matter how fast their oxygen level is dropping
target.Oxygen = Math.Max(target.Oxygen, -10.0f);
}
}
//pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17) //pump for 15 seconds (cprAnimTimer 0-15), then do mouth-to-mouth for 2 seconds (cprAnimTimer 15-17)
if (cprAnimTimer > 15.0f && targetHead != null && head != null) if (cprAnimTimer > 15.0f && targetHead != null && head != null)
{ {
@@ -1405,23 +1415,15 @@ namespace Barotrauma
torso.PullJointEnabled = true; torso.PullJointEnabled = true;
//Serverside code //Serverside code
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) if (GameMain.NetworkMember is not { IsClient: true })
{ {
if (target.Oxygen < -10.0f) if (target.Oxygen < -10.0f)
{ {
if (cprBoost >= 1f) //stabilize the oxygen level but don't allow it to go positive and revive the character yet
{ float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
//prevent the patient from suffocating no matter how fast their oxygen level is dropping stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
target.Oxygen = Math.Max(target.Oxygen, -10.0f); character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
} if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
else
{
//stabilize the oxygen level but don't allow it to go positive and revive the character yet
float stabilizationAmount = skill * CPRSettings.Active.StabilizationPerSkill;
stabilizationAmount = MathHelper.Clamp(stabilizationAmount, CPRSettings.Active.StabilizationMin, CPRSettings.Active.StabilizationMax);
character.Oxygen -= 1.0f / stabilizationAmount * deltaTime; //Worse skill = more oxygen required
if (character.Oxygen > 0.0f) { target.Oxygen += stabilizationAmount * deltaTime; } //we didn't suffocate yet did we
}
} }
} }
} }
@@ -25,6 +25,8 @@ namespace Barotrauma
FriendlyNPC = 3 FriendlyNPC = 3
} }
public readonly record struct TalentResistanceIdentifier(Identifier ResistanceIdentifier, Identifier TalentIdentifier);
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerPositionSync
{ {
public readonly static List<Character> CharacterList = new List<Character>(); public readonly static List<Character> CharacterList = new List<Character>();
@@ -347,7 +349,7 @@ namespace Barotrauma
private readonly Dictionary<ItemPrefab, double> itemSelectedDurations = new Dictionary<ItemPrefab, double>(); private readonly Dictionary<ItemPrefab, double> itemSelectedDurations = new Dictionary<ItemPrefab, double>();
private double itemSelectedTime; private double itemSelectedTime;
public float InvisibleTimer; public float InvisibleTimer { get; set; }
public readonly CharacterPrefab Prefab; public readonly CharacterPrefab Prefab;
@@ -1372,7 +1374,28 @@ namespace Barotrauma
tags.RemoveWhere(t => t.StartsWith("variant")); tags.RemoveWhere(t => t.StartsWith("variant"));
tags.Add($"variant{headId.Value}".ToIdentifier()); tags.Add($"variant{headId.Value}".ToIdentifier());
} }
var oldHeadInfo = Info.Head;
Info.RecreateHead(tags.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex); Info.RecreateHead(tags.ToImmutableHashSet(), hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
if (hairIndex == -1)
{
Info.Head.HairIndex = oldHeadInfo.HairIndex;
}
if (beardIndex == -1)
{
Info.Head.BeardIndex = oldHeadInfo.BeardIndex;
}
if (moustacheIndex == -1)
{
Info.Head.MoustacheIndex = oldHeadInfo.MoustacheIndex;
}
if (faceAttachmentIndex == -1)
{
Info.Head.FaceAttachmentIndex = oldHeadInfo.FaceAttachmentIndex;
}
Info.Head.SkinColor = oldHeadInfo.SkinColor;
Info.Head.HairColor = oldHeadInfo.HairColor;
Info.Head.FacialHairColor = oldHeadInfo.FacialHairColor;
Info.CheckColors();
#if CLIENT #if CLIENT
head.RecreateSprites(); head.RecreateSprites();
#endif #endif
@@ -3050,7 +3073,8 @@ namespace Barotrauma
ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime); ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime); ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (aiTarget != null) //wait 0.1 seconds so status effects that continuously set InDetectable to true can keep the character InDetectable
if (aiTarget != null && Timing.TotalTime > aiTarget.InDetectableSetTime + 0.1f)
{ {
aiTarget.InDetectable = false; aiTarget.InDetectable = false;
} }
@@ -5087,25 +5111,48 @@ namespace Barotrauma
return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag); return abilityFlags.HasFlag(abilityFlag) || CharacterHealth.HasFlag(abilityFlag);
} }
private readonly Dictionary<Identifier, float> abilityResistances = new Dictionary<Identifier, float>(); private readonly Dictionary<TalentResistanceIdentifier, float> abilityResistances = new();
public float GetAbilityResistance(AfflictionPrefab affliction) public float GetAbilityResistance(AfflictionPrefab affliction)
{ {
return abilityResistances.TryGetValue(affliction.Identifier, out float value) ? value : abilityResistances.TryGetValue(affliction.AfflictionType, out float typeValue) ? typeValue : 1f; float resistance = 0f;
bool hadResistance = false;
foreach (var (key, value) in abilityResistances)
{
if (key.ResistanceIdentifier == affliction.AfflictionType ||
key.ResistanceIdentifier == affliction.Identifier)
{
resistance += value;
hadResistance = true;
}
}
return hadResistance ? resistance : 1f;
} }
public void ChangeAbilityResistance(Identifier resistanceId, float value) public void ChangeAbilityResistance(TalentResistanceIdentifier identifier, float value)
{ {
if (abilityResistances.ContainsKey(resistanceId)) if (!MathUtils.IsValid(value))
{ {
abilityResistances[resistanceId] *= value; #if DEBUG
DebugConsole.ThrowError($"Attempted to set ability resistance to an invalid value ({value})\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
if (abilityResistances.ContainsKey(identifier))
{
abilityResistances[identifier] *= value;
} }
else else
{ {
abilityResistances.Add(resistanceId, value); abilityResistances.Add(identifier, value);
} }
} }
public void RemoveAbilityResistance(TalentResistanceIdentifier identifier) => abilityResistances.Remove(identifier);
/// <summary> /// <summary>
/// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs /// Compares just the species name and the group, ignores teams. There's a more complex version found in HumanAIController.cs
/// </summary> /// </summary>
@@ -63,27 +63,34 @@ namespace Barotrauma
public readonly CharacterInfo CharacterInfo; public readonly CharacterInfo CharacterInfo;
public readonly HeadPreset Preset; public readonly HeadPreset Preset;
private int hairIndex; public int HairIndex { get; set; }
public int HairIndex private int? hairWithHatIndex;
public void SetHairWithHatIndex()
{ {
get => hairIndex; if (CharacterInfo.Hairs is null)
set
{ {
hairIndex = value; if (HairIndex == -1)
if (CharacterInfo.Hairs is null)
{ {
HairWithHatIndex = value; #if DEBUG
return; DebugConsole.ThrowError("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
#else
DebugConsole.AddWarning("Setting \"hairWithHatIndex\" before \"Hairs\" are defined!");
#endif
} }
HairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", hairIndex) ?? -1; hairWithHatIndex = HairIndex;
if (HairWithHatIndex < 0 || HairWithHatIndex >= CharacterInfo.Hairs.Count) }
else
{
hairWithHatIndex = HairElement?.GetAttributeInt("replacewhenwearinghat", HairIndex) ?? -1;
if (hairWithHatIndex < 0 || hairWithHatIndex >= CharacterInfo.Hairs.Count)
{ {
HairWithHatIndex = hairIndex; hairWithHatIndex = HairIndex;
} }
} }
} }
public int HairWithHatIndex { get; private set; }
public int BeardIndex; public int BeardIndex;
public int MoustacheIndex; public int MoustacheIndex;
public int FaceAttachmentIndex; public int FaceAttachmentIndex;
@@ -99,26 +106,29 @@ namespace Barotrauma
get get
{ {
if (CharacterInfo.Hairs == null) { return null; } if (CharacterInfo.Hairs == null) { return null; }
if (hairIndex >= CharacterInfo.Hairs.Count) if (HairIndex >= CharacterInfo.Hairs.Count)
{ {
DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairIndex})"); DebugConsole.AddWarning($"Hair index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairIndex})");
} }
return CharacterInfo.Hairs.ElementAtOrDefault(hairIndex); return CharacterInfo.Hairs.ElementAtOrDefault(HairIndex);
} }
} }
public ContentXElement HairWithHatElement public ContentXElement HairWithHatElement
{ {
get get
{ {
if (CharacterInfo.Hairs == null) { return null; } if (hairWithHatIndex == null)
if (HairWithHatIndex >= CharacterInfo.Hairs.Count)
{ {
DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {HairWithHatIndex})"); SetHairWithHatIndex();
} }
return CharacterInfo.Hairs.ElementAtOrDefault(HairWithHatIndex); if (CharacterInfo.Hairs == null) { return null; }
if (hairWithHatIndex >= CharacterInfo.Hairs.Count)
{
DebugConsole.AddWarning($"Hair with hat index out of range (character: {CharacterInfo?.Name ?? "null"}, index: {hairWithHatIndex})");
}
return CharacterInfo.Hairs.ElementAtOrDefault(hairWithHatIndex.Value);
} }
} }
public ContentXElement BeardElement public ContentXElement BeardElement
{ {
get get
@@ -711,7 +721,7 @@ namespace Barotrauma
private bool IsColorValid(in Color clr) private bool IsColorValid(in Color clr)
=> clr.R != 0 || clr.G != 0 || clr.B != 0; => clr.R != 0 || clr.G != 0 || clr.B != 0;
private void CheckColors() public void CheckColors()
{ {
if (!IsColorValid(Head.HairColor)) if (!IsColorValid(Head.HairColor))
{ {
@@ -27,6 +27,14 @@ namespace Barotrauma
get { return _strength; } get { return _strength; }
set set
{ {
if (!MathUtils.IsValid(value))
{
#if DEBUG
DebugConsole.ThrowError($"Attempted to set an affliction to an invalid strength ({value})\n" + Environment.StackTrace.CleanupStackTrace());
#endif
return;
}
if (_nonClampedStrength < 0 && value > 0) if (_nonClampedStrength < 0 && value > 0)
{ {
_nonClampedStrength = value; _nonClampedStrength = value;
@@ -440,9 +448,9 @@ namespace Barotrauma
{ {
statusEffect.Apply(type, deltaTime, characterHealth.Character, targetLimb); statusEffect.Apply(type, deltaTime, characterHealth.Character, targetLimb);
} }
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs)) if (characterHealth?.Character?.AnimController?.Limbs != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{ {
statusEffect.Apply(type, deltaTime, targetLimb.character, targets: targetLimb.character.AnimController.Limbs); statusEffect.Apply(type, deltaTime, characterHealth.Character, targets: characterHealth.Character.AnimController.Limbs);
} }
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) || if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters)) statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
@@ -405,45 +405,53 @@ namespace Barotrauma
var root = doc.Root.FromPackage(pathToAppendage.ContentPackage); var root = doc.Root.FromPackage(pathToAppendage.ContentPackage);
var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e); var limbElements = root.GetChildElements("limb").ToDictionary(e => e.GetAttributeString("id", null), e => e);
//the IDs may need to be offset if the character has other extra appendages (e.g. from gene splicing)
//that take up the IDs of this appendage
int idOffset = 0;
foreach (var jointElement in root.GetChildElements("joint")) foreach (var jointElement in root.GetChildElements("joint"))
{ {
if (limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) if (!limbElements.TryGetValue(jointElement.GetAttributeString("limb2", null), out ContentXElement limbElement)) { continue; }
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams);
Limb attachLimb = null;
if (matchingAffliction.AttachLimbId > -1)
{ {
var jointParams = new RagdollParams.JointParams(jointElement, ragdoll.RagdollParams); attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId);
Limb attachLimb = null;
if (matchingAffliction.AttachLimbId > -1)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == matchingAffliction.AttachLimbId);
}
else if (matchingAffliction.AttachLimbName != null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName);
}
else if (matchingAffliction.AttachLimbType != LimbType.None)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType);
}
if (attachLimb == null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1);
}
if (attachLimb != null)
{
jointParams.Limb1 = attachLimb.Params.ID;
var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams)
{
// Ensure that we have a valid id for the new limb
ID = ragdoll.Limbs.Length
};
jointParams.Limb2 = appendageLimbParams.ID;
Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
} }
else if (matchingAffliction.AttachLimbName != null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Name == matchingAffliction.AttachLimbName);
}
else if (matchingAffliction.AttachLimbType != LimbType.None)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.type == matchingAffliction.AttachLimbType);
}
if (attachLimb == null)
{
attachLimb = ragdoll.Limbs.FirstOrDefault(l => !l.IsSevered && l.Params.ID == jointParams.Limb1);
}
if (attachLimb != null)
{
jointParams.Limb1 = attachLimb.Params.ID;
//the joint attaches to a limb outside the character's normal limb count = to another part of the appendage
// -> if the appendage's IDs have been offset, we need to take that into account to attach to the correct limb
if (jointParams.Limb1 >= ragdoll.RagdollParams.Limbs.Count)
{
jointParams.Limb1 += idOffset;
}
var appendageLimbParams = new RagdollParams.LimbParams(limbElement, ragdoll.RagdollParams);
if (idOffset == 0)
{
idOffset = ragdoll.Limbs.Length - appendageLimbParams.ID;
}
jointParams.Limb2 = appendageLimbParams.ID = ragdoll.Limbs.Length;
Limb huskAppendage = new Limb(ragdoll, character, appendageLimbParams);
huskAppendage.body.Submarine = character.Submarine;
huskAppendage.body.SetTransform(attachLimb.SimPosition, attachLimb.Rotation);
ragdoll.AddLimb(huskAppendage);
ragdoll.AddJoint(jointParams);
appendage.Add(huskAppendage);
}
} }
return appendage; return appendage;
} }
@@ -887,7 +887,7 @@ namespace Barotrauma
//clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely) //clamp above 0.1 (no amount of oxygen low resistance should keep the character alive indefinitely)
float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance); float decreaseSpeed = Math.Max(0.1f, 1f - oxygenlowResistance);
//the character dies of oxygen deprivation in 100 seconds after losing consciousness //the character dies of oxygen deprivation in 100 seconds after losing consciousness
OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f); OxygenAmount = MathHelper.Clamp(OxygenAmount - decreaseSpeed * deltaTime, -100.0f, 100.0f);
} }
else else
{ {
@@ -895,15 +895,21 @@ namespace Barotrauma
float increaseSpeed = 10.0f; float increaseSpeed = 10.0f;
decreaseSpeed *= (1f - oxygenlowResistance); decreaseSpeed *= (1f - oxygenlowResistance);
increaseSpeed *= (1f + oxygenlowResistance); increaseSpeed *= (1f + oxygenlowResistance);
float holdBreathMultiplier = Character.GetStatValue(StatTypes.HoldBreathMultiplier);
float holdBreathMultiplier = 1f + GetStatValue(StatTypes.HoldBreathMultiplier); if (holdBreathMultiplier <= -1.0f)
decreaseSpeed *= holdBreathMultiplier; {
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f); OxygenAmount = -100.0f;
}
else
{
decreaseSpeed /= 1.0f + Character.GetStatValue(StatTypes.HoldBreathMultiplier);
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? decreaseSpeed : increaseSpeed), -100.0f, 100.0f);
}
} }
UpdateOxygenProjSpecific(prevOxygen, deltaTime); UpdateOxygenProjSpecific(prevOxygen, deltaTime);
} }
partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime); partial void UpdateOxygenProjSpecific(float prevOxygen, float deltaTime);
partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime); partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime);
@@ -2,7 +2,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -1,5 +1,5 @@
using System.Collections.Immutable; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework; using System.Collections.Immutable;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -1,7 +1,4 @@
using Microsoft.Xna.Framework; namespace Barotrauma.Abilities
using System.Xml.Linq;
namespace Barotrauma.Abilities
{ {
class CharacterAbilityGainSimultaneousSkill : CharacterAbility class CharacterAbilityGainSimultaneousSkill : CharacterAbility
{ {
@@ -15,6 +12,10 @@ namespace Barotrauma.Abilities
skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", ""); skillIdentifier = abilityElement.GetAttributeIdentifier("skillidentifier", "");
ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true); ignoreAbilitySkillGain = abilityElement.GetAttributeBool("ignoreabilityskillgain", true);
targetAllies = abilityElement.GetAttributeBool("targetallies", false); targetAllies = abilityElement.GetAttributeBool("targetallies", false);
if (skillIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}: skill identifier not defined.");
}
} }
protected override void ApplyEffect(AbilityObject abilityObject) protected override void ApplyEffect(AbilityObject abilityObject)
@@ -23,13 +24,12 @@ namespace Barotrauma.Abilities
{ {
if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; } if (ignoreAbilitySkillGain && abilitySkillGain.GainedFromAbility) { return; }
Identifier identifier = skillIdentifier == "inherit" ? abilitySkillGain.SkillIdentifier : skillIdentifier; Identifier identifier = skillIdentifier == "inherit" ? abilitySkillGain.SkillIdentifier : skillIdentifier;
if (targetAllies) if (targetAllies)
{ {
foreach (Character character in Character.GetFriendlyCrew(Character)) foreach (Character otherCharacter in Character.GetFriendlyCrew(Character))
{ {
if (character == Character) { continue; } if (otherCharacter == Character) { continue; }
Character.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true); otherCharacter.Info?.IncreaseSkillLevel(identifier, abilitySkillGain.Value, gainedFromAbility: true);
} }
} }
else else
@@ -1,6 +1,4 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityGiveAffliction : CharacterAbility class CharacterAbilityGiveAffliction : CharacterAbility
{ {
@@ -18,7 +16,7 @@ namespace Barotrauma.Abilities
if (afflictionId.IsEmpty) if (afflictionId.IsEmpty)
{ {
DebugConsole.ThrowError("Error in CharacterAbilityGiveAffliction - affliction identifier not set."); DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveAffliction - affliction identifier not set.");
} }
} }
@@ -1,6 +1,4 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityGiveFlag : CharacterAbility class CharacterAbilityGiveFlag : CharacterAbility
{ {
@@ -1,6 +1,4 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityGiveMoney : CharacterAbility class CharacterAbilityGiveMoney : CharacterAbility
{ {
@@ -13,6 +11,11 @@ namespace Barotrauma.Abilities
{ {
amount = abilityElement.GetAttributeInt("amount", 0); amount = abilityElement.GetAttributeInt("amount", 0);
scalingStatIdentifier = abilityElement.GetAttributeIdentifier("scalingstatidentifier", Identifier.Empty); scalingStatIdentifier = abilityElement.GetAttributeIdentifier("scalingstatidentifier", Identifier.Empty);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, CharacterAbilityGiveMoney - amount of money set to 0.");
}
} }
private void ApplyEffectSpecific(Character targetCharacter) private void ApplyEffectSpecific(Character targetCharacter)
@@ -1,6 +1,4 @@
using System; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
public enum PermanentStatPlaceholder public enum PermanentStatPlaceholder
{ {
@@ -28,6 +26,10 @@ namespace Barotrauma.Abilities
public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityGivePermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent \"{CharacterTalent.DebugIdentifier}\" - stat identifier not defined.");
}
string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty); string statTypeName = abilityElement.GetAttributeString("stattype", string.Empty);
statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier); statType = string.IsNullOrEmpty(statTypeName) ? StatTypes.None : CharacterAbilityGroup.ParseStatType(statTypeName, CharacterTalent.DebugIdentifier);
value = abilityElement.GetAttributeFloat("value", 0f); value = abilityElement.GetAttributeFloat("value", 0f);
@@ -11,6 +11,14 @@ namespace Barotrauma.Abilities
{ {
factionIdentifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); factionIdentifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
amount = abilityElement.GetAttributeFloat("amount", 0f); amount = abilityElement.GetAttributeFloat("amount", 0f);
if (factionIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, faction identifier not defined.");
}
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of reputation to give is 0.");
}
} }
protected override void ApplyEffect() protected override void ApplyEffect()
@@ -1,6 +1,4 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityGiveResistance : CharacterAbility class CharacterAbilityGiveResistance : CharacterAbility
{ {
@@ -10,17 +8,23 @@ namespace Barotrauma.Abilities
public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityGiveResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty)); resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty));
multiplier = abilityElement.GetAttributeFloat("multiplier", 1f); // rename this to resistance for consistency multiplier = abilityElement.GetAttributeFloat("multiplier", 1f);
if (resistanceId.IsEmpty) if (resistanceId.IsEmpty)
{ {
DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set."); DebugConsole.ThrowError("Error in CharacterAbilityGiveResistance - resistance identifier not set.");
} }
if (MathUtils.NearlyEqual(multiplier, 1))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - multiplier set to 1, which will do nothing.");
}
} }
public override void InitializeAbility(bool addingFirstTime) public override void InitializeAbility(bool addingFirstTime)
{ {
Character.ChangeAbilityResistance(resistanceId, multiplier); TalentResistanceIdentifier identifier = new(resistanceId, CharacterTalent.Prefab.Identifier);
Character.ChangeAbilityResistance(identifier, multiplier);
} }
} }
} }
@@ -1,6 +1,4 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityGiveStat : CharacterAbility class CharacterAbilityGiveStat : CharacterAbility
{ {
@@ -1,7 +1,4 @@
using Barotrauma.Extensions; namespace Barotrauma.Abilities
using System.Xml.Linq;
namespace Barotrauma.Abilities
{ {
class CharacterAbilityGiveTalentPoints : CharacterAbility class CharacterAbilityGiveTalentPoints : CharacterAbility
{ {
@@ -9,7 +6,11 @@ namespace Barotrauma.Abilities
public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityGiveTalentPoints(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
amount = abilityElement.GetAttributeInt("amount", 0); amount = abilityElement.GetAttributeInt("amount", 0);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.");
}
} }
public override void InitializeAbility(bool addingFirstTime) public override void InitializeAbility(bool addingFirstTime)
@@ -9,6 +9,10 @@ namespace Barotrauma.Abilities
public CharacterAbilityGiveTalentPointsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityGiveTalentPointsToAllies(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
amount = abilityElement.GetAttributeInt("amount", 0); amount = abilityElement.GetAttributeInt("amount", 0);
if (amount == 0)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, amount of talent points to give is 0.");
}
} }
public override void InitializeAbility(bool addingFirstTime) public override void InitializeAbility(bool addingFirstTime)
@@ -1,6 +1,4 @@
using Barotrauma.Extensions; using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -6,6 +6,10 @@ namespace Barotrauma.Abilities
public CharacterAbilityMarkAsLooted(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityMarkAsLooted(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, identifier is empty in {nameof(CharacterAbilityMarkAsLooted)}.");
}
} }
protected override void ApplyEffect(AbilityObject abilityObject) protected override void ApplyEffect(AbilityObject abilityObject)
@@ -1,7 +1,4 @@
using System; namespace Barotrauma.Abilities
using System.Xml.Linq;
namespace Barotrauma.Abilities
{ {
class CharacterAbilityModifyFlag : CharacterAbility class CharacterAbilityModifyFlag : CharacterAbility
{ {
@@ -1,23 +1,25 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityModifyResistance : CharacterAbility class CharacterAbilityModifyResistance : CharacterAbility
{ {
private readonly Identifier resistanceId; private readonly Identifier resistanceId;
private readonly float resistance; private readonly float multiplier;
bool lastState; bool lastState;
public override bool AllowClientSimulation => true; public override bool AllowClientSimulation => true;
// should probably be split to different classes // should probably be split to different classes
public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityModifyResistance(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", ""); resistanceId = abilityElement.GetAttributeIdentifier("resistanceid", abilityElement.GetAttributeIdentifier("resistance", Identifier.Empty));
resistance = abilityElement.GetAttributeFloat("resistance", 1f); multiplier = abilityElement.GetAttributeFloat("multiplier", 1f);
if (resistanceId.IsEmpty) if (resistanceId.IsEmpty)
{ {
DebugConsole.ThrowError("Error in CharacterAbilityModifyResistance - resistance identifier not set."); DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier} - resistance identifier not set in {nameof(CharacterAbilityModifyResistance)}.");
}
if (MathUtils.NearlyEqual(multiplier, 1.0f))
{
DebugConsole.AddWarning($"Possible error in talent {CharacterTalent.DebugIdentifier} - resistance set to 1, which will do nothing.");
} }
} }
@@ -25,7 +27,15 @@ namespace Barotrauma.Abilities
{ {
if (conditionsMatched != lastState) if (conditionsMatched != lastState)
{ {
Character.ChangeAbilityResistance(resistanceId, conditionsMatched ? resistance : 1 / resistance); TalentResistanceIdentifier identifier = new(resistanceId, CharacterTalent.Prefab.Identifier);
if (conditionsMatched)
{
Character.ChangeAbilityResistance(identifier, multiplier);
}
else
{
Character.RemoveAbilityResistance(identifier);
}
lastState = conditionsMatched; lastState = conditionsMatched;
} }
} }
@@ -1,6 +1,4 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityModifyStat : CharacterAbility class CharacterAbilityModifyStat : CharacterAbility
{ {
@@ -1,5 +1,4 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -1,5 +1,4 @@
using System.Linq; using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -1,6 +1,4 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityModifyValue : CharacterAbility class CharacterAbilityModifyValue : CharacterAbility
{ {
@@ -11,6 +9,10 @@ namespace Barotrauma.Abilities
{ {
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f); multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
if (MathUtils.NearlyEqual(addedValue, 0.0f) && MathUtils.NearlyEqual(multiplyValue, 1.0f))
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityModifyValue)} - added value is 0 and multiplier is 1, the ability will do nothing.");
}
} }
protected override void ApplyEffect(AbilityObject abilityObject) protected override void ApplyEffect(AbilityObject abilityObject)
@@ -1,6 +1,4 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityPutItem : CharacterAbility class CharacterAbilityPutItem : CharacterAbility
{ {
@@ -9,7 +9,11 @@ namespace Barotrauma.Abilities
public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityResetPermanentStat(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty); statIdentifier = abilityElement.GetAttributeIdentifier("statidentifier", Identifier.Empty);
if (statIdentifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilityResetPermanentStat)} - statIdentifier is empty.");
}
} }
protected override void ApplyEffect(AbilityObject abilityObject) protected override void ApplyEffect(AbilityObject abilityObject)
{ {
@@ -1,7 +1,4 @@
using Microsoft.Xna.Framework; namespace Barotrauma.Abilities
using System.Xml.Linq;
namespace Barotrauma.Abilities
{ {
class CharacterAbilityRevive : CharacterAbility class CharacterAbilityRevive : CharacterAbility
{ {
@@ -11,6 +11,10 @@ namespace Barotrauma.Abilities
{ {
identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty); identifier = abilityElement.GetAttributeIdentifier("identifier", Identifier.Empty);
value = abilityElement.GetAttributeInt("value", 0); value = abilityElement.GetAttributeInt("value", 0);
if (identifier.IsEmpty)
{
DebugConsole.ThrowError($"Error in talent {CharacterTalent.DebugIdentifier}, {nameof(CharacterAbilitySetMetadataInt)} - identifier is empty.");
}
} }
public override void InitializeAbility(bool addingFirstTime) public override void InitializeAbility(bool addingFirstTime)
@@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -1,7 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -1,6 +1,4 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityApprenticeship : CharacterAbility class CharacterAbilityApprenticeship : CharacterAbility
{ {
@@ -1,20 +1,17 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
class CharacterAbilityAtmosMachine : CharacterAbility class CharacterAbilityAtmosMachine : CharacterAbility
{ {
private readonly float addedValue; private readonly float addedValue;
private readonly float multiplyValue;
private readonly Identifier[] tags; private readonly Identifier[] tags;
private readonly int maxMultiplyCount; private readonly int maxMultiplyCount;
public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityAtmosMachine(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f); addedValue = abilityElement.GetAttributeFloat("addedvalue", 0f);
multiplyValue = abilityElement.GetAttributeFloat("multiplyvalue", 1f);
tags = abilityElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()); tags = abilityElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>());
maxMultiplyCount = abilityElement.GetAttributeInt("maxmultiplycount", int.MaxValue); maxMultiplyCount = abilityElement.GetAttributeInt("maxmultiplycount", int.MaxValue);
} }
@@ -1,11 +1,8 @@
using System.Collections.Generic; namespace Barotrauma.Abilities
using System.Xml.Linq;
namespace Barotrauma.Abilities
{ {
class CharacterAbilityBountyHunter : CharacterAbility class CharacterAbilityBountyHunter : CharacterAbility
{ {
private float vitalityPercentage; private readonly float vitalityPercentage;
public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement) public CharacterAbilityBountyHunter(CharacterAbilityGroup characterAbilityGroup, ContentXElement abilityElement) : base(characterAbilityGroup, abilityElement)
{ {
@@ -1,6 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -1,7 +1,4 @@
using Microsoft.Xna.Framework; namespace Barotrauma.Abilities
using System.Xml.Linq;
namespace Barotrauma.Abilities
{ {
class CharacterAbilityMultitasker : CharacterAbility class CharacterAbilityMultitasker : CharacterAbility
{ {
@@ -1,12 +1,10 @@
using System.Xml.Linq; namespace Barotrauma.Abilities
namespace Barotrauma.Abilities
{ {
class CharacterAbilityPsychoClown : CharacterAbility class CharacterAbilityPsychoClown : CharacterAbility
{ {
private StatTypes statType; private readonly StatTypes statType;
private float maxValue; private readonly float maxValue;
private string afflictionIdentifier; private readonly string afflictionIdentifier;
private float lastValue = 0f; private float lastValue = 0f;
public override bool AllowClientSimulation => true; public override bool AllowClientSimulation => true;
@@ -1,8 +1,5 @@
using Barotrauma.Items.Components; using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Xml.Linq;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -1,8 +1,8 @@
#nullable enable #nullable enable
using Barotrauma.Extensions;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Immutable; using System.Collections.Immutable;
using Barotrauma.Extensions;
namespace Barotrauma.Abilities namespace Barotrauma.Abilities
{ {
@@ -66,8 +66,8 @@ namespace Barotrauma
IEnumerable<TalentSubTree> blockingSubTrees = tree.TalentSubTrees.Where(tst => tst.BlockedTrees.Contains(targetTree.Identifier)), IEnumerable<TalentSubTree> blockingSubTrees = tree.TalentSubTrees.Where(tst => tst.BlockedTrees.Contains(targetTree.Identifier)),
requiredSubTrees = tree.TalentSubTrees.Where(tst => targetTree.RequiredTrees.Contains(tst.Identifier)); requiredSubTrees = tree.TalentSubTrees.Where(tst => targetTree.RequiredTrees.Contains(tst.Identifier));
return requiredSubTrees.All(tst => tst.IsCompleted(selectedTalents)) && // check if we meet requirements return requiredSubTrees.All(tst => tst.HasEnoughTalents(selectedTalents)) && // check if we meet requirements
!blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents)); // check if any other talent trees are blocking this one !blockingSubTrees.Any(tst => tst.HasAnyTalent(selectedTalents) && !tst.HasMaxTalents(selectedTalents)); // check if any other talent trees are blocking this one
} }
// i hate this function - markus // i hate this function - markus
@@ -128,16 +128,26 @@ namespace Barotrauma
public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents) public static bool IsViableTalentForCharacter(Character character, Identifier talentIdentifier, IReadOnlyCollection<Identifier> selectedTalents)
{ {
if (character?.Info?.Job.Prefab == null) { return false; } if (character?.Info?.Job.Prefab == null) { return false; }
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; } if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; } if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
foreach (var subTree in talentTree!.TalentSubTrees) foreach (var subTree in talentTree!.TalentSubTrees)
{ {
if (subTree.AllTalentIdentifiers.Contains(talentIdentifier) && subTree.HasMaxTalents(selectedTalents)) { return false; }
foreach (var talentOptionStage in subTree.TalentOptionStages) foreach (var talentOptionStage in subTree.TalentOptionStages)
{ {
bool hasTalentInThisTier = talentOptionStage.HasEnoughTalents(selectedTalents); if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
{
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
}
bool optionStageCompleted = talentOptionStage.HasEnoughTalents(selectedTalents);
if (!optionStageCompleted)
{
break;
}
/*bool hasTalentInThisTier = talentOptionStage.HasMaxTalents(selectedTalents);
if (!hasTalentInThisTier) if (!hasTalentInThisTier)
{ {
if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier)) if (talentOptionStage.TalentIdentifiers.Contains(talentIdentifier))
@@ -145,7 +155,7 @@ namespace Barotrauma
return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents); return TalentTreeMeetsRequirements(talentTree, subTree, selectedTalents);
} }
break; break;
} }*/
} }
} }
@@ -196,7 +206,8 @@ namespace Barotrauma
public readonly ImmutableHashSet<Identifier> RequiredTrees; public readonly ImmutableHashSet<Identifier> RequiredTrees;
public readonly ImmutableHashSet<Identifier> BlockedTrees; public readonly ImmutableHashSet<Identifier> BlockedTrees;
public bool IsCompleted(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents)); public bool HasEnoughTalents(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasEnoughTalents(talents));
public bool HasMaxTalents(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.All(option => option.HasMaxTalents(talents));
public bool HasAnyTalent(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.Any(option => option.HasSelectedTalent(talents)); public bool HasAnyTalent(IReadOnlyCollection<Identifier> talents) => TalentOptionStages.Any(option => option.HasSelectedTalent(talents));
public TalentSubTree(ContentXElement subTreeElement) public TalentSubTree(ContentXElement subTreeElement)
@@ -228,6 +239,13 @@ namespace Barotrauma
public IEnumerable<Identifier> TalentIdentifiers => talentIdentifiers; public IEnumerable<Identifier> TalentIdentifiers => talentIdentifiers;
/// <summary>
/// How many talents need to be unlocked to consider this tree completed
/// </summary>
public readonly int RequiredTalents;
/// <summary>
/// How many talents can be unlocked in total
/// </summary>
public readonly int MaxChosenTalents; public readonly int MaxChosenTalents;
/// <summary> /// <summary>
@@ -236,8 +254,9 @@ namespace Barotrauma
/// </summary> /// </summary>
public readonly Dictionary<Identifier, ImmutableHashSet<Identifier>> ShowCaseTalents = new Dictionary<Identifier, ImmutableHashSet<Identifier>>(); public readonly Dictionary<Identifier, ImmutableHashSet<Identifier>> ShowCaseTalents = new Dictionary<Identifier, ImmutableHashSet<Identifier>>();
public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= MaxChosenTalents; public bool HasEnoughTalents(CharacterInfo character) => CountMatchingTalents(character.UnlockedTalents) >= RequiredTalents;
public bool HasEnoughTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents; public bool HasEnoughTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= RequiredTalents;
public bool HasMaxTalents(IReadOnlyCollection<Identifier> selectedTalents) => CountMatchingTalents(selectedTalents) >= MaxChosenTalents;
// No LINQ // No LINQ
public bool HasSelectedTalent(IReadOnlyCollection<Identifier> selectedTalents) public bool HasSelectedTalent(IReadOnlyCollection<Identifier> selectedTalents)
@@ -267,10 +286,15 @@ namespace Barotrauma
public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier) public TalentOption(ContentXElement talentOptionsElement, Identifier debugIdentifier)
{ {
MaxChosenTalents = talentOptionsElement.GetAttributeInt("maxchosentalents", 1); MaxChosenTalents = talentOptionsElement.GetAttributeInt(nameof(MaxChosenTalents), 1);
RequiredTalents = talentOptionsElement.GetAttributeInt(nameof(RequiredTalents), MaxChosenTalents);
if (RequiredTalents > MaxChosenTalents)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - MaxChosenTalents is larger than RequiredTalents.");
}
HashSet<Identifier> identifiers = new HashSet<Identifier>(); HashSet<Identifier> identifiers = new HashSet<Identifier>();
foreach (ContentXElement talentOptionElement in talentOptionsElement.Elements()) foreach (ContentXElement talentOptionElement in talentOptionsElement.Elements())
{ {
Identifier elementName = talentOptionElement.Name.ToIdentifier(); Identifier elementName = talentOptionElement.Name.ToIdentifier();
@@ -293,6 +317,15 @@ namespace Barotrauma
} }
talentIdentifiers = identifiers.ToImmutableHashSet(); talentIdentifiers = identifiers.ToImmutableHashSet();
if (RequiredTalents > talentIdentifiers.Count)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - completing a stage of the tree requires more talents than there are in the stage.");
}
if (MaxChosenTalents > talentIdentifiers.Count)
{
DebugConsole.ThrowError($"Error in talent tree {debugIdentifier} - maximum number of talents to choose is larger than the number of talents.");
}
} }
} }
} }
@@ -22,7 +22,7 @@ namespace Barotrauma
[Serialize("", IsPropertySaveable.Yes)] [Serialize("", IsPropertySaveable.Yes)]
public Identifier OrderTargetTag { get; set; } public Identifier OrderTargetTag { get; set; }
[Serialize(OrderPriority.Top, IsPropertySaveable.Yes)] [Serialize(OrderPriority.Any, IsPropertySaveable.Yes)]
public OrderPriority Priority { get; set; } public OrderPriority Priority { get; set; }
public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public CheckOrderAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -43,6 +43,9 @@ partial class UIHighlightAction : EventAction
[Serialize(true, IsPropertySaveable.Yes)] [Serialize(true, IsPropertySaveable.Yes)]
public bool Bounce { get; set; } public bool Bounce { get; set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool HighlightMultiple { get; set; }
private bool isFinished; private bool isFinished;
public UIHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { } public UIHighlightAction(ScriptedEvent parentEvent, ContentXElement element) : base(parentEvent, element) { }
@@ -207,25 +207,23 @@ namespace Barotrauma
level.StartLocation.Connections.ForEach(c => c.Locked = false); level.StartLocation.Connections.ForEach(c => c.Locked = false);
} }
} }
}
AddChildEvents(initialEventSet); RegisterNonRepeatableChildEvents(initialEventSet);
void RegisterNonRepeatableChildEvents(EventSet eventSet)
void AddChildEvents(EventSet eventSet) {
if (eventSet == null) { return; }
if (eventSet.OncePerLevel)
{ {
if (eventSet == null) { return; } foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs))
if (eventSet.OncePerOutpost)
{ {
foreach (EventPrefab ep in eventSet.EventPrefabs.SelectMany(e => e.EventPrefabs)) nonRepeatableEvents.Add(ep);
{
nonRepeatableEvents.Add(ep);
}
}
foreach (EventSet childSet in eventSet.ChildSets)
{
AddChildEvents(childSet);
} }
} }
} foreach (EventSet childSet in eventSet.ChildSets)
{
RegisterNonRepeatableChildEvents(childSet);
}
}
} }
PreloadContent(GetFilesToPreload()); PreloadContent(GetFilesToPreload());
@@ -374,26 +372,18 @@ namespace Barotrauma
/// </summary> /// </summary>
public void RegisterEventHistory() public void RegisterEventHistory()
{ {
if (level?.LevelData != null) if (level?.LevelData == null) { return; }
level.LevelData.EventsExhausted = true;
if (level.LevelData.Type == LevelData.LevelType.Outpost)
{ {
level.LevelData.EventsExhausted = true; level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e)));
if (level.LevelData.Type == LevelData.LevelType.Outpost) if (level.LevelData.EventHistory.Count > MaxEventHistory)
{ {
level.LevelData.EventHistory.AddRange(selectedEvents.Values.SelectMany(v => v).Select(e => e.Prefab).Where(e => !level.LevelData.EventHistory.Contains(e))); level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
if (level.LevelData.EventHistory.Count > MaxEventHistory)
{
level.LevelData.EventHistory.RemoveRange(0, level.LevelData.EventHistory.Count - MaxEventHistory);
}
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e)));
}
foreach (var usedUniqueSet in usedUniqueSets)
{
if (!level.LevelData.UsedUniqueSets.Contains(usedUniqueSet.Identifier))
{
level.LevelData.UsedUniqueSets.Add(usedUniqueSet.Identifier);
}
} }
} }
level.LevelData.NonRepeatableEvents.AddRange(nonRepeatableEvents.Where(e => !level.LevelData.NonRepeatableEvents.Contains(e)));
} }
public void SkipEventCooldown() public void SkipEventCooldown()
@@ -418,16 +408,11 @@ namespace Barotrauma
DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true); DebugConsole.NewMessage($"Loading event set {eventSet.Identifier}", Color.LightBlue, debugOnly: true);
if (eventSet.Unique && !usedUniqueSets.Contains(eventSet))
{
usedUniqueSets.Add(eventSet);
}
int applyCount = 1; int applyCount = 1;
List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>(); List<Func<Level.InterestingPosition, bool>> spawnPosFilter = new List<Func<Level.InterestingPosition, bool>>();
if (eventSet.PerRuin) if (eventSet.PerRuin)
{ {
applyCount = level.Ruins.Count(); applyCount = level.Ruins.Count;
foreach (var ruin in level.Ruins) foreach (var ruin in level.Ruins)
{ {
spawnPosFilter.Add(pos => pos.Ruin == ruin); spawnPosFilter.Add(pos => pos.Ruin == ruin);
@@ -435,7 +420,7 @@ namespace Barotrauma
} }
else if (eventSet.PerCave) else if (eventSet.PerCave)
{ {
applyCount = level.Caves.Count(); applyCount = level.Caves.Count;
foreach (var cave in level.Caves) foreach (var cave in level.Caves)
{ {
spawnPosFilter.Add(pos => pos.Cave == cave); spawnPosFilter.Add(pos => pos.Cave == cave);
@@ -452,8 +437,8 @@ namespace Barotrauma
} }
bool isPrefabSuitable(EventPrefab e) bool isPrefabSuitable(EventPrefab e)
=> e.BiomeIdentifier.IsEmpty || => (e.BiomeIdentifier.IsEmpty || e.BiomeIdentifier == level.LevelData?.Biome?.Identifier) &&
e.BiomeIdentifier == level.LevelData?.Biome?.Identifier; !level.LevelData.NonRepeatableEvents.Contains(e);
foreach (var subEventPrefab in eventSet.EventPrefabs) foreach (var subEventPrefab in eventSet.EventPrefabs)
{ {
@@ -610,8 +595,7 @@ namespace Barotrauma
return return
level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty && level.Difficulty >= eventSet.MinLevelDifficulty && level.Difficulty <= eventSet.MaxLevelDifficulty &&
level.LevelData.Type == eventSet.LevelType && level.LevelData.Type == eventSet.LevelType &&
(eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier) && (eventSet.BiomeIdentifier.IsEmpty || eventSet.BiomeIdentifier == level.LevelData.Biome.Identifier);
(!eventSet.Unique || !level.LevelData.UsedUniqueSets.Contains(eventSet.Identifier));
} }
private bool IsValidForLocation(EventSet eventSet, Location location) private bool IsValidForLocation(EventSet eventSet, Location location)
@@ -114,15 +114,9 @@ namespace Barotrauma
public readonly bool DisableInHuntingGrounds; public readonly bool DisableInHuntingGrounds;
/// <summary> /// <summary>
/// If true, events from this set shouldn't be selected again as long as they remain in <see cref="LevelData.NonRepeatableEvents"/> which has a limited size. /// If true, events from this set can only occur once in the level.
/// Use <see cref="Unique"/> to prevent selecting the whole set again altogether.
/// </summary> /// </summary>
public readonly bool OncePerOutpost; public readonly bool OncePerLevel;
/// <summary>
/// If true, the whole set can only be selected once for a level.
/// </summary>
public readonly bool Unique;
public readonly bool DelayWhenCrewAway; public readonly bool DelayWhenCrewAway;
@@ -289,8 +283,7 @@ namespace Barotrauma
DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false); DisableInHuntingGrounds = element.GetAttributeBool("disableinhuntinggrounds", false);
IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck)); IgnoreCoolDown = element.GetAttributeBool("ignorecooldown", parentSet?.IgnoreCoolDown ?? (PerRuin || PerCave || PerWreck));
DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck); DelayWhenCrewAway = element.GetAttributeBool("delaywhencrewaway", !PerRuin && !PerCave && !PerWreck);
OncePerOutpost = element.GetAttributeBool("onceperoutpost", false); OncePerLevel = element.GetAttributeBool("onceperlevel", element.GetAttributeBool("onceperoutpost", false));
Unique = element.GetAttributeBool("unique", false);
TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true); TriggerEventCooldown = element.GetAttributeBool("triggereventcooldown", true);
IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false)); IsCampaignSet = element.GetAttributeBool("campaign", LevelType == LevelData.LevelType.Outpost || (parentSet?.IsCampaignSet ?? false));
ResetTime = element.GetAttributeFloat("resettime", 0); ResetTime = element.GetAttributeFloat("resettime", 0);
@@ -386,23 +386,27 @@ namespace Barotrauma
#if CLIENT #if CLIENT
foreach (Character character in crewCharacters) foreach (Character character in crewCharacters)
{ {
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); GiveMissionExperience(character.Info);
character.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
character.Info?.GiveExperience(experienceGain, isMissionExperience: true);
} }
#else #else
foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients) foreach (Barotrauma.Networking.Client c in GameMain.Server.ConnectedClients)
{ {
//give the experience to the stored characterinfo if the client isn't currently controlling a character //give the experience to the stored characterinfo if the client isn't currently controlling a character
CharacterInfo info = c.Character?.Info ?? c.CharacterInfo; GiveMissionExperience(c.Character?.Info ?? c.CharacterInfo);
}
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f); foreach (Character bot in GameSession.GetSessionCrewCharacters(CharacterType.Bot))
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual); {
GiveMissionExperience(bot.Info);
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true);
} }
#endif #endif
void GiveMissionExperience(CharacterInfo info)
{
var experienceGainMultiplierIndividual = new AbilityMissionExperienceGainMultiplier(this, 1f);
info?.Character?.CheckTalents(AbilityEffectType.OnGainMissionExperience, experienceGainMultiplierIndividual);
info?.GiveExperience((int)(experienceGain * experienceGainMultiplier.Value), isMissionExperience: true);
}
// apply money gains afterwards to prevent them from affecting XP gains // apply money gains afterwards to prevent them from affecting XP gains
var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f); var missionMoneyGainMultiplier = new AbilityMissionMoneyGainMultiplier(this, 1f);
crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier)); crewCharacters.ForEach(c => c.CheckTalents(AbilityEffectType.OnGainMissionMoney, missionMoneyGainMultiplier));
@@ -306,14 +306,6 @@ namespace Barotrauma
return validContainers; return validContainers;
} }
private static readonly (int quality, float commonness)[] qualityCommonnesses = new (int quality, float commonness)[Quality.MaxQuality + 1]
{
(0, 1.0f),
(1, 0.0f),
(2, 0.0f),
(3, 0.0f),
};
private static List<Item> CreateItems(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer) private static List<Item> CreateItems(ItemPrefab itemPrefab, List<ItemContainer> containers, KeyValuePair<ItemContainer, PreferredContainer> validContainer)
{ {
List<Item> newItems = new List<Item>(); List<Item> newItems = new List<Item>();
@@ -336,11 +328,7 @@ namespace Barotrauma
break; break;
} }
var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.Prefab == itemPrefab); var existingItem = validContainer.Key.Inventory.AllItems.FirstOrDefault(it => it.Prefab == itemPrefab);
int quality = int quality = existingItem?.Quality ?? Quality.GetSpawnedItemQuality(validContainer.Key.Item.Submarine, Level.Loaded, Rand.RandSync.ServerAndClient);
existingItem?.Quality ??
ToolBox.SelectWeightedRandom(
qualityCommonnesses.Select(q => q.quality).ToList(),
qualityCommonnesses.Select(q => q.commonness).ToList(), Rand.RandSync.ServerAndClient);
if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; } if (!validContainer.Key.Inventory.CanBePut(itemPrefab, quality: quality)) { break; }
var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false) var item = new Item(itemPrefab, validContainer.Key.Item.Position, validContainer.Key.Item.Submarine, callOnItemLoaded: false)
{ {
@@ -276,7 +276,7 @@ namespace Barotrauma.Items.Components
} }
} }
//get all walls within range //get all walls within range the arc could potentially hit
List<Entity> entitiesInRange = new List<Entity>(100); List<Entity> entitiesInRange = new List<Entity>(100);
foreach (Structure structure in Structure.WallList) foreach (Structure structure in Structure.WallList)
{ {
@@ -284,10 +284,10 @@ namespace Barotrauma.Items.Components
if (structure.Submarine != null&& !submarinesInRange.Contains(structure.Submarine)) { continue; } if (structure.Submarine != null&& !submarinesInRange.Contains(structure.Submarine)) { continue; }
var structureWorldRect = structure.WorldRect; var structureWorldRect = structure.WorldRect;
if (worldPosition.X < structureWorldRect.X - range) continue; if (worldPosition.X < structureWorldRect.X - range) { continue; }
if (worldPosition.X > structureWorldRect.Right + range) continue; if (worldPosition.X > structureWorldRect.Right + range) { continue; }
if (worldPosition.Y > structureWorldRect.Y + range) continue; if (worldPosition.Y > structureWorldRect.Y + range) { continue; }
if (worldPosition.Y < structureWorldRect.Y -structureWorldRect.Height - range) continue; if (worldPosition.Y < structureWorldRect.Y - structureWorldRect.Height - range) { continue; }
if (structure.Submarine != null) if (structure.Submarine != null)
{ {
@@ -317,6 +317,7 @@ namespace Barotrauma.Items.Components
nodes.Add(new Node(worldPosition, -1)); nodes.Add(new Node(worldPosition, -1));
} }
//get all characters within range the arc could potentially hit
float totalRange = RaycastRange + range; float totalRange = RaycastRange + range;
foreach (Character character in Character.CharacterList) foreach (Character character in Character.CharacterList)
{ {
@@ -325,11 +326,20 @@ namespace Barotrauma.Items.Components
if (OutdoorsOnly && character.Submarine != null) { continue; } if (OutdoorsOnly && character.Submarine != null) { continue; }
if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) { continue; } if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) { continue; }
if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < totalRange * totalRange * RangeMultiplierInWalls || if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < totalRange * totalRange * RangeMultiplierInWalls)
(RaycastRange > 0.0f && MathUtils.LineToPointDistanceSquared(worldPosition, item.WorldPosition, character.WorldPosition) < range * range * RangeMultiplierInWalls))
{ {
entitiesInRange.Add(character); entitiesInRange.Add(character);
charactersInRange.Add((character, nodes[0])); }
//if the weapon does a raycast, check distance to the ray too (not just the end of the ray)
if (RaycastRange > 0)
{
float distSqr = MathUtils.LineSegmentToPointDistanceSquared(worldPosition, item.WorldPosition, character.WorldPosition);
//if the distance from the initial raycast to the character is small (e.g. goes through the character), we know it must hit
if (distSqr < range * range * RangeMultiplierInWalls)
{
if (!entitiesInRange.Contains(character)) { entitiesInRange.Add(character); }
charactersInRange.Add((character, nodes.First()));
}
} }
} }
@@ -378,7 +388,7 @@ namespace Barotrauma.Items.Components
} }
else if (entitiesInRange[i] is Character character) else if (entitiesInRange[i] is Character character)
{ {
dist = Vector2.Distance(character.WorldPosition, currPos); dist = MathUtils.LineSegmentToPointDistanceSquared(currPos, nodes[parentNodeIndex].WorldPosition, character.WorldPosition);
} }
if (dist < closestDist) if (dist < closestDist)
@@ -494,17 +504,31 @@ namespace Barotrauma.Items.Components
if (IgnoreUser && character == user) { continue; } if (IgnoreUser && character == user) { continue; }
if (OutdoorsOnly && character.Submarine != null) { continue; } if (OutdoorsOnly && character.Submarine != null) { continue; }
Vector2 characterMin = new Vector2(character.AnimController.Limbs.Min(l => l.WorldPosition.X), character.AnimController.Limbs.Min(l => l.WorldPosition.Y));
Vector2 characterMax = new Vector2(character.AnimController.Limbs.Max(l => l.WorldPosition.X), character.AnimController.Limbs.Max(l => l.WorldPosition.Y));
if (targetStructure.IsHorizontal) if (targetStructure.IsHorizontal)
{ {
if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) { continue; } if (characterMax.X < targetStructure.WorldRect.X) { continue; }
if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) { continue; } if (characterMin.X > targetStructure.WorldRect.Right) { continue; }
if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) { continue; } if (Math.Abs(characterMin.Y - targetStructure.WorldPosition.Y) > currentRange &&
Math.Abs(characterMax.Y - targetStructure.WorldPosition.Y) > currentRange)
{
continue;
}
} }
else else
{ {
if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) { continue; } if (characterMax.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) { continue; }
if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) { continue; } if (characterMin.Y > targetStructure.WorldRect.Y) { continue; }
if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) { continue; } if (Math.Abs(characterMin.X - targetStructure.WorldPosition.X) > currentRange &&
Math.Abs(characterMax.X - targetStructure.WorldPosition.X) > currentRange)
{
continue;
}
}
if (!charactersInRange.Any(c => c.character == character))
{
charactersInRange.Add((character, nodes[parentNodeIndex]));
} }
float closestNodeDistSqr = float.MaxValue; float closestNodeDistSqr = float.MaxValue;
int closestNodeIndex = -1; int closestNodeIndex = -1;
@@ -12,7 +12,7 @@ namespace Barotrauma.Items.Components
{ {
public enum UseEnvironment public enum UseEnvironment
{ {
Air, Water, Both Air, Water, Both, None
}; };
private float useState; private float useState;
@@ -44,6 +44,7 @@ namespace Barotrauma.Items.Components
{ {
if (character == null || character.Removed) { return false; } if (character == null || character.Removed) { return false; }
if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) { return false; } if (!character.IsKeyDown(InputType.Aim) || character.Stun > 0.0f) { return false; }
if (UsableIn == UseEnvironment.None) { return false; }
IsActive = true; IsActive = true;
useState = 0.1f; useState = 0.1f;
@@ -476,6 +476,7 @@ namespace Barotrauma.Items.Components
{ {
foreach (Item item in Inventory.AllItemsMod) foreach (Item item in Inventory.AllItemsMod)
{ {
item.ApplyStatusEffects(ActionType.OnSuccess, 1.0f, ownerCharacter);
item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter); item.ApplyStatusEffects(ActionType.OnUse, 1.0f, ownerCharacter);
item.GetComponent<GeneticMaterial>()?.Equip(ownerCharacter); item.GetComponent<GeneticMaterial>()?.Equip(ownerCharacter);
autoInjectCooldown = AutoInjectInterval; autoInjectCooldown = AutoInjectInterval;
@@ -450,6 +450,7 @@ namespace Barotrauma.Items.Components
public override bool Select(Character activator) public override bool Select(Character activator)
{ {
if (activator == null || activator.Removed) { return false; } if (activator == null || activator.Removed) { return false; }
if (Item.Condition <= 0.0f && !UpdateWhenInactive) { return false; }
if (UsableIn == UseEnvironment.Water && !activator.AnimController.InWater || if (UsableIn == UseEnvironment.Water && !activator.AnimController.InWater ||
UsableIn == UseEnvironment.Air && activator.AnimController.InWater) UsableIn == UseEnvironment.Air && activator.AnimController.InWater)
@@ -15,6 +15,8 @@ namespace Barotrauma.Items.Components
{ {
private ImmutableDictionary<uint, FabricationRecipe> fabricationRecipes; //this is not readonly because tutorials fuck this up!!!! private ImmutableDictionary<uint, FabricationRecipe> fabricationRecipes; //this is not readonly because tutorials fuck this up!!!!
private const int MaxAmountToFabricate = 99;
private FabricationRecipe fabricatedItem; private FabricationRecipe fabricatedItem;
private float timeUntilReady; private float timeUntilReady;
private float requiredTime; private float requiredTime;
@@ -39,6 +41,16 @@ namespace Barotrauma.Items.Components
[Serialize(1.0f, IsPropertySaveable.Yes)] [Serialize(1.0f, IsPropertySaveable.Yes)]
public float SkillRequirementMultiplier { get; set; } public float SkillRequirementMultiplier { get; set; }
private int amountToFabricate;
[Serialize(1, IsPropertySaveable.Yes)]
public int AmountToFabricate
{
get { return amountToFabricate; }
set { amountToFabricate = MathHelper.Clamp(value, 1, MaxAmountToFabricate); }
}
private int amountRemaining;
private const float TinkeringSpeedIncrease = 2.5f; private const float TinkeringSpeedIncrease = 2.5f;
private enum FabricatorState private enum FabricatorState
@@ -183,16 +195,20 @@ namespace Barotrauma.Items.Components
if (selectedItem == null) { return; } if (selectedItem == null) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; } if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
#if CLIENT
itemList.Enabled = false;
activateButton.Text = TextManager.Get("FabricatorCancel");
#endif
IsActive = true; IsActive = true;
this.user = user; this.user = user;
fabricatedItem = selectedItem; fabricatedItem = selectedItem;
RefreshAvailableIngredients(); RefreshAvailableIngredients();
#if CLIENT
itemList.Enabled = false;
if (amountInput != null)
{
amountInput.Enabled = false;
}
RefreshActivateButtonText();
#endif
bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient; bool isClient = GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient;
if (!isClient) if (!isClient)
{ {
@@ -249,10 +265,11 @@ namespace Barotrauma.Items.Components
} }
#elif CLIENT #elif CLIENT
itemList.Enabled = true; itemList.Enabled = true;
if (activateButton != null) if (amountInput != null)
{ {
activateButton.Text = TextManager.Get(CreateButtonText); amountInput.Enabled = true;
} }
RefreshActivateButtonText();
#endif #endif
fabricatedItem = null; fabricatedItem = null;
} }
@@ -518,20 +535,16 @@ namespace Barotrauma.Items.Components
} }
} }
//disabled "continuous fabrication" for now var prevFabricatedItem = fabricatedItem;
//before we enable it, there should be some UI controls for fabricating a specific number of items
/*var prevFabricatedItem = fabricatedItem;
var prevUser = user; var prevUser = user;
CancelFabricating(); CancelFabricating();
if (CanBeFabricated(prevFabricatedItem))
amountRemaining--;
if (amountRemaining > 0 && CanBeFabricated(prevFabricatedItem, availableIngredients, prevUser))
{ {
//keep fabricating if we can fabricate more //keep fabricating if we can fabricate more
StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false); StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false);
}*/ }
CancelFabricating();
} }
} }
@@ -594,7 +607,15 @@ namespace Barotrauma.Items.Components
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character) private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
{ {
if (fabricableItem == null) { return false; } if (fabricableItem == null) { return false; }
if (fabricableItem.RequiresRecipe && (character == null || !character.HasRecipeForItem(fabricableItem.TargetItem.Identifier))) { return false; } if (fabricableItem.RequiresRecipe)
{
if (character == null) { return false; }
if (!character.HasRecipeForItem(fabricableItem.TargetItem.Identifier) &&
GameSession.GetSessionCrewCharacters(CharacterType.Bot).None(c => c.HasRecipeForItem(fabricableItem.TargetItem.Identifier)))
{
return false;
}
}
if (fabricableItem.RequiredMoney > 0) if (fabricableItem.RequiredMoney > 0)
{ {
@@ -151,7 +151,6 @@ namespace Barotrauma.Items.Components
set set
{ {
bool changed = currentMode != value; bool changed = currentMode != value;
currentMode = value; currentMode = value;
#if CLIENT #if CLIENT
if (changed) { prevPassivePingRadius = float.MaxValue; } if (changed) { prevPassivePingRadius = float.MaxValue; }
@@ -1,6 +1,7 @@
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
namespace Barotrauma.Items.Components namespace Barotrauma.Items.Components
@@ -9,14 +10,6 @@ namespace Barotrauma.Items.Components
{ {
public const int MaxQuality = 3; public const int MaxQuality = 3;
public static readonly float[] QualityCommonnesses = new float[]
{
0.8f,
0.15f,
0.045f,
0.005f,
};
public enum StatType public enum StatType
{ {
Condition, Condition,
@@ -81,5 +74,29 @@ namespace Barotrauma.Items.Components
if (!statValues.ContainsKey(statType)) { return 0.0f; } if (!statValues.ContainsKey(statType)) { return 0.0f; }
return statValues[statType] * qualityLevel; return statValues[statType] * qualityLevel;
} }
/// <summary>
/// Get a random quality for an item spawning in some sub, taking into account the type of the submarine and the difficulty of the current level
/// (high-quality items become more common as difficulty increases)
/// </summary>
public static int GetSpawnedItemQuality(Submarine submarine, Level level, Rand.RandSync randSync = Rand.RandSync.ServerAndClient)
{
if (submarine?.Info == null || level == null || submarine.Info.Type == SubmarineType.Player) { return 0; }
float difficultyFactor = MathHelper.Clamp(level.Difficulty, 0.0f, 1.0f);
return ToolBox.SelectWeightedRandom(Enumerable.Range(0, MaxQuality + 1), q => GetCommonness(q, difficultyFactor), randSync);
static float GetCommonness(int quality, float difficultyFactor)
{
return quality switch
{
0 => 1,
1 => MathHelper.Lerp(0.0f, 1f, difficultyFactor),
2 => MathHelper.Lerp(0.0f, 1f, Math.Max(difficultyFactor-0.15f, 0f)), //15 difficulty transition to next biome - unlock Excellent loot
3 => MathHelper.Lerp(0.0f, 1f, Math.Max(difficultyFactor-0.35f, 0f)), //35 difficulty transition to next biome - unlock Masterwork loot
_ => 0.0f,
};
}
}
} }
} }
@@ -1617,11 +1617,7 @@ namespace Barotrauma
public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, bool checkCondition = true, Vector2? worldPosition = null) public void ApplyStatusEffect(StatusEffect effect, ActionType type, float deltaTime, Character character = null, Limb limb = null, Entity useTarget = null, bool isNetworkEvent = false, bool checkCondition = true, Vector2? worldPosition = null)
{ {
if (effect.intervalTimer > 0.0f) if (effect.ShouldWaitForInterval(this, deltaTime)) { return; }
{
effect.intervalTimer -= deltaTime;
return;
}
if (!isNetworkEvent && checkCondition) if (!isNetworkEvent && checkCondition)
{ {
if (condition == 0.0f && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { return; } if (condition == 0.0f && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { return; }
@@ -2758,7 +2754,6 @@ namespace Barotrauma
return; return;
} }
#endif #endif
bool remove = false; bool remove = false;
foreach (ItemComponent ic in components) foreach (ItemComponent ic in components)
{ {
@@ -2788,7 +2783,6 @@ namespace Barotrauma
{ {
var abilityItem = new AbilityApplyTreatment(user, character, this); var abilityItem = new AbilityApplyTreatment(user, character, this);
user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem); user.CheckTalents(AbilityEffectType.OnApplyTreatment, abilityItem);
} }
if (remove) { Spawner?.AddItemToRemoveQueue(this); } if (remove) { Spawner?.AddItemToRemoveQueue(this); }
@@ -15,10 +15,7 @@ namespace Barotrauma
} }
[NetworkSerialize] [NetworkSerialize]
public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct public readonly record struct TalentStatIdentifier(ItemTalentStats Stat, Identifier TalentIdentifier, UInt32 CharacterID) : INetSerializableStruct;
{
public override int GetHashCode() => HashCode.Combine(TalentIdentifier, CharacterID, Stat);
}
private readonly Dictionary<TalentStatIdentifier, float> talentStats = new(); private readonly Dictionary<TalentStatIdentifier, float> talentStats = new();
@@ -3943,31 +3943,11 @@ namespace Barotrauma
Submarine outpost = null; Submarine outpost = null;
if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null) if (i == 0 && preSelectedStartOutpost == null || i == 1 && preSelectedEndOutpost == null)
{ {
if (OutpostGenerationParams.OutpostParams.Any() || LevelData.ForceOutpostGenerationParams != null) if (LevelData.OutpostGenerationParamsExist)
{ {
Location location = i == 0 ? StartLocation : EndLocation; Location location = i == 0 ? StartLocation : EndLocation;
OutpostGenerationParams outpostGenerationParams = LevelData.ForceOutpostGenerationParams ??
OutpostGenerationParams outpostGenerationParams = null; LevelData.GetSuitableOutpostGenerationParams(location).GetRandom(Rand.RandSync.ServerAndClient);
if (LevelData.ForceOutpostGenerationParams != null)
{
outpostGenerationParams = LevelData.ForceOutpostGenerationParams;
}
else
{
var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
if (!suitableParams.Any())
{
suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any());
if (!suitableParams.Any())
{
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
suitableParams = OutpostGenerationParams.OutpostParams;
}
}
outpostGenerationParams = suitableParams.GetRandom(Rand.RandSync.ServerAndClient);
}
LocationType locationType = location?.Type; LocationType locationType = location?.Type;
if (locationType == null) if (locationType == null)
{ {
@@ -57,10 +57,19 @@ namespace Barotrauma
/// </summary> /// </summary>
public int? MinMainPathWidth; public int? MinMainPathWidth;
/// <summary>
/// Events that have previously triggered in this level. Used for making events the player hasn't seen yet more likely to trigger when re-entering the level. Has a maximum size of <see cref="EventManager.MaxEventHistory"/>.
/// </summary>
public readonly List<EventPrefab> EventHistory = new List<EventPrefab>(); public readonly List<EventPrefab> EventHistory = new List<EventPrefab>();
public readonly List<EventPrefab> NonRepeatableEvents = new List<EventPrefab>();
public readonly HashSet<Identifier> UsedUniqueSets = new HashSet<Identifier>();
/// <summary>
/// Events that have already triggered in this level and can never trigger again. <see cref="EventSet.OncePerLevel"/>.
/// </summary>
public readonly List<EventPrefab> NonRepeatableEvents = new List<EventPrefab>();
/// <summary>
/// 'Exhaustible' sets won't appear in the same level until after one world step (~10 min, see Map.ProgressWorld) has passed. <see cref="EventSet.Exhaustible"/>.
/// </summary>
public bool EventsExhausted { get; set; } public bool EventsExhausted { get; set; }
/// <summary> /// <summary>
@@ -144,8 +153,6 @@ namespace Barotrauma
string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { }); string[] nonRepeatablePrefabNames = element.GetAttributeStringArray("nonrepeatableevents", new string[] { });
NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n))); NonRepeatableEvents.AddRange(EventPrefab.Prefabs.Where(p => nonRepeatablePrefabNames.Any(n => p.Identifier == n)));
UsedUniqueSets = element.GetAttributeIdentifierArray(nameof(UsedUniqueSets), Array.Empty<Identifier>()).ToHashSet();
EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false); EventsExhausted = element.GetAttributeBool(nameof(EventsExhausted).ToLower(), false);
} }
@@ -245,6 +252,23 @@ namespace Barotrauma
return levelData; return levelData;
} }
public bool OutpostGenerationParamsExist => ForceOutpostGenerationParams != null || OutpostGenerationParams.OutpostParams.Any();
public static IEnumerable<OutpostGenerationParams> GetSuitableOutpostGenerationParams(Location location)
{
var suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || p.AllowedLocationTypes.Contains(location.Type.Identifier));
if (!suitableParams.Any())
{
suitableParams = OutpostGenerationParams.OutpostParams.Where(p => location == null || !p.AllowedLocationTypes.Any());
if (!suitableParams.Any())
{
DebugConsole.ThrowError($"No suitable outpost generation parameters found for the location type \"{location.Type.Identifier}\". Selecting random parameters.");
suitableParams = OutpostGenerationParams.OutpostParams;
}
}
return suitableParams;
}
public void Save(XElement parentElement) public void Save(XElement parentElement)
{ {
var newElement = new XElement("Level", var newElement = new XElement("Level",
@@ -287,11 +311,6 @@ namespace Barotrauma
} }
} }
if (UsedUniqueSets.Any())
{
newElement.Add(new XAttribute(nameof(UsedUniqueSets), string.Join(',', UsedUniqueSets)));
}
parentElement.Add(newElement); parentElement.Add(newElement);
} }
} }
@@ -1291,19 +1291,32 @@ namespace Barotrauma
return characters.Sum(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount)); return characters.Sum(c => (int)c.GetStatValue(StatTypes.ExtraSpecialSalesCount));
} }
public int HighestSubmarineTierAvailable(SubmarineClass submarineClass) public bool CanHaveSubsForSale()
{ {
if (!HasOutpost()) { return 0; } return HasOutpost() && CanHaveCampaignInteraction(CampaignMode.InteractionType.PurchaseSub);
return Biome?.HighestSubmarineTierAvailable(submarineClass, Type.Identifier) ?? SubmarineInfo.HighestTier;
} }
public int HighestSubmarineTierAvailable() => HighestSubmarineTierAvailable(SubmarineClass.Undefined); public int HighestSubmarineTierAvailable(SubmarineClass submarineClass = SubmarineClass.Undefined)
{
if (CanHaveSubsForSale())
{
return Biome?.HighestSubmarineTierAvailable(submarineClass, Type.Identifier) ?? SubmarineInfo.HighestTier;
}
return 0;
}
public bool IsSubmarineAvailable(SubmarineInfo info) public bool IsSubmarineAvailable(SubmarineInfo info)
{ {
return Biome?.IsSubmarineAvailable(info, Type.Identifier) ?? true; return Biome?.IsSubmarineAvailable(info, Type.Identifier) ?? true;
} }
private bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType)
{
return LevelData != null &&
LevelData.OutpostGenerationParamsExist &&
LevelData.GetSuitableOutpostGenerationParams(this).Any(p => p.CanHaveCampaignInteraction(interactionType));
}
public void Reset() public void Reset()
{ {
if (Type != OriginalType) if (Type != OriginalType)
@@ -85,9 +85,9 @@ namespace Barotrauma
} }
public float StoreMaxReputationModifier { get; } = 0.1f; public float StoreMaxReputationModifier { get; } = 0.1f;
public float StoreSellPriceModifier { get; } = 0.8f; public float StoreSellPriceModifier { get; } = 0.3f;
public float DailySpecialPriceModifier { get; } = 0.5f; public float DailySpecialPriceModifier { get; } = 0.5f;
public float RequestGoodPriceModifier { get; } = 1.5f; public float RequestGoodPriceModifier { get; } = 2f;
public int StoreInitialBalance { get; } = 5000; public int StoreInitialBalance { get; } = 5000;
/// <summary> /// <summary>
/// In percentages /// In percentages
@@ -260,6 +260,21 @@ namespace Barotrauma
return humanPrefabCollections.GetRandom(randSync); return humanPrefabCollections.GetRandom(randSync);
} }
public bool CanHaveCampaignInteraction(CampaignMode.InteractionType interactionType)
{
foreach (var collection in humanPrefabCollections)
{
foreach (var prefab in collection)
{
if (prefab.CampaignInteractionType == interactionType)
{
return true;
}
}
}
return false;
}
public ImmutableHashSet<Identifier> GetStoreIdentifiers() public ImmutableHashSet<Identifier> GetStoreIdentifiers()
{ {
if (StoreIdentifiers == null) if (StoreIdentifiers == null)
@@ -164,7 +164,11 @@ namespace Barotrauma.Networking
return command; return command;
} }
public static float GetGarbleAmount(Entity listener, Entity sender, float range, float obstructionmult = 2.0f) /// <summary>
/// How much messages sent by <paramref name="sender"/> should get garbled. Takes the distance between the entities and optionally the obstructions between them into account (see <paramref name="obstructionMultiplier"/>).
/// </summary>
/// <param name="obstructionMultiplier">Values greater than or equal to 1 cause the message to get garbled more heavily when there's some obstruction between the characters. Values smaller than 1 mean the garbling only depends on distance.</param>
public static float GetGarbleAmount(Entity listener, Entity sender, float range, float obstructionMultiplier = 2.0f)
{ {
if (listener == null || sender == null) if (listener == null || sender == null)
{ {
@@ -177,12 +181,12 @@ namespace Barotrauma.Networking
Hull listenerHull = listener == null ? null : Hull.FindHull(listener.WorldPosition); Hull listenerHull = listener == null ? null : Hull.FindHull(listener.WorldPosition);
Hull sourceHull = sender == null ? null : Hull.FindHull(sender.WorldPosition); Hull sourceHull = sender == null ? null : Hull.FindHull(sender.WorldPosition);
if (sourceHull != listenerHull) if (sourceHull != listenerHull && obstructionMultiplier >= 1.0f)
{ {
if ((sourceHull == null || !sourceHull.GetConnectedHulls(includingThis: false, searchDepth: 2, ignoreClosedGaps: true).Contains(listenerHull)) && if ((sourceHull == null || !sourceHull.GetConnectedHulls(includingThis: false, searchDepth: 2, ignoreClosedGaps: true).Contains(listenerHull)) &&
Submarine.CheckVisibility(listener.SimPosition, sender.SimPosition) != null) Submarine.CheckVisibility(listener.SimPosition, sender.SimPosition) != null)
{ {
dist = (dist + 100f) * obstructionmult; dist = (dist + 100f) * obstructionMultiplier;
} }
} }
if (dist > range) { return 1.0f; } if (dist > range) { return 1.0f; }
@@ -197,9 +201,9 @@ namespace Barotrauma.Networking
return ApplyDistanceEffect(listener, Sender, Text, SpeakRange); return ApplyDistanceEffect(listener, Sender, Text, SpeakRange);
} }
public static string ApplyDistanceEffect(Entity listener, Entity sender, string text, float range, float obstructionmult = 2.0f) public static string ApplyDistanceEffect(Entity listener, Entity sender, string text, float range, float obstructionMultiplier = 2.0f)
{ {
return ApplyDistanceEffect(text, GetGarbleAmount(listener, sender, range, obstructionmult)); return ApplyDistanceEffect(text, GetGarbleAmount(listener, sender, range, obstructionMultiplier));
} }
public static string ApplyDistanceEffect(string text, float garbleAmount) public static string ApplyDistanceEffect(string text, float garbleAmount)
@@ -252,7 +256,7 @@ namespace Barotrauma.Networking
var senderRadio = senderItem.GetComponent<WifiComponent>(); var senderRadio = senderItem.GetComponent<WifiComponent>();
if (!receiverRadio.CanReceive(senderRadio)) { continue; } if (!receiverRadio.CanReceive(senderRadio)) { continue; }
string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range); string msg = ApplyDistanceEffect(receiverItem, senderItem, message, senderRadio.Range, obstructionMultiplier: 0);
if (sender.SpeechImpediment > 0.0f) if (sender.SpeechImpediment > 0.0f)
{ {
//speech impediment doesn't reduce the range when using a radio, but adds extra garbling //speech impediment doesn't reduce the range when using a radio, but adds extra garbling

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