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

This commit is contained in:
Evil Factory
2026-06-16 09:40:27 -03:00
74 changed files with 1167 additions and 408 deletions

View File

@@ -73,7 +73,7 @@ body:
label: Version
description: Which version of the game did the bug happen in? You can see the current version number in the bottom left corner of your screen in the main menu.
options:
- v1.12.6.2 (Spring Update 2026)
- v1.13.3.1 (Summer Update 2026)
- Other
validations:
required: true

View File

@@ -258,7 +258,9 @@ namespace Barotrauma
//RelativeSpacing = 0.05f
};
InventorySlotContainer = new GUICustomComponent(new RectTransform(new Vector2(0.1f, 1.0f), characterIndicatorArea.RectTransform, Anchor.TopLeft, Pivot.TopRight),
GUIFrame left = new(new RectTransform(new Vector2(0.25f, 1f), characterIndicatorArea.RectTransform), style: null);
InventorySlotContainer = new GUICustomComponent(new RectTransform(Vector2.One, left.RectTransform),
(spriteBatch, component) =>
{
for (int i = 0; i < character.Inventory.Capacity; i++)
@@ -266,6 +268,10 @@ namespace Barotrauma
if (character.Inventory.SlotTypes[i] != InvSlotType.HealthInterface) { continue; }
if (character.Inventory.HideSlot(i)) { continue; }
int width = Character.Inventory.visualSlots[i].Rect.Width;
left.RectTransform.MinSize = new Point(width, left.RectTransform.MinSize.Y);
if (afflictionIconList != null) { afflictionIconList.RectTransform.MinSize = new Point(width, afflictionIconList.RectTransform.MinSize.Y); }
//don't draw the item if it's being dragged out of the slot
bool drawItem = !Inventory.DraggingItems.Any() || !Character.Inventory.GetItemsAt(i).All(it => Inventory.DraggingItems.Contains(it)) || character.Inventory.visualSlots[i].MouseOn();
@@ -292,8 +298,7 @@ namespace Barotrauma
}
});
cprButton = new GUIButton(new RectTransform(new Vector2(0.17f, 0.17f), characterIndicatorArea.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton")
cprButton = new GUIButton(new RectTransform(new Vector2(0.75f), left.RectTransform, Anchor.BottomLeft, scaleBasis: ScaleBasis.Smallest), text: "", style: "CPRButton")
{
UserData = UIHighlightAction.ElementId.CPRButton,
OnClicked = (button, userData) =>
@@ -316,12 +321,11 @@ namespace Barotrauma
return true;
},
ToolTip = TextManager.Get("doctor.cprobjective"),
IgnoreLayoutGroups = true,
ToolTip = TextManager.Get("tutorial.roles.medic.objective.cpr"),
Visible = false
};
var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.4f, 1.0f), characterIndicatorArea.RectTransform),
var limbSelection = new GUICustomComponent(new RectTransform(new Vector2(0.5f, 1.0f), characterIndicatorArea.RectTransform),
(spriteBatch, component) =>
{
DrawHealthWindow(spriteBatch, component.RectTransform.Rect, true);
@@ -368,8 +372,6 @@ namespace Barotrauma
CanBeFocused = false
};
characterIndicatorArea.Recalculate();
healthBarHolder = new GUIFrame(new RectTransform(Point.Zero, GUI.Canvas), style: null)
{
HoverCursor = CursorState.Hand

View File

@@ -866,7 +866,20 @@ namespace Barotrauma
GameSettings.SaveCurrentConfig();
});
}, isCheat: false));
commands.Add(new Command("togglespoofeventmanagerid", "togglespoofeventmanagerid: Forces the client to report the last received event ID as always being 1, making the server believe the client is always behind.", (string[] args) =>
{
if (GameMain.Client != null)
{
GameMain.Client.SpoofEntityManagerReceivedId = !GameMain.Client.SpoofEntityManagerReceivedId;
DebugConsole.NewMessage(GameMain.Client.SpoofEntityManagerReceivedId ? "Spoofing enabled ": "Spoofing disabled", Color.Green);
}
else
{
DebugConsole.NewMessage("Not connected to server", Color.Red);
}
}));
commands.Add(new Command("togglegrid", "Toggle visual snap grid in sub editor.", (string[] args) =>
{
SubEditorScreen.ShouldDrawGrid = !SubEditorScreen.ShouldDrawGrid;
@@ -4441,17 +4454,24 @@ namespace Barotrauma
public static void StartLocalMPSession(int numClients = 2)
{
string extraArguments = "-multiclienttestmode";
if (NetConfig.UseLenientHandshake)
{
extraArguments += " -lenienthandshake";
}
try
{
if (Process.GetProcessesByName("DedicatedServer").Length == 0)
{
#if WINDOWS
Process.Start("DedicatedServer.exe", arguments: "-multiclienttestmode");
Process.Start("DedicatedServer.exe", arguments: extraArguments);
#else
Process.Start("./DedicatedServer", arguments: "-multiclienttestmode");
Process.Start("./DedicatedServer", arguments: extraArguments);
#endif
System.Threading.Thread.Sleep(1000);
}
#if DEBUG
GameClient.MultiClientTestMode = true;
#endif
@@ -4465,10 +4485,13 @@ namespace Barotrauma
for (int i = 2; i <= numClients; i++)
{
System.Threading.Thread.Sleep(1000);
string clientArguments = $"-connect server localhost -username client{i} -skipintro";
#if WINDOWS
Process.Start("Barotrauma.exe", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode");
Process.Start("Barotrauma.exe", arguments: $"{clientArguments} {extraArguments}");
#else
Process.Start("./Barotrauma", arguments: "-connect server localhost -username client" + i + " -multiclienttestmode");
Process.Start("./Barotrauma", arguments: $"{clientArguments} {extraArguments}");
#endif
}
}

View File

@@ -14,6 +14,7 @@ using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma
{
@@ -649,12 +650,12 @@ namespace Barotrauma
DrawMessages(spriteBatch, cam);
if (MouseOn != null)
{
{
MouseOn.OnDrawToolTip?.Invoke(MouseOn);
if (!MouseOn.ToolTip.IsNullOrWhiteSpace())
{
MouseOn.DrawToolTip(spriteBatch);
}
MouseOn.OnDrawToolTip?.Invoke(MouseOn);
}
if (SubEditorScreen.IsSubEditor())
@@ -2323,10 +2324,10 @@ namespace Barotrauma
/// <summary>
/// Creates a 7-segment display.
/// </summary>
/// <param name="leftLabel">Returns <see langword="null"/> if <paramref name="leftLabelText"/> is <see langword="null"/> or empty.</param>
/// <param name="leftLabel">Returns <see langword="null"/> if <paramref name="leftLabelText"/> is <see langword="null"/>.</param>
/// <param name="rightLabelText">Defaults to <c>TextManager.Get("kilowatt")</c>.</param>
/// <param name="leftLabelFont">Defaults to <see cref="GUIStyle.LargeFont"/>.</param>
public static GUITextBlock CreateDigitalDisplay(RectTransform rect, out GUITextBlock? leftLabel, out GUITextBlock rightLabel, LocalizedString? leftLabelText = null, LocalizedString? rightLabelText = null, LocalizedString? tooltip = null, GUIFont? leftLabelFont = null)
public static GUITextBlock CreateDigitalDisplay(RectTransform rect, [NotNullIfNotNull(nameof(leftLabelText))] out GUITextBlock? leftLabel, out GUITextBlock rightLabel, LocalizedString? leftLabelText = null, LocalizedString? rightLabelText = null, LocalizedString? tooltip = null, GUIFont? leftLabelFont = null)
{
GUILayoutGroup textArea = new(rect, isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
@@ -2337,7 +2338,7 @@ namespace Barotrauma
};
leftLabel = null;
if (!leftLabelText.IsNullOrEmpty())
if (leftLabelText != null)
{
leftLabel = new GUITextBlock(new RectTransform(new Vector2(0.4f, 1f), textArea.RectTransform), leftLabelText, textColor: GUIStyle.TextColorBright, font: leftLabelFont ?? GUIStyle.LargeFont, textAlignment: Alignment.CenterRight);
}

View File

@@ -384,14 +384,14 @@ namespace Barotrauma
CaretIndex = forcedCaretIndex == - 1 ? textBlock.GetCaretIndexFromScreenPos(PlayerInput.MousePosition) : forcedCaretIndex;
CalculateCaretPos();
ClearSelection();
bool wasSelected = selected;
selected = true;
GUI.KeyboardDispatcher.Subscriber = this;
OnSelected?.Invoke(this, Keys.None);
if (!wasSelected && PlaySoundOnSelect && !ignoreSelectSound)
if (!selected && PlaySoundOnSelect && !ignoreSelectSound)
{
SoundPlayer.PlayUISound(GUISoundType.Select);
}
selected = true;
//set this after we've set selected to true -> otherwise the textbox taking keyboard focus will trigger Select again
GUI.KeyboardDispatcher.Subscriber = this;
}
public void Deselect()

View File

@@ -113,7 +113,10 @@ namespace Barotrauma
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, samplerState: GUI.SamplerState);
GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea);
if (currentBackgroundTexture.Texture != null)
{
GUI.DrawBackgroundSprite(spriteBatch, currentBackgroundTexture, Color.White, drawArea);
}
overlay.Draw(spriteBatch, Vector2.Zero, scale: overlayScale);
double noiseT = Timing.TotalTime * 0.02f;

View File

@@ -278,6 +278,12 @@ namespace Barotrauma
GameClient.MultiClientTestMode = true;
}
#endif
if (ConsoleArguments.Contains("-lenienthandshake"))
{
NetConfig.UseLenientHandshake = true;
}
GUI.KeyboardDispatcher = new EventInput.KeyboardDispatcher(Window);
PerformanceCounter = new PerformanceCounter();

View File

@@ -127,12 +127,6 @@ namespace Barotrauma
(int)SlotPositions[i].X,
(int)SlotPositions[i].Y,
(int)(slotSprite.size.X * multiplier), (int)(slotSprite.size.Y * multiplier));
if (SlotTypes[i] == InvSlotType.HealthInterface &&
character.CharacterHealth?.InventorySlotContainer != null)
{
slotRect.Width = slotRect.Height = (int)(character.CharacterHealth.InventorySlotContainer.Rect.Width * 1.2f);
}
ItemContainer itemContainer = slots[i].FirstOrDefault()?.GetComponent<ItemContainer>();
if (itemContainer != null)
@@ -622,6 +616,7 @@ namespace Barotrauma
for (int i = 0; i < capacity; i++)
{
if (HideSlot(i)) { continue; }
var item = slots[i].FirstOrDefault();
if (item != null)
{

View File

@@ -26,7 +26,7 @@ namespace Barotrauma.Items.Components
}
else
{
DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg));
DisplayMsg = TextManager.ParseInputTypes(TextManager.Get(Msg)).Fallback(Msg);
}
CharacterHUD.RecreateHudTextsIfControlling(Character.Controlled);

View File

@@ -12,9 +12,12 @@ namespace Barotrauma.Items.Components
private partial class PowerGroup
{
private GUIFrame? frame;
private GUIFrame? groupContent;
private GUILayoutGroup? nameGroup;
private GUITextBox? nameBox;
private GUITextBlock? loadDisplayNameLabel;
private GUIScrollBar? ratioSlider;
private readonly List<GUITextBlock> powerUnitLabels = new List<GUITextBlock>();
private readonly List<GUITextBlock> powerUnitLabels = [];
private GUIFrame? divider;
public bool IsVisible { get; private set; } = true;
@@ -22,9 +25,9 @@ namespace Barotrauma.Items.Components
public void CreateGUI()
{
frame = new GUIFrame(new RectTransform(new Vector2(1f, 0.25f), distributor.groupList!.Content.RectTransform, minSize: (0, 130)), style: null);
GUIFrame groupContent = new(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null);
groupContent = new GUIFrame(new RectTransform(frame.Rect.Size - new Point(10), frame.RectTransform, Anchor.Center), style: null);
GUILayoutGroup nameGroup = new(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft)
nameGroup = new GUILayoutGroup(new RectTransform(new Vector2(0.65f, 0.33f), groupContent.RectTransform, Anchor.TopLeft), isHorizontal: true, childAnchor: Anchor.CenterLeft)
{
Stretch = true
};
@@ -37,7 +40,7 @@ namespace Barotrauma.Items.Components
return true;
}
};
nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Name, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle")
nameBox = new GUITextBox(new RectTransform(Vector2.One, nameGroup.RectTransform), Screen.Selected == GameMain.SubEditorScreen ? Name : DisplayName.Value, font: GUIStyle.SubHeadingFont, style: "GUITextBoxNoStyle")
{
MaxTextLength = MaxNameLength,
OverflowClip = true,
@@ -48,17 +51,40 @@ namespace Barotrauma.Items.Components
return true;
}
};
nameBox.OnSelected += (tb, _) =>
{
if (tb.Selected) { return; }
tb.Text = Name;
};
nameBox.OnDeselected += (tb, _) =>
{
Name = tb.Text;
tb.CaretIndex = 0;
if (GameMain.Client == null) { return; }
distributor.item.CreateClientEvent(distributor, new EventData(this, EventType.NameChange));
};
nameBox.GetChild<GUIFrame>().GetChild<GUICustomComponent>().OnDrawToolTip = comp =>
{
if (Screen.Selected != GameMain.SubEditorScreen && !nameBox.Selected)
{
comp.ToolTip = null;
return;
}
LocalizedString localizedText = TextManager.Get(nameBox.Text);
comp.ToolTip = localizedText.IsNullOrEmpty()
? TextManager.GetWithVariable("StringPropertyCannotTranslate", "[tag]", nameBox.Text)
: TextManager.GetWithVariable("StringPropertyTranslate", "[translation]", localizedText);
};
GUITextBlock loadDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.TopRight) { AbsoluteOffset = (5, 0) },
out GUITextBlock? _, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font);
out loadDisplayNameLabel, out GUITextBlock loadDisplayUnitLabel, TextManager.Get("PowerTransferLoadLabel"), tooltip: TextManager.Get("PowerTransferTipLoad"), leftLabelFont: GUIStyle.Font);
loadDisplay.TextGetter = () => MathUtils.RoundToInt(Load).ToString();
float textAndPaddingWidth = loadDisplayNameLabel!.Font.MeasureString(loadDisplayNameLabel!.Text).X + loadDisplayNameLabel.Padding.X + loadDisplayNameLabel.Padding.Z;
float availableWidth = groupContent!.Rect.Width - loadDisplayNameLabel.Parent.Rect.Width + loadDisplayNameLabel.Rect.Width - textAndPaddingWidth;
nameGroup!.RectTransform.Resize(new Point((int)availableWidth, nameGroup.Rect.Height));
ratioSlider = new GUIScrollBar(new RectTransform(new Vector2(1f, 0.33f), groupContent.RectTransform, Anchor.Center), barSize: 0.15f, style: "DeviceSlider")
{
Step = SupplyRatioStep,
@@ -78,16 +104,17 @@ namespace Barotrauma.Items.Components
ratioSlider.Bar.RectTransform.MaxSize = new Point(ratioSlider.Bar.Rect.Height);
GUITextBlock ratioDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.2f, 0.33f), groupContent.RectTransform, Anchor.BottomLeft),
out GUITextBlock? _, out GUITextBlock _,
out GUITextBlock? _, out GUITextBlock ratioDisplayUnitLabel,
rightLabelText: "%");
ratioDisplay.TextGetter = () => DisplayRatio.ToString();
GUITextBlock outputDisplay = GUI.CreateDigitalDisplay(new RectTransform(new Vector2(0.35f, 0.33f), groupContent.RectTransform, Anchor.BottomRight) { AbsoluteOffset = (5, 0) },
out GUITextBlock? _, out GUITextBlock outputDisplayUnitLabel,
out GUITextBlock? outputDisplayNameLabel, out GUITextBlock outputDisplayUnitLabel,
TextManager.Get("powerdistributor.supplylabel"), tooltip: TextManager.Get("PowerTransferTipPower"), leftLabelFont: GUIStyle.Font);
outputDisplay.TextGetter = () => distributor.IsShortCircuited(PowerOut) ? "err" : MathUtils.RoundToInt(distributor.CalculatePowerOut(this)).ToString();
powerUnitLabels.Add(loadDisplayUnitLabel);
powerUnitLabels.Add(ratioDisplayUnitLabel);
powerUnitLabels.Add(outputDisplayUnitLabel);
GUITextBlock.AutoScaleAndNormalize(powerUnitLabels);
@@ -111,7 +138,14 @@ namespace Barotrauma.Items.Components
IsVisible = PowerOut.Wires.Count >= 1;
frame!.Visible = IsVisible;
divider!.Visible = IsVisible && distributor.powerGroups.Last(group => group.frame!.Visible) != this;
if (distributor.prevLanguage != GameSettings.CurrentConfig.Language) { GUITextBlock.AutoScaleAndNormalize(powerUnitLabels); }
if (distributor.prevLanguage != GameSettings.CurrentConfig.Language)
{
GUITextBlock.AutoScaleAndNormalize(powerUnitLabels);
float textAndPaddingWidth = loadDisplayNameLabel!.Font.MeasureString(loadDisplayNameLabel!.Text).X + loadDisplayNameLabel.Padding.X + loadDisplayNameLabel.Padding.Z;
float availableWidth = groupContent!.Rect.Width - loadDisplayNameLabel.Parent.Rect.Width + loadDisplayNameLabel.Rect.Width - textAndPaddingWidth;
nameGroup!.RectTransform.Resize(new Point((int)availableWidth, nameGroup.Rect.Height));
}
}
}

View File

@@ -716,13 +716,19 @@ namespace Barotrauma.Items.Components
GetAvailablePower(out float batteryCharge, out float batteryCapacity);
List<Item> availableAmmo = new List<Item>();
List<Item> availableAmmo = [];
AddAmmoFromContainer(item.GetComponent<ItemContainer>());
foreach (MapEntity e in item.linkedTo)
{
if (!(e is Item linkedItem)) { continue; }
var itemContainer = linkedItem.GetComponent<ItemContainer>();
if (itemContainer == null) { continue; }
if (e is not Item linkedItem) { continue; }
AddAmmoFromContainer(linkedItem.GetComponent<ItemContainer>());
}
void AddAmmoFromContainer(ItemContainer itemContainer)
{
if (itemContainer == null) { return; }
availableAmmo.AddRange(itemContainer.Inventory.AllItems);
//add empty slots too
for (int i = 0; i < itemContainer.Inventory.Capacity - itemContainer.Inventory.AllItems.Count(); i++)
{
availableAmmo.Add(null);

View File

@@ -339,6 +339,19 @@ namespace Barotrauma
toolTip += $"‖color:{conditionColorStr}‖ ({(int)item.ConditionPercentage} %)‖color:end‖";
}
if (!description.IsNullOrEmpty()) { toolTip += '\n' + description; }
if (item.Prefab.UnlockedRecipeInToolTip.Length > 0 && GameMain.GameSession is { } GameSession)
{
if (item.Prefab.UnlockedRecipeInToolTip.All(id => GameSession.HasUnlockedRecipe(Character.Controlled, id)))
{
toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Green)}‖{TextManager.Get("unlockedrecipe.true")}‖color:end‖";
}
else
{
toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Yellow)}‖{TextManager.Get("unlockedrecipe.false")}‖color:end‖";
}
}
if (item.Prefab.ContentPackage != GameMain.VanillaContent && item.Prefab.ContentPackage != null)
{
colorStr = XMLExtensions.ToStringHex(Color.MediumPurple);
@@ -356,19 +369,7 @@ namespace Barotrauma
}
#if DEBUG
toolTip += $" ({item.Prefab.Identifier})";
#endif
if (!item.Prefab.UnlockedRecipeInToolTip.IsEmpty && GameMain.GameSession is { } GameSession)
{
if (GameSession.HasUnlockedRecipe(Character.Controlled, item.Prefab.UnlockedRecipeInToolTip))
{
toolTip += TextManager.Get("unlockedrecipe.true");
}
else
{
toolTip += $"\n‖color:{XMLExtensions.ToStringHex(GUIStyle.Yellow)}‖{TextManager.Get("unlockedrecipe.false")}‖color:end‖";
}
}
#endif
if (PlayerInput.KeyDown(InputType.ContextualCommand))
{
toolTip += $"\n‖color:gui.blue‖{TextManager.ParseInputTypes(TextManager.Get("itemmsgcontextualorders"))}‖color:end‖";
@@ -1300,8 +1301,7 @@ namespace Barotrauma
SubEditorScreen.StoreCommand(new InventoryPlaceCommand(DraggingItems.First().ParentInventory, new List<Item>(DraggingItems), true));
}
}
SoundPlayer.PlayUISound(GUISoundType.DropItem);
bool removed = false;
if (Screen.Selected is SubEditorScreen editor)
{

View File

@@ -8,7 +8,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
class BackgroundCreature : ISteerable
class BackgroundCreature : ISteerable, ILevelRenderableObject
{
const float MaxDepth = 10000.0f;
@@ -76,6 +76,8 @@ namespace Barotrauma
set;
}
public Vector3 Position => new Vector3(position.X, position.Y, Depth);
public BackgroundCreature(BackgroundCreaturePrefab prefab, Vector2 position)
{
this.Prefab = prefab;

View File

@@ -3,7 +3,6 @@ using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
@@ -15,52 +14,11 @@ namespace Barotrauma
private float checkVisibleTimer;
private readonly List<BackgroundCreature> creatures = new List<BackgroundCreature>();
private readonly List<BackgroundCreature> creatures = [];
private readonly List<BackgroundCreature> visibleCreatures = new List<BackgroundCreature>();
private readonly List<BackgroundCreature> visibleCreatures = [];
public BackgroundCreatureManager()
{
/*foreach(var file in files)
{
LoadConfig(file.Path);
}*/
}
/*public BackgroundCreatureManager(string path)
{
DebugConsole.AddWarning($"Couldn't find any BackgroundCreaturePrefabs files, falling back to {path}");
LoadConfig(ContentPath.FromRaw(null, path));
}
private void LoadConfig(ContentPath configPath)
{
try
{
XDocument doc = XMLExtensions.TryLoadXml(configPath);
if (doc == null) { return; }
var mainElement = doc.Root.FromPackage(configPath.ContentPackage);
if (mainElement.IsOverride())
{
mainElement = mainElement.FirstElement();
Prefabs.Clear();
DebugConsole.NewMessage($"Overriding all background creatures with '{configPath}'", Color.MediumPurple);
}
else if (Prefabs.Any())
{
DebugConsole.NewMessage($"Loading additional background creatures from file '{configPath}'");
}
foreach (var element in mainElement.Elements())
{
Prefabs.Add(new BackgroundCreaturePrefab(element));
};
}
catch (Exception e)
{
DebugConsole.ThrowError(String.Format("Failed to load BackgroundCreatures from {0}", configPath), e);
}
}*/
public IEnumerable<BackgroundCreature> VisibleCreatures => visibleCreatures;
public void SpawnCreatures(Level level, int count, Vector2? position = null)
{
@@ -161,14 +119,6 @@ namespace Barotrauma
}
}
public void Draw(SpriteBatch spriteBatch, Camera cam)
{
foreach (BackgroundCreature creature in visibleCreatures)
{
creature.Draw(spriteBatch, cam);
}
}
public void DrawLights(SpriteBatch spriteBatch, Camera cam)
{
foreach (BackgroundCreature creature in visibleCreatures)

View File

@@ -140,7 +140,7 @@ namespace Barotrauma
public void DrawFront(SpriteBatch spriteBatch, Camera cam)
{
renderer?.DrawForeground(spriteBatch, cam, LevelObjectManager);
renderer?.DrawForeground(spriteBatch, cam, backgroundCreatureManager, LevelObjectManager);
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{

View File

@@ -12,7 +12,7 @@ using System.Xml.Linq;
namespace Barotrauma
{
partial class LevelObject
partial class LevelObject : ILevelRenderableObject
{
public float SwingTimer;
public float ScaleOscillateTimer;

View File

@@ -8,13 +8,20 @@ using System.Linq;
namespace Barotrauma
{
public interface ILevelRenderableObject
{
public Vector3 Position { get; }
}
partial class LevelObjectManager
{
// Pre-initialized to the max size, so that we don't have to resize the lists at runtime. TODO: Could the capacity (of some collections?) be lower?
private readonly List<LevelObject> visibleObjectsBack = new List<LevelObject>(MaxVisibleObjects);
private readonly List<LevelObject> visibleObjectsMid = new List<LevelObject>(MaxVisibleObjects);
private readonly List<LevelObject> visibleObjectsFront = new List<LevelObject>(MaxVisibleObjects);
private readonly HashSet<LevelObject> allVisibleObjects = new HashSet<LevelObject>(MaxVisibleObjects);
private readonly List<ILevelRenderableObject> visibleObjectsBack = new List<ILevelRenderableObject>(MaxVisibleObjects);
private readonly List<ILevelRenderableObject> visibleObjectsMid = new List<ILevelRenderableObject>(MaxVisibleObjects);
private readonly List<ILevelRenderableObject> visibleObjectsFront = new List<ILevelRenderableObject>(MaxVisibleObjects);
private readonly HashSet<ILevelRenderableObject> allVisibleObjects = new HashSet<ILevelRenderableObject>(MaxVisibleObjects);
private double NextRefreshTime;
@@ -35,35 +42,44 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime, Camera cam)
{
foreach (LevelObject obj in visibleObjectsBack)
foreach (ILevelRenderableObject obj in visibleObjectsBack)
{
obj.Update(deltaTime, cam);
if (obj is LevelObject levelObj)
{
levelObj.Update(deltaTime, cam);
}
}
foreach (LevelObject obj in visibleObjectsMid)
foreach (ILevelRenderableObject obj in visibleObjectsMid)
{
obj.Update(deltaTime, cam);
if (obj is LevelObject levelObj)
{
levelObj.Update(deltaTime, cam);
}
}
foreach (LevelObject obj in visibleObjectsFront)
foreach (ILevelRenderableObject obj in visibleObjectsFront)
{
obj.Update(deltaTime, cam);
if (obj is LevelObject levelObj)
{
levelObj.Update(deltaTime, cam);
}
}
}
/// <summary>
/// Returns all visible objects, but not in order, because internally uses a HashSet.
/// </summary>
public IEnumerable<LevelObject> GetAllVisibleObjects()
public IEnumerable<ILevelRenderableObject> GetAllVisibleObjects()
{
allVisibleObjects.Clear();
foreach (LevelObject obj in visibleObjectsBack)
foreach (ILevelRenderableObject obj in visibleObjectsBack)
{
allVisibleObjects.Add(obj);
}
foreach (LevelObject obj in visibleObjectsMid)
foreach (ILevelRenderableObject obj in visibleObjectsMid)
{
allVisibleObjects.Add(obj);
}
foreach (LevelObject obj in visibleObjectsFront)
foreach (ILevelRenderableObject obj in visibleObjectsFront)
{
allVisibleObjects.Add(obj);
}
@@ -73,7 +89,7 @@ namespace Barotrauma
/// <summary>
/// Checks which level objects are in camera view and adds them to the visibleObjects lists
/// </summary>
private void RefreshVisibleObjects(Rectangle currentIndices, float zoom)
private void RefreshVisibleObjects(Rectangle currentIndices, BackgroundCreatureManager backgroundCreatureManager, float zoom)
{
visibleObjectsBack.Clear();
visibleObjectsMid.Clear();
@@ -152,6 +168,27 @@ namespace Barotrauma
}
}
foreach (var backgroundCreature in backgroundCreatureManager.VisibleCreatures)
{
int drawOrderIndex = 0;
for (int i = 0; i < visibleObjectsBack.Count; i++)
{
if (visibleObjectsBack[i].Position.Z > backgroundCreature.Position.Z)
{
break;
}
else
{
drawOrderIndex = i + 1;
if (drawOrderIndex >= MaxVisibleObjects) { break; }
}
}
if (drawOrderIndex >= 0 && drawOrderIndex < MaxVisibleObjects)
{
visibleObjectsBack.Insert(drawOrderIndex, backgroundCreature);
}
}
//object grid is sorted in an ascending order
//(so we prefer the objects in the foreground instead of ones in the background if some need to be culled)
//rendering needs to be done in a descending order though to get the background objects to be drawn first -> reverse the lists
@@ -165,28 +202,28 @@ namespace Barotrauma
/// <summary>
/// Draw the objects behind the level walls
/// </summary>
public void DrawObjectsBack(SpriteBatch spriteBatch, Camera cam)
public void DrawObjectsBack(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
{
DrawObjects(spriteBatch, cam, visibleObjectsBack);
DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsBack);
}
/// <summary>
/// Draw the objects in front of the level walls, but behind characters
/// </summary>
public void DrawObjectsMid(SpriteBatch spriteBatch, Camera cam)
public void DrawObjectsMid(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
{
DrawObjects(spriteBatch, cam, visibleObjectsMid);
DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsMid);
}
/// <summary>
/// Draw the objects in front of the level walls and characters
/// </summary>
public void DrawObjectsFront(SpriteBatch spriteBatch, Camera cam)
public void DrawObjectsFront(SpriteBatch spriteBatch, BackgroundCreatureManager backgroundCreatureManager, Camera cam)
{
DrawObjects(spriteBatch, cam, visibleObjectsFront);
DrawObjects(spriteBatch, cam, backgroundCreatureManager, visibleObjectsFront);
}
private void DrawObjects(SpriteBatch spriteBatch, Camera cam, List<LevelObject> objectList)
private void DrawObjects(SpriteBatch spriteBatch, Camera cam, BackgroundCreatureManager backgroundCreatureManager, List<ILevelRenderableObject> objectList)
{
Rectangle indices = Rectangle.Empty;
indices.X = (int)Math.Floor(cam.WorldView.X / (float)GridSize);
@@ -207,7 +244,7 @@ namespace Barotrauma
float z = 0.0f;
if (ForceRefreshVisibleObjects || (currentGridIndices != indices && Timing.TotalTime > NextRefreshTime))
{
RefreshVisibleObjects(indices, cam.Zoom);
RefreshVisibleObjects(indices, backgroundCreatureManager, cam.Zoom);
ForceRefreshVisibleObjects = false;
if (cam.Zoom < 0.1f)
{
@@ -216,61 +253,93 @@ namespace Barotrauma
}
}
foreach (LevelObject obj in objectList)
bool prevObjectHasDeformableSprite = false;
foreach (ILevelRenderableObject obj2 in objectList)
{
Vector2 camDiff = new Vector2(obj.Position.X, obj.Position.Y) - cam.WorldViewCenter;
Vector2 camDiff = new Vector2(obj2.Position.X, obj2.Position.Y) - cam.WorldViewCenter;
camDiff.Y = -camDiff.Y;
Sprite activeSprite = obj.Sprite;
activeSprite?.Draw(
spriteBatch,
new Vector2(obj.Position.X, -obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength,
Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / obj.Prefab.FadeOutDepth),
activeSprite.Origin,
obj.CurrentRotation,
obj.CurrentScale,
SpriteEffects.None,
z);
if (obj.ActivePrefab.DeformableSprite != null)
bool hasDeformableSprite = false;
if (obj2 is LevelObject levelObject)
{
if (obj.CurrentSpriteDeformation != null)
hasDeformableSprite = levelObject.ActivePrefab.DeformableSprite != null;
if (hasDeformableSprite != prevObjectHasDeformableSprite)
{
obj.ActivePrefab.DeformableSprite.Deform(obj.CurrentSpriteDeformation);
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied,
SamplerState.LinearWrap, DepthStencilState.DepthRead,
transformMatrix: cam.Transform);
}
else
Sprite activeSprite = levelObject.Sprite;
activeSprite?.Draw(
spriteBatch,
new Vector2(levelObject.Position.X, -levelObject.Position.Y) - camDiff * levelObject.Position.Z * ParallaxStrength,
Color.Lerp(levelObject.Prefab.SpriteColor, levelObject.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), levelObject.Position.Z / levelObject.Prefab.FadeOutDepth),
activeSprite.Origin,
levelObject.CurrentRotation,
levelObject.CurrentScale,
SpriteEffects.None,
z);
if (hasDeformableSprite)
{
obj.ActivePrefab.DeformableSprite.Reset();
}
obj.ActivePrefab.DeformableSprite?.Draw(cam,
new Vector3(new Vector2(obj.Position.X, obj.Position.Y) - camDiff * obj.Position.Z * ParallaxStrength, z * 10.0f),
obj.ActivePrefab.DeformableSprite.Origin,
obj.CurrentRotation,
obj.CurrentScale,
Color.Lerp(obj.Prefab.SpriteColor, obj.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), obj.Position.Z / 5000.0f));
}
if (GameMain.DebugDraw)
{
GUI.DrawRectangle(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true);
if (obj.Triggers == null) { continue; }
foreach (LevelTrigger trigger in obj.Triggers)
{
if (trigger.PhysicsBody == null) continue;
GUI.DrawLine(spriteBatch, new Vector2(obj.Position.X, -obj.Position.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), Color.Cyan, 0, 3);
Vector2 flowForce = trigger.GetWaterFlowVelocity();
if (flowForce.LengthSquared() > 1)
if (levelObject.CurrentSpriteDeformation != null)
{
flowForce.Y = -flowForce.Y;
GUI.DrawLine(spriteBatch, new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y) + flowForce * 10, GUIStyle.Orange, 0, 5);
levelObject.ActivePrefab.DeformableSprite.Deform(levelObject.CurrentSpriteDeformation);
}
trigger.PhysicsBody.UpdateDrawPosition();
trigger.PhysicsBody.DebugDraw(spriteBatch, trigger.IsTriggered ? Color.Cyan : Color.DarkCyan);
else
{
levelObject.ActivePrefab.DeformableSprite.Reset();
}
levelObject.ActivePrefab.DeformableSprite?.Draw(cam,
new Vector3(new Vector2(levelObject.Position.X, levelObject.Position.Y) - camDiff * levelObject.Position.Z * ParallaxStrength, z * 10.0f),
levelObject.ActivePrefab.DeformableSprite.Origin,
levelObject.CurrentRotation,
levelObject.CurrentScale,
Color.Lerp(levelObject.Prefab.SpriteColor, levelObject.Prefab.SpriteColor.Multiply(Level.Loaded.BackgroundTextureColor), levelObject.Position.Z / 5000.0f));
}
prevObjectHasDeformableSprite = hasDeformableSprite;
if (GameMain.DebugDraw)
{
GUI.DrawRectangle(spriteBatch, new Vector2(levelObject.Position.X, -levelObject.Position.Y), new Vector2(10.0f, 10.0f), GUIStyle.Red, true);
if (levelObject.Triggers == null) { continue; }
foreach (LevelTrigger trigger in levelObject.Triggers)
{
if (trigger.PhysicsBody == null) continue;
GUI.DrawLine(spriteBatch, new Vector2(levelObject.Position.X, -levelObject.Position.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), Color.Cyan, 0, 3);
Vector2 flowForce = trigger.GetWaterFlowVelocity();
if (flowForce.LengthSquared() > 1)
{
flowForce.Y = -flowForce.Y;
GUI.DrawLine(spriteBatch, new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y), new Vector2(trigger.WorldPosition.X, -trigger.WorldPosition.Y) + flowForce * 10, GUIStyle.Orange, 0, 5);
}
trigger.PhysicsBody.UpdateDrawPosition();
trigger.PhysicsBody.DebugDraw(spriteBatch, trigger.IsTriggered ? Color.Cyan : Color.DarkCyan);
}
}
}
else if (obj2 is BackgroundCreature backgroundCreature && cam.Zoom > 0.05f)
{
hasDeformableSprite = backgroundCreature.Prefab.DeformableSprite != null;
if (hasDeformableSprite != prevObjectHasDeformableSprite)
{
spriteBatch.End();
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied,
SamplerState.LinearWrap, DepthStencilState.DepthRead,
transformMatrix: cam.Transform);
}
backgroundCreature.Draw(spriteBatch, cam);
}
prevObjectHasDeformableSprite = hasDeformableSprite;
z += 0.0001f;
}

View File

@@ -214,8 +214,9 @@ namespace Barotrauma
//calculate the sum of the forces of nearby level triggers
//and use it to move the water texture and water distortion effect
Vector2 currentWaterParticleVel = level.GenerationParams.WaterParticleVelocity;
foreach (LevelObject levelObject in level.LevelObjectManager.GetAllVisibleObjects())
foreach (ILevelRenderableObject obj in level.LevelObjectManager.GetAllVisibleObjects())
{
if (obj is not LevelObject levelObject) { continue; }
if (levelObject.Triggers == null) { continue; }
//use the largest water flow velocity of all the triggers
Vector2 objectMaxFlow = Vector2.Zero;
@@ -274,11 +275,7 @@ namespace Barotrauma
SamplerState.LinearWrap, DepthStencilState.DepthRead, null, null,
cam.Transform);
backgroundSpriteManager?.DrawObjectsBack(spriteBatch, cam);
if (cam.Zoom > 0.05f)
{
backgroundCreatureManager?.Draw(spriteBatch, cam);
}
backgroundSpriteManager?.DrawObjectsBack(spriteBatch, backgroundCreatureManager, cam);
level.GenerationParams.DrawWaterParticles(spriteBatch, cam, waterParticleOffset);
@@ -292,17 +289,18 @@ namespace Barotrauma
BlendState.NonPremultiplied,
SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null,
cam.Transform);
backgroundSpriteManager?.DrawObjectsMid(spriteBatch, cam);
backgroundSpriteManager?.DrawObjectsMid(spriteBatch, backgroundCreatureManager, cam);
spriteBatch.End();
}
public void DrawForeground(SpriteBatch spriteBatch, Camera cam, LevelObjectManager backgroundSpriteManager = null)
public void DrawForeground(SpriteBatch spriteBatch, Camera cam,
BackgroundCreatureManager backgroundCreatureManager, LevelObjectManager backgroundSpriteManager = null)
{
spriteBatch.Begin(SpriteSortMode.Deferred,
BlendState.NonPremultiplied,
SamplerState.LinearClamp, DepthStencilState.DepthRead, null, null,
cam.Transform);
backgroundSpriteManager?.DrawObjectsFront(spriteBatch, cam);
backgroundSpriteManager?.DrawObjectsFront(spriteBatch, backgroundCreatureManager, cam);
spriteBatch.End();
}

View File

@@ -2547,7 +2547,7 @@ namespace Barotrauma.Networking
segmentTable.StartNewSegment(ClientNetSegment.SyncIds);
//outmsg.Write(GameMain.NetLobbyScreen.LastUpdateID);
outmsg.WriteUInt16(ChatMessage.LastID);
outmsg.WriteUInt16(EntityEventManager.LastReceivedID);
outmsg.WriteUInt16(SpoofEntityManagerReceivedId ? (ushort)1 : EntityEventManager.LastReceivedID);
outmsg.WriteUInt16(LastClientListUpdateID);
if (!(GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign) || campaign.LastSaveID == 0)
@@ -3370,6 +3370,12 @@ namespace Barotrauma.Networking
{
get { return votingInterface; }
}
/// <summary>
/// Forces the client to report the last received event ID as always being 1, making the server believe the client is always behind.
/// </summary>
public bool SpoofEntityManagerReceivedId { get; set; }
private VotingInterface votingInterface;
public bool TypingChatMessage(GUITextBox textBox, string text)

View File

@@ -89,6 +89,7 @@ namespace Barotrauma.Networking
{
byte queueId = msg.ReadByte();
float distanceFactor = msg.ReadRangedSingle(0.0f, 1.0f, 8);
bool isRadio = msg.ReadBoolean();
VoipQueue queue = queues.Find(q => q.QueueID == queueId);
if (queue == null)
@@ -117,19 +118,21 @@ namespace Barotrauma.Networking
float rangeMultiplier = spectating ? 2.0f : 1.0f;
WifiComponent senderRadio = null;
var messageType =
!client.VoipQueue.ForceLocal &&
ChatMessage.CanUseRadio(client.Character, out senderRadio) &&
(spectating || (ChatMessage.CanUseRadio(Character.Controlled, out var recipientRadio) && senderRadio.CanReceive(recipientRadio)))
? ChatMessageType.Radio : ChatMessageType.Default;
var messageType = isRadio ? ChatMessageType.Radio : ChatMessageType.Default;
client.Character.ShowTextlessSpeechBubble(1.25f, ChatMessage.MessageColor[(int)messageType]);
client.VoipSound.UseRadioFilter = messageType == ChatMessageType.Radio && !GameSettings.CurrentConfig.Audio.DisableVoiceChatFilters;
client.RadioNoise = 0.0f;
if (messageType == ChatMessageType.Radio)
{
//If the client cannot establish a radio, use a headsets default range as a fallback to calculate the radio noise.
//This cannot happen in an un-modded setting as CanUseRadio is part of the server side check for isRadio to be true.
ChatMessage.CanUseRadio(client.Character, out senderRadio);
float senderRadioRange = (senderRadio == null) ? 35000.0f : senderRadio.Range;
client.VoipSound.UsingRadio = true;
client.VoipSound.SetRange(senderRadio.Range * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadio.Range * speechImpedimentMultiplier * rangeMultiplier);
client.VoipSound.SetRange(senderRadioRange * RangeNear * speechImpedimentMultiplier * rangeMultiplier, senderRadioRange * speechImpedimentMultiplier * rangeMultiplier);
if (distanceFactor > RangeNear && !spectating)
{
//noise starts increasing exponentially after 40% range

View File

@@ -49,9 +49,6 @@ namespace Barotrauma
private GUITextBox serverNameBox, passwordBox, maxPlayersBox;
private GUITickBox isPublicBox, wrongPasswordBanBox, karmaBox;
private GUIDropDown languageDropdown, serverExecutableDropdown;
#if DEBUG
private GUITickBox lenientHandshakeBox;
#endif
private readonly GUIButton joinServerButton, hostServerButton;
private readonly GUIFrame modsButtonContainer;
@@ -533,6 +530,18 @@ namespace Barotrauma
return true;
}
};
new GUITickBox(new RectTransform(new Point(300, 30), Frame.RectTransform, Anchor.TopRight) { AbsoluteOffset = new Point(40, 280) },
"Lenient server startup timeouts")
{
Selected = NetConfig.UseLenientHandshake,
ToolTip = "Start with more lenient Lidgren handshake timeouts. The server is more likely to start even when running multiple instances on the same machine under heavy load.",
OnSelected = (tickBox) =>
{
NetConfig.UseLenientHandshake = tickBox.Selected;
return true;
}
};
#endif
var minButtonSize = new Point(120, 20);
@@ -1118,13 +1127,11 @@ namespace Barotrauma
int ownerKey = Math.Max(CryptoRandom.Instance.Next(), 1);
arguments.Add("-ownerkey");
arguments.Add(ownerKey.ToString());
#if DEBUG
if (lenientHandshakeBox.Selected)
if (NetConfig.UseLenientHandshake)
{
arguments.Add("-lenienthandshake");
NetConfig.UseLenientHandshake = true;
}
#endif
var processInfo = new ProcessStartInfo
{
@@ -1594,14 +1601,6 @@ namespace Barotrauma
ToolTip = TextManager.Get("hostserverkarmasettingtooltip")
};
#if DEBUG
lenientHandshakeBox = new GUITickBox(new RectTransform(new Vector2(0.5f, 1.0f), tickboxAreaLower.RectTransform), "DEBUG: Lenient server startup timeouts")
{
Selected = true,
ToolTip = "Start with more lenient Lidgren handshake timeouts. The server is more likely to start even when running multiple instances on the same machine under heavy load."
};
#endif
tickboxAreaLower.RectTransform.IsFixedSize = true;
//spacing

View File

@@ -1,19 +1,22 @@
using Barotrauma.Extensions;
using Barotrauma.IO;
using Barotrauma.Items.Components;
using Barotrauma.Sounds;
using Barotrauma.Steam;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Steamworks;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.IO.Compression;
using System.Linq;
using System.Threading;
using System.Xml;
using System.Xml.Linq;
using Barotrauma.LuaCs.Events;
using Barotrauma.Sounds;
namespace Barotrauma
{
@@ -2044,6 +2047,8 @@ namespace Barotrauma
private bool SaveSubToFile(string name, ContentPackage packageToSaveTo)
{
bool remoteStorageWasEnabled = Submarine.MainSub.Info.SaveToRemoteStorage;
Type subFileType = DetermineSubFileType(MainSub?.Info.Type ?? SubmarineType.Player);
static string getExistingFilePath(ContentPackage package, string fileName)
@@ -2272,6 +2277,16 @@ namespace Barotrauma
linkedSubBox.AddItem(sub.Name, sub);
}
subNameLabel.Text = ToolBox.LimitString(MainSub.Info.Name, subNameLabel.Font, subNameLabel.Rect.Width);
if (remoteStorageWasEnabled)
{
Submarine.MainSub.Info.SaveToRemoteStorage = true;
RemoteStorageHelper.TryWrite(
localPath: MainSub.Info.FilePath,
saveAs: MainSub.Info.FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false),
allowOverwrite: true);
}
}
}
}
@@ -3223,6 +3238,42 @@ namespace Barotrauma
previewImageButtonHolder.RectTransform.MinSize = new Point(0, previewImageButtonHolder.RectTransform.Children.Max(c => c.MinSize.Y));
if (SteamManager.IsInitialized)
{
GUILayoutGroup remoteStorageArea = new(
new RectTransform(new Vector2(1f, 0.05f), rightColumn.RectTransform,
minSize: new Point(0, minHeight)),
isHorizontal: true,
childAnchor: Anchor.CenterLeft)
{
Stretch = true,
AbsoluteSpacing = 5
};
new GUITextBlock(new RectTransform(Vector2.One, remoteStorageArea.RectTransform),
TextManager.Get("RemoteStorageToggle.Title"), textAlignment: Alignment.CenterLeft, wrap: true);
new GUITickBox(new RectTransform(Vector2.One, remoteStorageArea.RectTransform), label: "")
{
OnAddedToGUIUpdateList = component =>
{
// Values may change outside of game.
component.Enabled = SteamRemoteStorage.IsCloudEnabledForAccount;
component.ToolTip = !SteamRemoteStorage.IsCloudEnabledForAccount ? TextManager.Get("RemoteStorageToggle.Disabled") : "";
((GUITickBox)component).SetSelected(SteamRemoteStorage.IsCloudEnabled && MainSub.Info.SaveToRemoteStorage, callOnSelected: false);
},
OnSelected = tickBox =>
{
if (tickBox.Selected && !SteamRemoteStorage.IsCloudEnabledForApp)
{
RemoteStorageHelper.AskToEnable(onAccepted: () => MainSub.Info.SaveToRemoteStorage = true);
return false;
}
return MainSub.Info.SaveToRemoteStorage = tickBox.Selected;
}
};
}
var contentPackageTabber = new GUILayoutGroup(new RectTransform((1.0f, 0.075f), rightColumn.RectTransform), isHorizontal: true);
GUIButton createTabberBtn(string labelTag)
@@ -3746,7 +3797,7 @@ namespace Barotrauma
}
var package = GetLocalPackageThatOwnsSub(subInfo);
if (package != null)
if (package != null || subInfo.IsFromRemoteStorage)
{
deleteBtn.Enabled = true;
}
@@ -3771,13 +3822,29 @@ namespace Barotrauma
searchBox.OnDeselected += (sender, userdata) => { searchTitle.Visible = sender.Text.IsNullOrEmpty(); };
searchBox.OnTextChanged += (textBox, text) => { FilterSubs(subList, text); return true; };
var sortedSubs = GetLoadableSubs()
.OrderBy(s => s.Type)
.ThenBy(s => s.Name)
.ToList();
List<SubmarineInfo> allSubs = [.. SubmarineInfo.SavedSubmarines];
foreach (SteamRemoteStorage.RemoteFile remoteFile in SteamRemoteStorage.Files.Where(file => file.Filename.EndsWith(".sub")))
{
if (!remoteFile.TryRead(out byte[] bytes)) { continue; }
using System.IO.MemoryStream stream = new(bytes);
using GZipStream zipStream = new(stream, CompressionMode.Decompress);
if (XMLExtensions.TryLoadXml(zipStream) is not XDocument doc)
{
DebugConsole.ThrowError($"{RemoteStorageHelper.DebugPrefix} Failed to load submarine \"{remoteFile.Filename}\" from remote storage: file is not a valid XML document.");
continue;
}
SubmarineInfo subInfo = new(remoteFile.Filename, element: doc.Root, tryLoad: false) { IsFromRemoteStorage = true };
allSubs.Add(subInfo);
}
IOrderedEnumerable<SubmarineInfo> sortedSubs = allSubs
.OrderBy(kvp => kvp.Type)
.ThenBy(kvp => kvp.Name)
.ThenBy(kvp => kvp.IsFromRemoteStorage);
SubmarineInfo prevSub = null;
foreach (SubmarineInfo sub in sortedSubs)
{
if (prevSub == null || prevSub.Type != sub.Type)
@@ -3795,35 +3862,44 @@ namespace Barotrauma
prevSub = sub;
}
string pathWithoutUserName = Path.GetFullPath(sub.FilePath);
string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder);
if (pathWithoutUserName.StartsWith(saveFolder))
string displayPath = sub.FilePath;
if (sub.IsFromRemoteStorage)
{
pathWithoutUserName = "..." + pathWithoutUserName[saveFolder.Length..];
displayPath += $" {TextManager.Get("RemoteStorage")}";
}
else
{
pathWithoutUserName = sub.FilePath;
string saveFolder = Path.GetFullPath(SaveUtil.DefaultSaveFolder);
string fullPath = Path.GetFullPath(displayPath);
if (fullPath.StartsWith(saveFolder))
{
displayPath = $"...{fullPath[saveFolder.Length..]}";
}
}
GUITextBlock textBlock = new GUITextBlock(new RectTransform(new Vector2(1.0f, 0.0f), subList.Content.RectTransform) { MinSize = new Point(0, 30) },
ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80))
LocalizedString limitedName = ToolBox.LimitString(sub.Name, GUIStyle.Font, subList.Rect.Width - 80);
GUITextBlock textBlock = new(new RectTransform(Vector2.UnitX, subList.Content.RectTransform) { MinSize = new Point(0, 30) }, limitedName)
{
UserData = sub,
ToolTip = pathWithoutUserName
ToolTip = displayPath
};
if (!(ContentPackageManager.VanillaCorePackage?.Files.Any(f => f.Path == sub.FilePath) ?? false))
if (sub.IsFromRemoteStorage)
{
// remote storage
textBlock.OverrideTextColor(RemoteStorageHelper.SteamColor);
}
else if (ContentPackageManager.VanillaCorePackage == null || ContentPackageManager.VanillaCorePackage.Files.None(f => f.Path == sub.FilePath))
{
if (GetLocalPackageThatOwnsSub(sub) == null &&
ContentPackageManager.AllPackages.FirstOrDefault(p => p.Files.Any(f => f.Path == sub.FilePath)) is ContentPackage subPackage)
{
//workshop mod
// workshop mod
textBlock.OverrideTextColor(Color.MediumPurple);
}
else
{
//local mod
// local mod
textBlock.OverrideTextColor(GUIStyle.TextColorBright);
}
}
@@ -4018,10 +4094,9 @@ namespace Barotrauma
return false;
}
if (!(subList.SelectedComponent?.UserData is SubmarineInfo selectedSubInfo)) { return false; }
if (subList.SelectedComponent?.UserData is not SubmarineInfo selectedSubInfo) { return false; }
var ownerPackage = GetLocalPackageThatOwnsSub(selectedSubInfo);
if (ownerPackage is null)
if (!selectedSubInfo.IsFromRemoteStorage && GetLocalPackageThatOwnsSub(selectedSubInfo) is null)
{
if (IsVanillaSub(selectedSubInfo))
{
@@ -4183,21 +4258,23 @@ namespace Barotrauma
{
if (sub == null) { return; }
//If the sub is included in a content package that only defines that one sub,
//check that it's a local content package and only allow deletion if it is.
//(deleting from the Submarines folder is also currently allowed, but this is temporary)
var subPackage = GetLocalPackageThatOwnsSub(sub);
if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { return; }
var msgBox = new GUIMessageBox(
TextManager.Get("DeleteDialogLabel"),
TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name),
new LocalizedString[] { TextManager.Get("Yes"), TextManager.Get("Cancel") });
// If the sub is included in a content package that only defines that one sub,
// check that it's a local content package and only allow deletion if it is.
// (deleting from the Submarines folder is also currently allowed, but this is temporary)
ContentPackage subPackage = GetLocalPackageThatOwnsSub(sub);
if (!ContentPackageManager.LocalPackages.Regular.Contains(subPackage)) { subPackage = null; }
if (!sub.IsFromRemoteStorage && subPackage == null) { return; }
GUIMessageBox msgBox = new(TextManager.Get("DeleteDialogLabel"), TextManager.GetWithVariable("DeleteDialogQuestion", "[file]", sub.Name), [TextManager.Get("Yes"), TextManager.Get("Cancel")]);
msgBox.Buttons[0].OnClicked += (btn, userData) =>
{
try
if (sub.IsFromRemoteStorage)
{
if (subPackage != null)
RemoteStorageHelper.TryDelete(sub.FilePath);
}
else if (subPackage != null)
{
try
{
File.Delete(sub.FilePath, catchUnauthorizedAccessExceptions: false);
ModProject modProject = new ModProject(subPackage);
@@ -4208,17 +4285,17 @@ namespace Barotrauma
{
MainSub.Info.FilePath = null;
}
}
sub.Dispose();
CreateLoadScreen();
}
catch (Exception e)
{
DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e);
}
}
catch (Exception e)
{
DebugConsole.ThrowErrorLocalized(TextManager.GetWithVariable("DeleteFileError", "[file]", sub.FilePath), e);
}
return true;
sub.Dispose();
CreateLoadScreen();
return msgBox.Close(btn, userData);
};
msgBox.Buttons[0].OnClicked += msgBox.Close;
msgBox.Buttons[1].OnClicked += msgBox.Close;
}

View File

@@ -445,7 +445,7 @@ namespace Barotrauma
{
DebugConsole.NewMessage("Missing Localization for property: " + propertyTag);
MissingLocalizations.Add($"sp.{propertyTag}.name|{displayName}");
MissingLocalizations.Add($"sp.{propertyTag}.description|{property.GetAttribute<Serialize>().Description}");
MissingLocalizations.Add($"sp.{propertyTag}.description|{property.GetAttribute<Serialize>()?.Description}");
}
}
#endif
@@ -467,7 +467,7 @@ namespace Barotrauma
}
if (toolTip.IsNullOrEmpty())
{
toolTip = property.GetAttribute<Serialize>().Description;
toolTip = property.GetAttribute<Serialize>()?.Description;
}
GUIComponent propertyField = null;

View File

@@ -0,0 +1,34 @@
#nullable enable
using Steamworks;
using System;
namespace Barotrauma.Steam;
internal static partial class RemoteStorageHelper
{
/// <summary>
/// Asks the user if they wish to enable remote storage. Accepting enables it automatically.
/// </summary>
/// <param name="onAccepted">Invoked when the user accepts enabling remote storage.</param>
/// <param name="onRejected">Invoked when the user rejects enabling remote storage.</param>
/// <remarks>Closes automatically if remote storage was enabled outside of the game, or if remote storage can not be enabled.</remarks>
public static void AskToEnable(Action? onAccepted = null, Action? onRejected = null)
{
GUIMessageBox confirmBox = new GUIMessageBox(
TextManager.Get("RemoteStorageEnablePopup.Header"),
TextManager.Get("RemoteStorageEnablePopup.Text"),
[TextManager.Get("Yes"), TextManager.Get("No")],
autoCloseCondition: () => !SteamRemoteStorage.IsCloudEnabledForAccount || SteamRemoteStorage.IsCloudEnabledForApp);
confirmBox.Buttons[0].OnClicked += (btn, data) =>
{
SteamRemoteStorage.IsCloudEnabledForApp = true;
onAccepted?.Invoke();
return confirmBox.Close(btn, data);
};
confirmBox.Buttons[1].OnClicked += (btn, data) =>
{
onRejected?.Invoke();
return confirmBox.Close(btn, data);
};
}
}

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.12.7.0</Version>
<Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.12.7.0</Version>
<Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma</Product>
<Version>1.12.7.0</Version>
<Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2024</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>Barotrauma</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.12.7.0</Version>
<Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.12.7.0</Version>
<Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -1799,22 +1799,35 @@ namespace Barotrauma
(Client client, Vector2 cursorWorldPos, string[] args) =>
{
if (Submarine.MainSub == null || Level.Loaded == null) { return; }
Submarine submarineToTeleport = Submarine.MainSub;
if (args.Length > 1)
{
foreach (Submarine sub in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
if ((sub.Info.Name + "_" + sub.TeamID) == args[1])
{
submarineToTeleport = sub;
break;
}
}
}
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(cursorWorldPos);
submarineToTeleport.SetPosition(cursorWorldPos);
}
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
submarineToTeleport.SetPosition(Level.Loaded.StartPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
}
else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
submarineToTeleport.SetPosition(Level.Loaded.EndPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
}
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
{
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport);
if (Level.Loaded?.EndOutpost == null)
{
NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);

View File

@@ -1179,6 +1179,7 @@ namespace Barotrauma
NetWalletTransfer transfer = INetSerializableStruct.Read<NetWalletTransfer>(msg);
if (GameMain.Server is null) { return; }
if (transfer.Amount <= 0) { return; }
if (transfer.Sender.TryUnwrap(out var id))
{

View File

@@ -193,13 +193,6 @@ namespace Barotrauma
(item.PreviousParentInventory == null ||
!sender.Character.CanAccessInventory(item.PreviousParentInventory));
// Prevent modified clients from being able to steal items from characters by item swapping with an existing item
// due to drag and drop being enabled
if (!sender.Character.CanAccessInventory(this, CharacterInventory.AccessLevel.AllowBotsAndPets) && GetItemAt(slotIndex) != null)
{
itemAccessDenied = true;
}
//more restricted "adding" of handcuffs: we can't allow putting handcuffs on a player just because dragging and dropping is allowed
if (item.HasTag(Tags.HandLockerItem) && !itemAccessDenied)
{
@@ -219,7 +212,7 @@ namespace Barotrauma
continue;
}
}
TryPutItem(item, slotIndex, true, true, sender.Character, false);
TryPutItem(item, slotIndex, allowSwapping: false, allowCombine: false, user: sender.Character, createNetworkEvent: false);
for (int j = 0; j < capacity; j++)
{
if (slots[j].Contains(item) && !receivedItemIdsFromClient[j].Contains(item.ID))

View File

@@ -8,6 +8,17 @@ namespace Barotrauma.Networking
{
partial class ChatMessage
{
private static string SanitizeText(Client client, string text)
{
if (!client.HasPermission(ClientPermissions.SpamImmunity))
{
// Prevent clients without spam immunity from being able to use RichString features
text = text.Replace('‖', ' ');
}
return text;
}
public static void ServerRead(IReadMessage msg, Client c)
{
c.KickAFKTimer = 0.0f;
@@ -69,8 +80,7 @@ namespace Barotrauma.Networking
txt = msg.ReadString() ?? "";
}
// Sanitize incoming text message from client so they can't use RichString features
txt = txt.Replace('‖', ' ');
txt = SanitizeText(c, txt);
if (!NetIdUtils.IdMoreRecent(ID, c.LastSentChatMsgID)) { return; }

View File

@@ -904,7 +904,10 @@ namespace Barotrauma.Networking
string subHash = inc.ReadString();
CampaignSettings settings = INetSerializableStruct.Read<CampaignSettings>(inc);
var matchingSub = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash);
var matchingSub =
ServerSettings.AllowSubVoting ?
Voting.HighestVoted<SubmarineInfo>(VoteType.Sub, connectedClients) :
SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == subName && s.MD5Hash.StringRepresentation == subHash);
if (GameStarted)
{

View File

@@ -249,7 +249,7 @@ namespace Barotrauma.Networking
// in which case we'll wait until the timeout runs out before kicking the client
List<Client> toKick = inGameClients.FindAll(c =>
NetIdUtils.IdMoreRecent((UInt16)(lastSentToAll + 1), c.LastRecvEntityEventID) &&
(firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0));
(!c.NeedsMidRoundSync || firstEventToResend.CreateTime > c.MidRoundSyncTimeOut || lastSentToAnyoneTime > c.MidRoundSyncTimeOut || Timing.TotalTime > c.MidRoundSyncTimeOut + 10.0));
toKick.ForEach(c =>
{
DebugConsole.NewMessage(c.Name + " was kicked because they were expecting a very old network event (" + (c.LastRecvEntityEventID + 1).ToString() + ")", Color.Red);

View File

@@ -54,13 +54,14 @@ namespace Barotrauma.Networking
{
if (recipient == sender) { continue; }
if (!CanReceive(sender, recipient, out float distanceFactor)) { continue; }
if (!CanReceive(sender, recipient, out float distanceFactor, out bool isRadio)) { continue; }
IWriteMessage msg = new WriteOnlyMessage();
msg.WriteByte((byte)ServerPacketHeader.VOICE);
msg.WriteByte((byte)queue.QueueID);
msg.WriteRangedSingle(distanceFactor, 0.0f, 1.0f, 8);
msg.WriteBoolean(isRadio);
queue.Write(msg);
netServer.Send(msg, recipient.Connection, DeliveryMethod.Unreliable);
@@ -68,15 +69,17 @@ namespace Barotrauma.Networking
}
}
private static bool CanReceive(Client sender, Client recipient, out float distanceFactor)
private static bool CanReceive(Client sender, Client recipient, out float distanceFactor, out bool isRadio)
{
if (Screen.Selected != GameMain.GameScreen)
{
distanceFactor = 0.0f;
return true;
isRadio = false;
return true;
}
distanceFactor = 0.0f;
isRadio = false;
//no-one can hear muted players
if (sender.Muted) { return false; }
@@ -109,12 +112,14 @@ namespace Barotrauma.Networking
if (recipientSpectating)
{
isRadio = true;
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))
{
isRadio = true;
distanceFactor = MathHelper.Clamp(Vector2.Distance(sender.Character.WorldPosition, recipient.Character.WorldPosition) / senderRadio.Range, 0.0f, 1.0f);
return true;
}

View File

@@ -6,7 +6,7 @@
<RootNamespace>Barotrauma</RootNamespace>
<Authors>FakeFish, Undertow Games</Authors>
<Product>Barotrauma Dedicated Server</Product>
<Version>1.12.7.0</Version>
<Version>1.13.3.1</Version>
<Copyright>Copyright © FakeFish 2018-2023</Copyright>
<Platforms>AnyCPU;x64</Platforms>
<AssemblyName>DedicatedServer</AssemblyName>

View File

@@ -23,6 +23,70 @@
</Holdable>
</Item>
<Item name="fliptestweapon" identifier="fliptestweapon" category="Weapon" cargocontaineridentifier="metalcrate" tags="mediumitem,weapon,gun,gunsmith,provocativetohumanai,mountableweapon" Scale="0.5" impactsoundtag="impact_metal_light">
<PreferredContainer primary="secarmcab" amount="1" spawnprobability="0.2" notcampaign="true" notpvp="true" />
<PreferredContainer secondary="wrecksecarmcab,abandonedsecarmcab,piratesecarmcab" spawnprobability="0.1" />
<PreferredContainer secondary="armcab" />
<Price baseprice="1000" sold="false" minleveldifficulty="30">
<Price storeidentifier="merchantoutpost" multiplier="1.5" />
<Price storeidentifier="merchantcity" multiplier="1.25" />
<Price storeidentifier="merchantresearch" multiplier="1.25" />
<Price storeidentifier="merchantmilitary" sold="true" multiplier="0.9" minavailable="1" />
<Price storeidentifier="merchantmine" multiplier="1.25" />
<Price storeidentifier="merchantarmory" sold="true" multiplier="0.9" minavailable="1" />
</Price>
<Deconstruct time="10">
<Item identifier="steel" />
<Item identifier="titaniumaluminiumalloy" />
</Deconstruct>
<InventoryIcon texture="Content/Items/InventoryIconAtlas.png" sourcerect="896,831,64,64" origin="0.5,0.5" />
<Sprite texture="Content/Items/Weapons/weapons_new.png" sourcerect="0,244,186,65" depth="0.55" origin="0.5,0.5" />
<Body width="170" height="40" density="25" />
<Holdable slots="RightHand+LeftHand" controlpose="true" holdpos="40,-20" aimpos="45,-10" handle1="-33,-15" handle2="26,5" holdangle="-25">
<StatusEffect type="OnActive" target="This" offset="80,20" OffsetCopiesEntityTransform="true">
<ParticleEmitter particle="electricshock" particlespersecond="60" copyentityangle="true" velocitymin="0" velocitymax="100" distancemin="0" distancemax="10" scalemin="0.03" scalemax="0.04" />
</StatusEffect>
</Holdable>
<Wearable slots="Bag" msg="ItemMsgEquipSelect" canbeselected="false" canbepicked="true" pickkey="Select">
<sprite name="Grenade Launcher Worn" texture="Content/Items/Weapons/weapons_new.png" canbehiddenbyotherwearables="false" sourcerect="0,244,186,65" rotation="90" depth="0.6" limb="Torso" depthlimb="LeftArm" scale="0.5" origin="0.5,0.8" />
</Wearable>
<RangedWeapon barrelpos="80,11" reload="0.8" spread="1" unskilledspread="10" combatPriority="75" drawhudwhenequipped="true" crosshairscale="0.2">
<Crosshair texture="Content/Items/Weapons/Crosshairs.png" sourcerect="0,256,256,256" />
<CrosshairPointer texture="Content/Items/Weapons/Crosshairs.png" sourcerect="256,256,256,256" />
<ParticleEmitter particle="muzzleflashchaingun" particleamount="1" velocitymin="0" velocitymax="0" scalemin="0.5" scalemax="0.6" />
<ParticleEmitter particle="explosionsmoke" particleamount="1" velocitymin="0" velocitymax="0" scalemin="0.5" scalemax="0.6" />
<Sound file="Content/Items/Weapons/GrenadeLauncherShot1.ogg" type="OnUse" range="1000" />
<Sound file="Content/Items/Weapons/GrenadeLauncherShot2.ogg" type="OnUse" range="1000" />
<Sound file="Content/Items/Weapons/GrenadeLauncherShot3.ogg" type="OnUse" range="1000" />
<StatusEffect type="OnUse" target="This">
<Explosion range="150.0" force="2" shockwave="false" smoke="false" flames="false" flash="true" sparks="false" underwaterbubble="false" applyfireeffects="false" camerashake="6.0" />
</StatusEffect>
<RequiredItems items="grenade" type="Contained" msg="ItemMsgAmmoRequired" />
<RequiredSkill identifier="weapons" level="60" />
</RangedWeapon>
<!--Holds six grenades at a time-->
<ItemContainer capacity="6" maxstacksize="1" hideitems="false" ShowTotalStackCapacityInContainedStateIndicator="true" containedstateindicatorstyle="bullet" containedspritedepth="0.56">
<Containable items="grenade" hide="true" />
<SlotIcon slotindex="0" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="1" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="2" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="3" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="4" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="5" texture="Content/UI/StatusMonitorUI.png" sourcerect="448,448,64,64" origin="0.5,0.5" />
<SlotIcon slotindex="6" texture="Content/UI/StatusMonitorUI.png" sourcerect="320,448,64,64" origin="0.5,0.5" />
<SubContainer capacity="1" maxstacksize="1">
<Containable items="flashlight" hide="false" itempos="22,-1" setactive="true" />
</SubContainer>
</ItemContainer>
<aitarget sightrange="500" soundrange="500" fadeouttime="3" />
<Quality>
<QualityStat stattype="ExplosionRadius" value="0.1" />
<QualityStat stattype="ExplosionDamage" value="0.1" />
</Quality>
<Upgrade gameversion="0.10.0.0" scale="0.5" />
<SkillRequirementHint identifier="weapons" level="60" />
</Item>
<Item name="fliptestlight" identifier="fliptestlighttower" width="176" height="352" texturescale="1.0,1.0" scale="0.5" category="Decorative" subcategory="mining" noninteractable="true">
<sprite texture="Content/Map/Outposts/Art/TunnelWalls.png" sourcerect="849,1697,176,352" depth="0.97" premultiplyalpha="false" origin="0.5,0.5" />
<LightComponent range="160.0" lightcolor="255,234,181,200" IsOn="true" castshadows="false" LightOffset="200,147" allowingameediting="false">

View File

@@ -524,8 +524,7 @@ namespace Barotrauma
{
UnlockAchievement(causeOfDeath.Killer, "killpoison".ToIdentifier());
}
else if (item.Prefab.Identifier == "nuclearshell" ||
item.Prefab.Identifier == "nucleardepthcharge")
else if (item.Prefab.Tags.Contains("nuclearexplosive"))
{
UnlockAchievement(causeOfDeath.Killer, "killnuke".ToIdentifier());
}

View File

@@ -197,6 +197,18 @@ namespace Barotrauma
private readonly List<Body> myBodies;
#if SERVER
/// <summary>
/// How often the server can send messages about a limb targeting some attack target.
/// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame)
/// </summary>
private const double MinSetAttackTargetEventInterval = 0.5;
private IDamageable lastDamageTarget;
private Limb lastTargetLimb;
private Limb lastAttackLimb;
private double lastSetAttackTargetEventTime;
#endif
public LatchOntoAI LatchOntoAI { get; private set; }
public SwarmBehavior SwarmBehavior { get; private set; }
public PetBehavior PetBehavior { get; private set; }
@@ -2679,11 +2691,19 @@ namespace Barotrauma
if (!ActiveAttack.IsRunning)
{
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
if (Timing.TotalTime > lastSetAttackTargetEventTime + MinSetAttackTargetEventInterval ||
damageTarget != lastDamageTarget || AttackLimb != lastAttackLimb || targetLimb != lastTargetLimb)
{
GameMain.NetworkMember.CreateEntityEvent(Character, new Character.SetAttackTargetEventData(
AttackLimb,
damageTarget,
targetLimb,
SimPosition));
lastSetAttackTargetEventTime = Timing.TotalTime;
lastDamageTarget = damageTarget;
lastAttackLimb = AttackLimb;
lastTargetLimb = targetLimb;
}
#else
Character.PlaySound(CharacterSound.SoundType.Attack, maxInterval: 3);
#endif

View File

@@ -46,7 +46,7 @@ namespace Barotrauma
private float respondToAttackTimer;
private const float RespondToAttackInterval = 1.0f;
private bool wasConscious;
private bool wasDead;
private bool freezeAI;
@@ -201,7 +201,7 @@ namespace Barotrauma
}
if (isIncapacitated) { return; }
wasConscious = true;
wasDead = false;
respondToAttackTimer -= deltaTime;
if (respondToAttackTimer <= 0.0f)
@@ -1256,14 +1256,15 @@ namespace Barotrauma
public override void OnAttacked(Character attacker, AttackResult attackResult)
{
// The attack incapacitated/killed the character: respond immediately to trigger nearby characters because the update loop no longer runs
if (wasConscious && (Character.IsIncapacitated || Character.Stun > 0.0f))
// If the character is incapacitated or dead, respond to the attack anyway to let other nearby characters react to it
// (But if the character is already dead, and was dead before this attack, don't react)
if (Character.IsDead && wasDead) { return; }
if (Character.IsIncapacitated || Character.Stun > 0.0f)
{
RespondToAttack(attacker, attackResult);
wasConscious = false;
wasDead = Character.IsDead;
return;
}
if (Character.IsDead) { return; }
if (attacker == null || Character.IsPlayer)
{
// The player characters need to "respond" to the attack always, because the update loop doesn't run for them.
@@ -1467,10 +1468,10 @@ namespace Barotrauma
otherHumanAI.VisibleHulls.Contains(attacker.CurrentHull) ||
otherCharacter.CanSeeTarget(attacker, seeThroughWindows: true);
if (!isWitnessing)
{
if (Character.IsDead || Character.IsUnconscious || otherCharacter.TeamID != Character.TeamID)
{
if (Character.IsKnockedDown || otherCharacter.TeamID != Character.TeamID)
{
// Dead or in different team -> cannot report.
// Knocked down or in different team -> cannot report.
continue;
}
if (otherHumanAI.objectiveManager.HasOrders())
@@ -1494,6 +1495,14 @@ namespace Barotrauma
continue;
}
}
else if (!otherCharacter.IsSecurity)
{
//witnessed the attack as non-security, trigger security
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID))
{
TriggerSecurity(security.AIController as HumanAIController, attacker, DetermineCombatMode(security, cumulativeDamage, isWitnessing));
}
}
float delay = isWitnessing ? GetReactionTime() : Rand.Range(2.0f, 3.0f, Rand.RandSync.Unsynced);
otherHumanAI.AddCombatObjective(combatMode, attacker, delay);
}
@@ -1926,12 +1935,12 @@ namespace Barotrauma
character.IsCriminal = true;
character.IsActingOffensively = true;
}
if (!TriggerSecurity(otherHumanAI, combatMode))
if (!TriggerSecurity(otherHumanAI, character, combatMode))
{
// Else call the others
foreach (Character security in Character.CharacterList.Where(c => c.TeamID == otherCharacter.TeamID).OrderBy(c => Vector2.DistanceSquared(character.WorldPosition, c.WorldPosition)))
{
if (!TriggerSecurity(security.AIController as HumanAIController, combatMode))
if (!TriggerSecurity(security.AIController as HumanAIController, character, combatMode))
{
// Only alert one guard at a time
return;
@@ -1941,25 +1950,25 @@ namespace Barotrauma
}
}
bool TriggerSecurity(HumanAIController humanAI, AIObjectiveCombat.CombatMode combatMode)
{
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(combatMode, character, delay: GetReactionTime(),
onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
}
private static bool TriggerSecurity(HumanAIController humanAI, Character targetCharacter, AIObjectiveCombat.CombatMode combatMode)
{
if (humanAI == null) { return false; }
if (!humanAI.Character.IsSecurity) { return false; }
if (humanAI.ObjectiveManager.IsCurrentObjective<AIObjectiveCombat>()) { return false; }
humanAI.AddCombatObjective(combatMode, targetCharacter, delay: GetReactionTime(),
onCompleted: () =>
{
//if the target is arrested successfully, reset the damage accumulator
foreach (Character anyCharacter in Character.CharacterList)
{
if (anyCharacter.AIController is HumanAIController anyAI)
{
if (anyCharacter.AIController is HumanAIController anyAI)
{
anyAI.structureDamageAccumulator?.Remove(character);
}
anyAI.structureDamageAccumulator?.Remove(targetCharacter);
}
});
return true;
}
}
});
return true;
}
public static void ItemTaken(Item item, Character thief)

View File

@@ -133,7 +133,7 @@ namespace Barotrauma
bool operateExtinguisher = !moveCloser || (dist < extinguisher.Range * 1.2f && character.CanSeeTarget(targetHull));
if (operateExtinguisher)
{
character.CursorPosition = fs.Position;
character.CursorPosition = FarseerPhysics.ConvertUnits.ToDisplayUnits(Submarine.GetRelativeSimPositionFromWorldPosition(fs.WorldPosition, character.Submarine, fs.Submarine));
Vector2 fromCharacterToFireSource = fs.WorldPosition - character.WorldPosition;
character.CursorPosition += VectorExtensions.Forward(extinguisherItem.body.TransformedRotation + (float)Math.Sin(sinTime) / 2, fromCharacterToFireSource.Length() / 2);
if (extinguisherItem.RequireAimToUse)

View File

@@ -173,6 +173,16 @@ namespace Barotrauma
if (character.CanInteractWith(Item, out _, checkLinked: false))
{
waitTimer += deltaTime;
//if we're climbing upwards to the item, ensure the character stays within arm's length of it
//without this, the character can get stuck in a loop where the GoTo objective takes them close enough to the item,
//then the character shifts a bit downwards on the ladder and goes outside interaction range, and the GoTo objective kicks in again
if (character.IsClimbing &&
Item.WorldPosition.Y > character.WorldPosition.Y + FarseerPhysics.ConvertUnits.ToDisplayUnits(character.AnimController.ArmLength))
{
character.AIController.SteeringManager.SteeringManual(deltaTime, Vector2.UnitY);
}
if (waitTimer < WaitTimeBeforeRepair) { return; }
HumanAIController.FaceTarget(Item);

View File

@@ -758,6 +758,11 @@ namespace Barotrauma
public float CoolDownTimer { get; set; }
public float CurrentRandomCoolDown { get; private set; }
public float SecondaryCoolDownTimer { get; set; }
/// <summary>
/// The attack is considered to be running from the moment it starts until the <see cref="AttackTimer"/> reaches the <see cref="Duration"/> of the attack, or until the attack lands successfully.
/// E.g. from the moment the monster decides to lunge itself towards the target until it hits a target or until it completes that lunge.
/// </summary>
public bool IsRunning { get; private set; }
public float AfterAttackTimer { get; set; }

View File

@@ -997,6 +997,9 @@ namespace Barotrauma
public bool IsForceRagdolled;
public bool FollowCursor = true;
/// <summary>
/// Is the character currently dead, unconscious or paralyzed?
/// </summary>
public bool IsIncapacitated
{
get
@@ -1006,6 +1009,9 @@ namespace Barotrauma
}
}
/// <summary>
/// Is the character dead or below 0 vitality and not able to stay conscious?
/// </summary>
public bool IsUnconscious
{
get { return CharacterHealth.IsUnconscious; }
@@ -1673,7 +1679,8 @@ namespace Barotrauma
AnimController.FindHull(setInWater: true);
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass <= 30.0f);
//mass < 35 = husk chimera is the largest vanilla monster that can be contained by default
IsContainable = prefab.ConfigElement.GetAttributeBool(nameof(IsContainable), def: Mass < 35.0f);
CharacterList.Add(this);
@@ -2262,7 +2269,10 @@ namespace Barotrauma
{
Vector2 targetMovement = GetTargetMovement();
AnimController.TargetMovement = targetMovement;
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
if (SelectedItem?.GetComponent<Controller>() is not { ControlCharacterPose: true })
{
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
}
}
if (AnimController is HumanoidAnimController humanAnimController)
@@ -3520,9 +3530,11 @@ namespace Barotrauma
UpdateAttackers(deltaTime);
foreach (var characterTalent in characterTalents)
//use a for loop instead of foreach because talents can unlock other talents via StatusEffectAction (see #17328)
//this way we'll just add them to the end of the list without causing a collection was modified exception
for (int i = 0; i < characterTalents.Count; i++)
{
characterTalent.UpdateTalent(deltaTime);
characterTalents[i].UpdateTalent(deltaTime);
}
if (IsDead) { return; }
@@ -5813,6 +5825,12 @@ namespace Barotrauma
return info.UnlockedTalents.Contains(identifier);
}
public bool IsTalentLocked(Identifier talentIdentifier)
{
if (info == null) { return true; }
return Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1;
}
public bool HasUnlockedAllTalents()
{
if (TalentTree.JobTalentTrees.TryGet(Info.Job.Prefab.Identifier, out TalentTree talentTree))

View File

@@ -1022,6 +1022,15 @@ namespace Barotrauma
partial void UpdateProjSpecific(float deltaTime);
#if SERVER
/// <summary>
/// How often the server can send messages about attacks being executed. Note that the timer is per-limb: if one limb executes an attack immediately after another, an network event can still be created.
/// Mainly relevant for attacks with no cooldown, e.g. fractal guardian's steam cannons which run continuously over time (we can't send events every frame)
/// </summary>
private const double MinExecuteAttackEventInterval = 0.5f;
private double lastExecuteAttackEventTime;
#endif
private readonly List<Body> contactBodies = new List<Body>();
/// <summary>
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
@@ -1142,9 +1151,13 @@ namespace Barotrauma
ExecuteAttack(damageTarget, targetLimb, out attackResult);
}
#if SERVER
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
targetSimPos: attackSimPos));
if (Timing.TotalTime > lastExecuteAttackEventTime + MinExecuteAttackEventInterval)
{
GameMain.NetworkMember.CreateEntityEvent(character, new Character.ExecuteAttackEventData(
attackLimb: this, targetEntity: damageTarget, targetLimb: targetLimb,
targetSimPos: attackSimPos));
lastExecuteAttackEventTime = Timing.TotalTime;
}
#endif
}

View File

@@ -43,14 +43,14 @@ namespace Barotrauma.Abilities
{
foreach (Identifier identifier in option.TalentIdentifiers)
{
if (IsShowCaseTalent(identifier, option) || TalentTree.IsTalentLocked(identifier, characters)) { continue; }
if (IsShowCaseTalent(identifier, option) || Character.IsTalentLocked(identifier)) { continue; }
identifiers.Add(identifier);
}
foreach (var (_, value) in option.ShowCaseTalents)
{
var ids = value.Where(i => !TalentTree.IsTalentLocked(i, characters)).ToImmutableHashSet();
var ids = value.Where(i => !Character.IsTalentLocked(i)).ToImmutableHashSet();
if (ids.Count is 0) { continue; }
identifiers.Add(value.GetRandomUnsynced());

View File

@@ -131,7 +131,7 @@ namespace Barotrauma
if (character.Info.GetTotalTalentPoints() - selectedTalents.Count <= 0) { return false; }
if (!JobTalentTrees.TryGet(character.Info.Job.Prefab.Identifier, out TalentTree talentTree)) { return false; }
if (IsTalentLocked(talentIdentifier, Character.GetFriendlyCrew(character))) { return false; }
if (character.IsTalentLocked(talentIdentifier)) { return false; }
if (character.Info.GetUnlockedTalentsInTree().Contains(talentIdentifier))
{
@@ -163,16 +163,6 @@ namespace Barotrauma
return false;
}
public static bool IsTalentLocked(Identifier talentIdentifier, IEnumerable<Character> characterList)
{
foreach (Character c in characterList)
{
if (c.Info.GetSavedStatValue(StatTypes.LockedTalents, talentIdentifier) >= 1) { return true; }
}
return false;
}
public static List<Identifier> CheckTalentSelection(Character controlledCharacter, IEnumerable<Identifier> selectedTalents)
{
List<Identifier> viableTalents = new List<Identifier>();

View File

@@ -252,7 +252,7 @@ namespace Barotrauma
GameMain.NetworkMember.ShowNetStats = !GameMain.NetworkMember.ShowNetStats;
}));
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
commands.Add(new Command("spawn|spawncharacter", "spawn [creaturename/jobname] [near/inside/outside/cursor] [team] [add to crew (true/false)] [name]: Spawn a creature at a random spawnpoint (use the second parameter to only select spawnpoints near/inside/outside the submarine). You can also enter the name of a job (e.g. \"Mechanic\") to spawn a character with a specific job and the appropriate equipment.", null,
() =>
{
string[] creatureAndJobNames =
@@ -271,7 +271,7 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition]: Spawn an item in the inventory of the controlled character",
commands.Add(new Command("give|giveitem", "give|giveitem [itemname/itemidentifier] [amount] [condition] [quality]: Spawn an item in the inventory of the controlled character",
(string[] args) =>
{
if (Character.Controlled == null)
@@ -292,9 +292,12 @@ namespace Barotrauma
},
getValidArgs: () =>
{
return new string[][]
return new string[][]
{
GetItemNameOrIdParams().ToArray()
GetItemNameOrIdParams().ToArray(),
new string[] { "1" },
new string[] { "100" },
ItemQualityNames.ToArray()
};
}, isCheat: true));
@@ -311,7 +314,7 @@ namespace Barotrauma
};
}, isCheat: true));
commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".",
commands.Add(new Command("spawnitem", "spawnitem [itemname/itemidentifier] [cursor/inventory/cargo/random/[name]] [amount] [condition] [quality]: Spawn an item at the position of the cursor, in the inventory of the controlled character, in the inventory of the client with the given name, or at a random spawnpoint if the location parameter is omitted or \"random\".",
(string[] args) =>
{
TrySpawnItem(args);
@@ -321,7 +324,10 @@ namespace Barotrauma
return new string[][]
{
GetItemNameOrIdParams().ToArray(),
GetSpawnPosParams().ToArray()
GetSpawnPosParams().ToArray(),
new string[] { "1" },
new string[] { "100" },
ItemQualityNames.ToArray()
};
}, isCheat: true));
@@ -1324,6 +1330,7 @@ namespace Barotrauma
}
else
{
if (GameMain.GameSession?.Map is Map map) { NewMessage("Map seed: " + map.Seed); }
NewMessage("Level seed: " + Level.Loaded.Seed);
NewMessage("Level generation params: " + Level.Loaded.GenerationParams.Identifier);
NewMessage("Adjacent locations: " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()) + ", " + (Level.Loaded.StartLocation?.Type.Identifier ?? "none".ToIdentifier()));
@@ -1333,17 +1340,29 @@ namespace Barotrauma
}
},null));
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.",
commands.Add(new Command("teleportsub", "teleportsub [start/end/endoutpost/cursor] [submarine_team]: Teleport the submarine to the position of the cursor, or the start or end of the level. The 'endoutpost' argument also automatically docks the sub with the outpost at the end of the level. WARNING: does not take outposts into account, so often leads to physics glitches. Only use for debugging.",
onExecute:(string[] args) =>
{
if (Submarine.MainSub == null) { return; }
Submarine submarineToTeleport = Submarine.MainSub;
if (args.Length > 1)
{
foreach (Submarine sub in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
if ((sub.Info.Name + "_" + sub.TeamID) == args[1])
{
submarineToTeleport = sub;
break;
}
}
}
if (args.Length == 0 || args[0].Equals("cursor", StringComparison.OrdinalIgnoreCase))
{
#if SERVER
ThrowError("Cannot teleport the sub to the position of the cursor. Use \"start\" or \"end\", or execute the command as a client.");
#else
Submarine.MainSub.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
submarineToTeleport.SetPosition(Screen.Selected.Cam.ScreenToWorld(PlayerInput.MousePosition));
#endif
}
else if (args[0].Equals("start", StringComparison.OrdinalIgnoreCase))
@@ -1356,9 +1375,9 @@ namespace Barotrauma
Vector2 pos = Level.Loaded.StartPosition;
if (Level.Loaded.StartOutpost != null)
{
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.StartOutpost.Borders.Height) / 2;
}
Submarine.MainSub.SetPosition(pos);
submarineToTeleport.SetPosition(pos);
}
else if (args[0].Equals("end", StringComparison.OrdinalIgnoreCase))
{
@@ -1370,9 +1389,9 @@ namespace Barotrauma
Vector2 pos = Level.Loaded.EndPosition;
if (Level.Loaded.EndOutpost != null)
{
pos -= Vector2.UnitY * (Submarine.MainSub.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
pos -= Vector2.UnitY * (submarineToTeleport.Borders.Height + Level.Loaded.EndOutpost.Borders.Height) / 2;
}
Submarine.MainSub.SetPosition(pos);
submarineToTeleport.SetPosition(pos);
}
else if (args[0].Equals("endoutpost", StringComparison.OrdinalIgnoreCase))
{
@@ -1381,8 +1400,8 @@ namespace Barotrauma
NewMessage("Can't teleport the sub to the end outpost (no outpost at the end of the level).", Color.Red);
return;
}
Submarine.MainSub.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * Submarine.MainSub.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Submarine.MainSub);
submarineToTeleport.SetPosition(Level.Loaded.EndExitPosition - Vector2.UnitY * submarineToTeleport.Borders.Height);
var submarineDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == submarineToTeleport);
var outpostDockingPort = DockingPort.List.FirstOrDefault(d => d.Item.Submarine == Level.Loaded.EndOutpost);
if (submarineDockingPort != null && outpostDockingPort != null)
{
@@ -1394,7 +1413,8 @@ namespace Barotrauma
{
return new string[][]
{
new string[] { "start", "end", "endoutpost", "cursor" }
new string[] { "start", "end", "endoutpost", "cursor" },
ListAvailableSubmarines()
};
}, isCheat: true));
@@ -2569,7 +2589,17 @@ namespace Barotrauma
return locationNames.ToArray();
}
private static string[] ListAvailableSubmarines()
{
List<string> submarineNames = new();
foreach (var submarine in Submarine.Loaded.Where(s => s.PhysicsBody.BodyType == FarseerPhysics.BodyType.Dynamic))
{
submarineNames.Add(submarine.Info.Name + "_" + submarine.TeamID);
}
return submarineNames.ToArray();
}
private static bool TryFindTeleportPosition(string locationName, out Vector2 teleportPosition)
{
if (Submarine.MainSub is Submarine mainSub && string.Equals(locationName, "mainsub", StringComparison.InvariantCultureIgnoreCase))
@@ -2952,7 +2982,7 @@ namespace Barotrauma
isHuman = job != null || characterLowerCase == CharacterPrefab.HumanSpeciesName;
}
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew);
ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter);
if (usePreConfiguredNPC)
{
@@ -2983,6 +3013,14 @@ namespace Barotrauma
CharacterInfo characterInfo = new CharacterInfo(CharacterPrefab.HumanSpeciesName, jobOrJobPrefab: job, variant: variant);
Entity.Spawner.AddCharacterToSpawnQueue(CharacterPrefab.HumanSpeciesName, spawnPosition, characterInfo, onSpawn: newCharacter =>
{
if (renameCharacter != null)
{
if (renameCharacter.Length > 31)
{
renameCharacter = renameCharacter.Substring(0, 32);
}
newCharacter.Info.Name = renameCharacter;
}
SetTeamAndCrew(newCharacter);
newCharacter.GiveJobItems(isPvPMode: GameMain.GameSession?.GameMode is PvPMode, spawnPoint);
newCharacter.GiveIdCardTags(spawnPoint);
@@ -3010,7 +3048,7 @@ namespace Barotrauma
}
}
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew)
void ParseOptionalArgs(out Vector2 spawnPosition, out WayPoint spawnPoint, out CharacterTeamType? teamType, out bool addToCrew, out string renameCharacter)
{
spawnPosition = Vector2.Zero;
spawnPoint = null;
@@ -3096,6 +3134,12 @@ namespace Barotrauma
ThrowError($"Could not parse the \"add to crew\" argument ({args[argIndex]}). Defaulting to {addToCrew}.");
}
}
argIndex++;
renameCharacter = null;
if (args.Length > argIndex)
{
renameCharacter = args[argIndex];
}
}
}
@@ -3103,6 +3147,7 @@ namespace Barotrauma
{
yield return "cursor";
yield return "inventory";
yield return "cargo";
#if SERVER
if (GameMain.Server != null)
@@ -3143,6 +3188,8 @@ namespace Barotrauma
}
}
private static ImmutableArray<string> ItemQualityNames = ["normal", "good", "excellent", "masterwork"];
private static void TrySpawnItem(string[] args)
{
try
@@ -3203,7 +3250,8 @@ namespace Barotrauma
int amount = 1;
int conditionPrc = 100;
int itemQuality = 0;
if (TryGetSpawnPosParam(out string spawnLocation, out int spawnLocationIndex))
{
switch (spawnLocation)
@@ -3223,7 +3271,7 @@ namespace Barotrauma
break;
default:
var matchingCharacter = FindMatchingCharacter(args.Skip(1).Take(1).ToArray());
if (matchingCharacter != null){ spawnInventory = matchingCharacter.Inventory; }
if (matchingCharacter != null) { spawnInventory = matchingCharacter.Inventory; }
break;
}
@@ -3232,10 +3280,21 @@ namespace Barotrauma
if (!int.TryParse(args[spawnLocationIndex + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out amount)) { amount = 1; }
amount = Math.Min(amount, 100);
}
if (args.Length > spawnLocationIndex + 2)
{
if (!int.TryParse(args[^1], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; }
if (!int.TryParse(args[spawnLocationIndex + 2], NumberStyles.Any, CultureInfo.InvariantCulture, out conditionPrc)) { conditionPrc = 100; }
}
if (args.Length > spawnLocationIndex + 3)
{
for (int i = 0; i <= Quality.MaxQuality; i++)
{
if (args[spawnLocationIndex + 3].ToLowerInvariant() == ItemQualityNames[i])
{
itemQuality = i;
}
}
}
}
@@ -3257,7 +3316,7 @@ namespace Barotrauma
}
else
{
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition);
Entity.Spawner?.AddItemToSpawnQueue(itemPrefab, spawnPos.Value, condition: itemCondition, quality: itemQuality);
}
}
else if (spawnInventory != null)
@@ -3284,6 +3343,7 @@ namespace Barotrauma
}
item.Condition = item.Health * Math.Clamp(conditionPrc / 100f, 0f, 1f);
item.Quality = itemQuality;
}
}
}

View File

@@ -31,12 +31,17 @@ namespace Barotrauma
Actions = new List<EventAction>();
foreach (var e in element.Elements())
{
if (e.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
if (e.NameAsIdentifier().Equals("statuseffect"))
{
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". Status effect configured as a sub action. Please configure status effects as child elements of a StatusEffectAction.",
contentPackage: element.ContentPackage);
continue;
}
else if (e.NameAsIdentifier().Equals(nameof(OnRoundEndAction)))
{
DebugConsole.ThrowError($"Error in event prefab \"{scriptedEvent.Prefab.Identifier}\". {nameof(OnRoundEndAction)} configured as a sub action. Please configure it as an action at the end of the event.",
contentPackage: element.ContentPackage);
}
var action = Instantiate(scriptedEvent, e);
if (action != null) { Actions.Add(action); }
}

View File

@@ -123,7 +123,7 @@ namespace Barotrauma
AddTargetPredicate(
Tags.Traitor,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated);
e => e is Character c && (c.IsPlayer || c.IsBot) && c.IsTraitor && !c.IsIncapacitated && CharacterTeamMatches(c));
}
private void TagNonTraitors()
@@ -131,7 +131,7 @@ namespace Barotrauma
AddTargetPredicate(
Tags.NonTraitor,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
e => e is Character c && (c.IsPlayer || c.IsBot) && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c));
}
private void TagNonTraitorPlayers()
@@ -139,7 +139,7 @@ namespace Barotrauma
AddTargetPredicate(
Tags.NonTraitorPlayer,
ScriptedEvent.TargetPredicate.EntityType.Character,
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated);
e => e is Character c && c.IsPlayer && !c.IsTraitor && c.IsOnPlayerTeam && !c.IsIncapacitated && CharacterTeamMatches(c));
}
private void TagBots(bool playerCrewOnly)
@@ -151,7 +151,8 @@ namespace Barotrauma
e is Character c &&
c.IsBot &&
(!c.IsIncapacitated || !IgnoreIncapacitatedCharacters) &&
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1));
(!playerCrewOnly || c.TeamID == CharacterTeamType.Team1) &&
CharacterTeamMatches(c));
}
private void TagCrew()
@@ -171,7 +172,7 @@ namespace Barotrauma
private void TagHumansByTag(Identifier tag)
{
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag)));
AddTarget(Tag, Character.CharacterList.Where(c => c.HumanPrefab != null && c.HumanPrefab.GetTags().Contains(tag) && CharacterTeamMatches(c)));
}
private void TagHumansByJobIdentifier(Identifier jobIdentifier)

View File

@@ -168,6 +168,9 @@ namespace Barotrauma.Items.Components
set { attachedByDefault = value; }
}
#if DEBUG
[Editable]
#endif
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at (in pixels, as an offset from the character's shoulder)."+
" For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards.")]
public Vector2 HoldPos
@@ -177,8 +180,10 @@ namespace Barotrauma.Items.Components
}
//the distance from the holding characters elbow to center of the physics body of the item
protected Vector2 holdPos;
#if DEBUG
[Editable]
#endif
[Serialize("0.0,0.0", IsPropertySaveable.No, description: "The position the character holds the item at when aiming (in pixels, as an offset from the character's shoulder)."+
" Works similarly as HoldPos, except that the position is rotated according to the direction the player is aiming at. For example, a value of 10,-100 would make the character hold the item 100 pixels below the shoulder and 10 pixels forwards when aiming directly to the right.")]
public Vector2 AimPos
@@ -279,6 +284,9 @@ namespace Barotrauma.Items.Components
/// <summary>
/// For setting the handle positions using status effects
/// </summary>
#if DEBUG
[Editable]
#endif
public Vector2 Handle1
{
get { return ConvertUnits.ToDisplayUnits(handlePos[0]); }
@@ -299,6 +307,9 @@ namespace Barotrauma.Items.Components
/// <summary>
/// For setting the handle positions using status effects
/// </summary>
#if DEBUG
[Editable]
#endif
public Vector2 Handle2
{
get { return ConvertUnits.ToDisplayUnits(handlePos[1]); }

View File

@@ -119,7 +119,7 @@ namespace Barotrauma.Items.Components
return OnPicked(picker, pickDroppedStack: true);
}
public virtual bool OnPicked(Character picker, bool pickDroppedStack)
public bool OnPicked(Character picker, bool pickDroppedStack, bool playSound = true)
{
//if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa)
if (item.GetComponents<Pickable>().Any())
@@ -156,7 +156,7 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
#if CLIENT
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
if (!GameMain.Instance.LoadingScreenOpen && playSound && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
PlaySound(ActionType.OnPicked, picker);
#endif
if (pickDroppedStack)
@@ -164,7 +164,7 @@ namespace Barotrauma.Items.Components
foreach (var droppedItem in droppedStack)
{
if (droppedItem == item) { continue; }
droppedItem.GetComponent<Pickable>().OnPicked(picker, pickDroppedStack: false);
droppedItem.GetComponent<Pickable>().OnPicked(picker, pickDroppedStack: false, playSound: false);
}
}
return true;

View File

@@ -42,6 +42,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: $"Should this item be removed if the linked character is null?")]
public bool RemoveItemIfCharacterNull { get; set; }
public Character? Character { get; private set; }
public bool DoesBleed => Character?.DoesBleed == true;
@@ -50,6 +53,8 @@ namespace Barotrauma.Items.Components
public LinkedControllerCharacterComponent(Item item, ContentXElement element) : base(item, element)
{
IsActive = true;
#if CLIENT
spriteOverrides = element.Elements()
.Where(static e => e.Name.LocalName.ToLowerInvariant() == "spriteoverride")
@@ -58,6 +63,16 @@ namespace Barotrauma.Items.Components
#endif
}
public override void Update(float deltaTime, Camera cam)
{
base.Update(deltaTime, cam);
if (RemoveItemIfCharacterNull && GameMain.NetworkMember is not { IsClient: true } && (Character == null || Character.Removed))
{
Entity.Spawner?.AddEntityToRemoveQueue(Item);
}
}
public void UpdateLinkedCharacter(Character? character)
{
Character = character;

View File

@@ -525,7 +525,7 @@ namespace Barotrauma.Items.Components
return true;
}
if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(x => x.Prefab == spawnItemOnSelectedPrefab))
if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(item => item == spawnedItemOnSelected))
{
return true;
}

View File

@@ -210,8 +210,6 @@ namespace Barotrauma.Items.Components
amountMultiplier = (int)itemCreationMultiplier.Value;
}
ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructed, 1f);
if (targetItem.Prefab.RandomDeconstructionOutput)
{
int amount = targetItem.Prefab.RandomDeconstructionOutputAmount;
@@ -345,6 +343,8 @@ namespace Barotrauma.Items.Components
if (targetItem.AllowDeconstruct && allowRemove)
{
ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructed, 1f);
//drop all items that are inside the deconstructed item
foreach (ItemContainer ic in targetItem.GetComponents<ItemContainer>())
{
@@ -480,6 +480,7 @@ namespace Barotrauma.Items.Components
// Move items again since the status effect could have spawned additional items in the character inventory
MoveItemsFromCharacterToOutput();
character.Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null);
Entity.Spawner?.AddEntityToRemoveQueue(character);
}, 0.1f);
}

View File

@@ -732,6 +732,7 @@ namespace Barotrauma.Items.Components
}
private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
private readonly Dictionary<ItemPrefab, int> ingredientFlexibilityCache = new Dictionary<ItemPrefab, int>();
public bool MissingRequiredRecipe(FabricationRecipe fabricableItem, Character character)
{
@@ -786,10 +787,24 @@ namespace Barotrauma.Items.Components
//maintain a list of used ingredients so we don't end up considering the same item a suitable for multiple required ingredients
usedIngredients.Clear();
return fabricableItem.RequiredItems.All(requiredItem =>
// Items are considered more flexible if they can be used in many different requirements
ingredientFlexibilityCache.Clear();
foreach (var prefab in fabricableItem.RequiredItems.SelectMany(static r => r.ItemPrefabs))
{
ingredientFlexibilityCache[prefab] = fabricableItem.RequiredItems.Count(r => r.ItemPrefabs.Contains(prefab));
}
return fabricableItem.RequiredItems
// Match the most restrictive requirements to least restrictive first, while we still have items that we can use
.OrderBy(static r => r.ItemPrefabs.Count())
.ThenByDescending(static requiredItem => requiredItem.Amount)
.All(requiredItem =>
{
int availableItemsAmount = 0;
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs
// Fill in the least flexible and more abundant items first, so we don't end up using unique items first
.OrderBy(GetItemFlexibility)
.ThenByDescending(GetAvailableItemsCount))
{
if (!availableIngredients.TryGetValue(requiredPrefab.Identifier, out var availableItems)) { continue; }
@@ -811,6 +826,16 @@ namespace Barotrauma.Items.Components
return false;
});
int GetAvailableItemsCount(ItemPrefab itemPrefab)
{
return availableIngredients.TryGetValue(itemPrefab.Identifier, out var list) ? list.Count : 0;
}
int GetItemFlexibility(ItemPrefab itemPrefab)
{
return ingredientFlexibilityCache[itemPrefab];
}
}
private float GetRequiredTime(FabricationRecipe fabricableItem, Character user)

View File

@@ -36,7 +36,7 @@ namespace Barotrauma.Items.Components
}
}
public LocalizedString? DisplayName { get; private set; }
public LocalizedString DisplayName { get; private set; }
private float supplyRatio = 1f;
public float SupplyRatio
@@ -80,6 +80,7 @@ namespace Barotrauma.Items.Components
SupplyRatio = element.GetAttributeFloat("ratio", SupplyRatio);
}
DisplayName = TextManager.Get(name).Fallback(name);
#if CLIENT
CreateGUI();
if (Screen.Selected is not { IsEditor: true })

View File

@@ -252,7 +252,7 @@ namespace Barotrauma.Items.Components
{
PhysicsBody = new PhysicsBody(currentWidth, currentHeight, radius: 0.0f, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
UserData = item
UserData = this
};
}
else
@@ -260,7 +260,7 @@ namespace Barotrauma.Items.Components
currentRadius = Math.Max(ConvertUnits.ToSimUnits(Radius * item.Scale), 0.01f);
PhysicsBody = new PhysicsBody(width: 0.0f, height: 0.0f, radius: currentRadius, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
UserData = item
UserData = this
};
}

View File

@@ -734,8 +734,8 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No, description: "Hides the condition displayed in the item's tooltip.")]
public bool HideConditionInTooltip { get; set; }
[Serialize("", IsPropertySaveable.No, description: "If set, displays if the given fabrication recipe has been unlocked or not in the tooltip. The actual unlocking of the recipe should be handled in a status effect.")]
public Identifier UnlockedRecipeInToolTip { get; set; }
[Serialize("", IsPropertySaveable.No, description: "If set, the item's tooltip displays if the given fabrication recipe has been unlocked or not. The actual unlocking of the recipe should be handled in a status effect.")]
public Identifier[] UnlockedRecipeInToolTip { get; set; }
//if true and the item has trigger areas defined, characters need to be within the trigger to interact with the item
//if false, trigger areas define areas that can be used to highlight the item

View File

@@ -2971,11 +2971,39 @@ namespace Barotrauma
string percentage = string.Format(CultureInfo.InvariantCulture, "{0:P2}", (float)spawnPointsContainingResources / PathPoints.Count);
DebugConsole.NewMessage($"Level resources spawned: {itemCount}\n" +
$" Spawn points containing resources: {spawnPointsContainingResources} ({percentage})\n" +
$" Total value: {PathPoints.Sum(p => p.ClusterLocations.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0)))} mk");
$" Total value: {GetTotalLevelResourceValue()} mk");
if (AbyssResources.Count > 0)
{
DebugConsole.NewMessage($"Abyss resources spawned: {AbyssResources.Sum(a => a.Resources.Count)}\n" +
$" Total value: {AbyssResources.Sum(c => c.Resources.Sum(r => r.Prefab.DefaultPrice?.Price ?? 0))} mk");
$" Total value: {GetTotalAbyssResourceValue()} mk");
}
int GetTotalLevelResourceValue()
{
int value = 0;
foreach (var pathPoint in PathPoints)
{
foreach (var clusterLocation in pathPoint.ClusterLocations)
{
foreach (var resource in clusterLocation.Resources)
{
value += resource.Prefab.DefaultPrice?.Price ?? 0;
}
}
}
return value;
}
int GetTotalAbyssResourceValue()
{
int value = 0;
foreach (var clusterLocation in AbyssResources)
{
foreach (var resource in clusterLocation.Resources)
{
value += resource.Prefab.DefaultPrice?.Price ?? 0;
}
}
return value;
}
#endif
@@ -3204,7 +3232,6 @@ namespace Barotrauma
}
}
/// <param name="rotation">Used by clients to set the rotation for the resources</param>
public List<Item> GenerateMissionResources(ItemPrefab prefab, int requiredAmount, PositionType positionType, IEnumerable<Cave> targetCaves = null)
{
var allValidLocations = GetAllValidClusterLocations();
@@ -5150,6 +5177,7 @@ namespace Barotrauma
renderer.Dispose();
renderer = null;
}
backgroundCreatureManager?.Clear();
#endif
if (LevelObjectManager != null)

View File

@@ -11,7 +11,7 @@ namespace Barotrauma
partial class LevelObject : ISpatialEntity, IDamageable, ISerializableEntity
{
public readonly LevelObjectPrefab Prefab;
public Vector3 Position;
public Vector3 Position { get; set; }
public float NetworkUpdateTimer;

View File

@@ -457,7 +457,7 @@ namespace Barotrauma
if (newObject.NeedsUpdate) { updateableObjects.Add(newObject); }
//add some variance to the Z position to prevent z-fighting
//(based on the x and y position of the object, scaled to be visually insignificant)
newObject.Position.Z += (minX + minY) % 100.0f * 0.00001f;
newObject.Position += new Vector3(0, 0, (minX + minY) % 100.0f * 0.00001f);
int xStart = (int)Math.Floor(minX / GridSize);
int xEnd = (int)Math.Floor(maxX / GridSize);

View File

@@ -348,11 +348,22 @@ namespace Barotrauma
{
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplierAffiliated, includeSaved: false));
price *= 1f - characters.Max(static c => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, Tags.StatIdentifierTargetAll));
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplierAffiliated, tag)));
price *= 1f - characters.Max(c => GetStatValuesForItem(c, item, StatTypes.StoreBuyMultiplierAffiliated));
}
price *= 1f - characters.Max(static c => c.GetStatValue(StatTypes.StoreBuyMultiplier, includeSaved: false));
price *= 1f - characters.Max(c => item.Tags.Sum(tag => c.Info.GetSavedStatValue(StatTypes.StoreBuyMultiplier, tag)));
price *= 1f - characters.Max(c => GetStatValuesForItem(c, item, StatTypes.StoreBuyMultiplier));
}
static float GetStatValuesForItem(Character character, ItemPrefab item, StatTypes statType)
{
float statValueSum = 0.0f;
foreach (Identifier itemTag in item.Tags)
{
statValueSum += character.Info.GetSavedStatValue(statType, itemTag);
}
return statValueSum;
}
// Price should never go below 1 mk
return Math.Max((int)price, 1);
}

View File

@@ -487,7 +487,19 @@ namespace Barotrauma
var portrait = new Sprite(subElement, lazyLoad: true);
if (portrait != null)
{
#if CLIENT
if (!File.Exists(portrait.FilePath))
{
DebugConsole.ThrowError($"Error in location type \"{Identifier}\": cannot find the location portrait \"{portrait.FilePath}\".");
}
else
{
portraitsList.Add(portrait);
}
#elif SERVER
// Add without checking the path, since servers don't parse the file path of the sprite
portraitsList.Add(portrait);
#endif
}
break;
case "store":

View File

@@ -261,15 +261,7 @@ namespace Barotrauma
}
}
foreach (var endLocation in EndLocations)
{
if (endLocation.Type?.ForceLocationName is { IsEmpty: false })
{
endLocation.ForceName(endLocation.Type.ForceLocationName);
}
}
AssignEndLocationLevelData();
AssignEndLocationLevelData(campaign);
//backwards compatibility: if locations go out of bounds (map saved with different generation parameters before width/height were included in the xml)
float maxX = Locations.Select(l => l.MapPosition.X).Max();
@@ -973,13 +965,43 @@ namespace Barotrauma
previousToEndLocation.Connections.Add(endConnection);
endLocation.Connections.Add(endConnection);
AssignEndLocationLevelData();
AssignEndLocationLevelData(campaign);
}
private void AssignEndLocationLevelData()
/// <summary>
/// Assigns the correct outpost generation parameters to the end locations. Also checks and ensures that all of them are correctly assigned to the end biome, and have a location type that can be generated in the end biome.
/// Strangely shaped custom maps may sometimes generate in a way that there aren't enough locations in the last biome to assign as the end locations, and we may end up choosing locations in the second-to-last biome instead - let's correct that here.
/// </summary>
/// <param name="campaign"></param>
/// <exception cref="InvalidOperationException"></exception>
private void AssignEndLocationLevelData(CampaignMode campaign)
{
Biome endBiome = Biome.Prefabs.OrderBy(p => p.UintIdentifier).FirstOrDefault(b => b.IsEndBiome) ?? throw new InvalidOperationException("Could not find an end biome to assign to the end locations.");
LocationType endLocationType =
LocationType.Prefabs
.OrderBy(p => p.UintIdentifier)
.FirstOrDefault(IsSuitableEndLocationType)
?? throw new InvalidOperationException("Could not find an a location type to assign to the end locations.");
bool IsSuitableEndLocationType(LocationType lt)
{
return lt.AreaSettings.Any(s =>
s.Commonness > 0 &&
(s.MatchesBiome(endBiome.Identifier) || s.MatchesZone(generationParams.DifficultyZones)));
}
for (int i = 0; i < endLocations.Count; i++)
{
if (endLocations[i].Biome != endBiome)
{
endLocations[i].Biome = endBiome;
endLocations[i].LevelData = new LevelData(endLocations[i], this, endLocations[i].LevelData.Difficulty);
}
endLocations[i].ChangeType(campaign: campaign, endLocationType);
if (endLocationType.ForceLocationName is { IsEmpty: false })
{
endLocations[i].ForceName(endLocationType.ForceLocationName);
}
endLocations[i].LevelData.ReassignGenerationParams(Seed);
var outpostParams = OutpostGenerationParams.OutpostParams.FirstOrDefault(p => p.ForceToEndLocationIndex == i);
if (outpostParams != null)

View File

@@ -275,7 +275,7 @@ namespace Barotrauma
{
CreateStairBodies();
}
else if (HasBody)
else if (Prefab.Body)
{
CreateSections();
UpdateSections();
@@ -346,7 +346,7 @@ namespace Barotrauma
{
Rectangle oldRect = Rect;
base.Rect = value;
if (HasBody)
if (Prefab.Body)
{
CreateSections();
UpdateSections();
@@ -668,7 +668,7 @@ namespace Barotrauma
{
prevSections = Sections.ToArray();
}
if (!HasBody)
if (!Prefab.Body)
{
if (FlippedX && IsHorizontal)
{
@@ -685,7 +685,7 @@ namespace Barotrauma
xsections = 1;
ysections = 1;
}
Sections = new WallSection[xsections];
Sections = new WallSection[Math.Max(xsections, ysections)];
}
else
{
@@ -1635,7 +1635,7 @@ namespace Barotrauma
CreateStairBodies();
}
if (HasBody)
if (Prefab.Body)
{
CreateSections();
UpdateSections();
@@ -1663,7 +1663,7 @@ namespace Barotrauma
CreateStairBodies();
}
if (HasBody)
if (Prefab.Body)
{
CreateSections();
UpdateSections();

View File

@@ -29,6 +29,31 @@ namespace Barotrauma
partial class SubmarineInfo : IDisposable
{
public static HashSet<string> SubmarinePathsWithRemoteStorage { get; set; } = [];
public bool SaveToRemoteStorage
{
get
{
if (FilePath == null) { return false; }
return SubmarinePathsWithRemoteStorage.Contains(FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false));
}
set
{
if (FilePath == null) { return; }
if (value)
{
SubmarinePathsWithRemoteStorage.Add(FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false));
}
else
{
SubmarinePathsWithRemoteStorage.Remove(FilePath.CleanUpPathCrossPlatform(correctFilenameCase: false));
}
}
}
private static List<SubmarineInfo> savedSubmarines = new List<SubmarineInfo>();
public static IEnumerable<SubmarineInfo> SavedSubmarines => savedSubmarines;
@@ -197,6 +222,8 @@ namespace Barotrauma
set;
}
public bool IsFromRemoteStorage;
/// <summary>
/// When enabled, the <see cref="SubmarineElement">XML element is not loaded</see> until it is accessed.
/// </summary>

View File

@@ -562,6 +562,19 @@ namespace Barotrauma
IgnoredHints.Init(currentConfigDoc.Root.GetChildElement("ignoredhints"));
DebugConsoleMapping.Init(currentConfigDoc.Root.GetChildElement("debugconsolemapping"));
CompletedTutorials.Init(currentConfigDoc.Root.GetChildElement("tutorials"));
var submarineSettings = currentConfigDoc.Root.GetChildElement("submarinesettings");
if (submarineSettings != null)
{
SubmarineInfo.SubmarinePathsWithRemoteStorage.Clear();
foreach (XElement subElement in submarineSettings.Elements("SubmarineWithRemoteStorage"))
{
string path = subElement.GetAttributeString("path", "");
if (!path.IsNullOrEmpty())
{
SubmarineInfo.SubmarinePathsWithRemoteStorage.Add(path);
}
}
}
#endif
}
else
@@ -689,7 +702,14 @@ namespace Barotrauma
XElement tutorialsElement = new XElement("tutorials"); root.Add(tutorialsElement);
CompletedTutorials.Instance.SaveTo(tutorialsElement);
XElement submarineSettings = new XElement("submarinesettings"); root.Add(submarineSettings);
SubmarineInfo.SubmarinePathsWithRemoteStorage.ForEach(path =>
{
submarineSettings.Add(new XElement("SubmarineWithRemoteStorage", new XAttribute("path", path)));
});
XElement keyMappingElement = new XElement("keymapping",
currentConfig.KeyMap.Bindings.Select(kvp
=> new XAttribute(kvp.Key.ToString(), kvp.Value.ToString())));

View File

@@ -1786,7 +1786,7 @@ namespace Barotrauma
offset *= item.Scale;
if (item.FlippedX) { offset.X *= -1; }
if (item.FlippedY) { offset.Y *= -1; }
offset = Vector2.Transform(offset, Matrix.CreateRotationZ(-item.RotationRad));
offset = Vector2.Transform(offset, Matrix.CreateRotationZ(item.body?.Rotation ?? -item.RotationRad));
}
}

View File

@@ -0,0 +1,105 @@
#nullable enable
using Barotrauma.IO;
using Microsoft.Xna.Framework;
using Steamworks;
using System;
using System.Diagnostics.CodeAnalysis;
namespace Barotrauma.Steam;
internal static partial class RemoteStorageHelper
{
public static readonly Color SteamColor = Color.DodgerBlue;
public static readonly string DebugPrefix = $"‖color:{SteamColor.ToStringHex()}‖[Remote Storage]‖end‖";
/// <summary>Attempts to read a file from remote storage into a byte array.</summary>
/// <param name="remoteFile">The remote file to read from.</param>
/// <param name="bytes">The bytes read from the remote file. Returns <see langword="null"/> if the operation failed.</param>
/// <returns>
/// <see langword="true"/> if the operation was successful.<br/>
/// <see langword="false"/> if the operation failed.
/// </returns>
public static bool TryRead(this SteamRemoteStorage.RemoteFile remoteFile, [NotNullWhen(returnValue: true)] out byte[]? bytes, bool logError = true)
{
bytes = SteamRemoteStorage.FileRead(remoteFile.Filename);
bool success = bytes != null;
if (logError && !success)
{
DebugConsole.ThrowError($"{DebugPrefix} Failed to read file \"{remoteFile.Filename}\" from remote storage: operation failed.");
}
return success;
}
/// <summary>Attempts to write a file to remote storage.</summary>
/// <param name="localPath">The path of the local file to read from.</param>
/// <param name="saveAs">The name of the remote file to write to. If <see langword="null"/>, the file name of <paramref name="localPath"/> is used.</param>
/// <param name="allowOverwrite">If <see langword="true"/>, overwriting existing remote files is allowed.</param>
/// <returns>
/// <see langword="true"/> if the operation was successful.<br/>
/// <see langword="false"/> if the operation failed.
/// </returns>
public static bool TryWrite(string localPath, string? saveAs = null, bool allowOverwrite = false, bool logError = true)
{
string fileName = saveAs ?? Path.GetFileName(localPath);
if (!allowOverwrite && SteamRemoteStorage.FileExists(fileName))
{
if (logError)
{
DebugConsole.ThrowError($"{DebugPrefix} Failed to write file \"{fileName}\" to remote storage: file already exists.");
}
return false;
}
byte[] data;
try
{
data = File.ReadAllBytes(localPath);
}
catch (Exception exception)
{
if (logError)
{
DebugConsole.ThrowError($"{DebugPrefix} Failed to read file \"{fileName}\" while writing to remote storage: {exception}");
}
return false;
}
bool success = SteamRemoteStorage.FileWrite(fileName, data);
if (logError && !success)
{
DebugConsole.ThrowError($"{DebugPrefix} Failed to write file \"{fileName}\" to remote storage: operation failed.");
}
return success;
}
/// <summary>Attempts to delete a file from remote storage.</summary>
/// <param name="fileName">The name of the remote file to delete.</param>
/// <returns>
/// <see langword="true"/> if the operation was successful.<br/>
/// <see langword="false"/> if the operation failed.
/// </returns>
public static bool TryDelete(string fileName, bool logError = true)
{
bool success = SteamRemoteStorage.FileDelete(fileName);
if (logError && !success)
{
DebugConsole.ThrowError($"{DebugPrefix} Failed to delete file \"{fileName}\" from remote storage: operation failed.");
}
return success;
}
/// <summary>Checks if a file is stored remotely.</summary>
/// <param name="fileName">The name of the remote file to check.</param>
/// <returns>
/// <see langword="true"/> if the file is stored.<br/>
/// <see langword="false"/> if the file is not stored or the operation failed.
/// </returns>
public static bool IsStored(string fileName) => SteamRemoteStorage.FileExists(fileName);
}

View File

@@ -1,4 +1,58 @@
-------------------------------------------------------------------------------------------------------------------------------------------------
v1.13.3.1 (Summer Update 2026)
-------------------------------------------------------------------------------------------------------------------------------------------------
Submarine reworks:
- Humpback, Orca 2, Azimuth, Typhon, and Herja have received their visual and gameplay reworks.
- The command room of the Orca 2 is now located at the center of the submarine.
- Typhon now comes with valves and pipe weakpoints.
- Herja has been upgraded with a power distributor, befitting its high-tech theme.
Changes and additions:
- Added an option to back up your custom submarines in the Steam Cloud. Can be enabled per-submarine using a checkbox in the sub editor's save dialog.
- Added quality parameter to the give/spawnitem console commands. Allows spawning in items with non-default quality.
- The teleportsub console command has a parameter for choosing which submarine to teleport.
- The spawncharacter console command has a parameter for renaming the spawned character.
- The showseed console command displays the map seed too if used in campaign mode.
Multiplayer:
- Fixed monster attacks that run over time (e.g. when fractal guardians fire the steam cannon) causing an excessive amount of network usage in multiplayer.
- Fixed an exploit that allowed modified clients to cause other clients to eventually get out of sync and disconnect.
- Fixed inability to drag and drop stacks of items to other players in multiplayer.
- Fixed submarine voting not working in campaign mode.
Miscellaneous fixes:
- Fixed security (or anyone else) not reacting to attacking stunned/incapacitated characters.
- Fixed the item pickup sound playing multiple times, for every item in a stack you're picking up.
- Fixed the item dropping sound playing twice when dropping an item.
- Fixed being unable to fabricate certain items with specific combinations of materials. Happened in some cases where the recipe accepted multiple different materials as ingredients: the fabricator would got through the requirements in order, and always take the first available items without considering that the item could've been necessary for another, more strict requirement.
- Followup to the "infinite explosion" fix in Summer Update 2025: the previous fix only applied to oxygen tank shelves, but it turned out oxygen generators could also cause the same kind of "explosion loop" where tanks keep exploding and getting refilled by the oxygen generator.
- Fixed "inspirational leader" talent not giving bonus XP like the description says it should.
- Fixed characters being able to drop off platforms while using a periscope (inconsistent with other movement inputs being disabled while on a periscope).
- Fixed bots being unable to extinguish fires in connected subs (e.g. in Remora's drone).
- Fixed parts of the CPR button not being clickable on the health HUD on certain resolutions (was getting blocked by the limb indicators).
- Fixed nuclear shells fabricated with the cheaper recipe variant not giving the "I am become death" achievement.
- Fixed gravity spheres (or more generally, any items with a triggercomponent) taking damage when you cut their trigger area with a plasma cutter, rather than the actual collider of the item.
- Fixed equip buttons being clickable despite the slot being hidden. Meant that when you had equipped an item in your hand, you could click an invisible button at the left side of the inventory where the hand slots would appear.
- Fixed turrets not showing the ammo on the HUD if the ammo is inside the turret itself, rather than a linked loader.
- If one of the unique hireable characters (e.g. Ignatius May, Aunt Doris) dies in the outpost before you hire them, they can no longer appear elsewhere or be hired.
- Fixed cargo scooter lights working, but not draining the battery, when the battery is in another slot than the battery slot.
- Fixed custom interaction messages set on items in the sub editor no longer appearing in-game.
- Allow combining defense bot ammo boxes the same way as other ammo boxes and magazines (merging their ammo together).
- Fixed the character deconstruction bag staying in the deconstructor if you do a level transition while a character is inside the deconstructor.
- Fixed items duplicating if a character gets deconstructed without dying first (possible e.g. by taking advantage of the Miracle Worker talent).
- Fixed crafting blueprint tooltips not showing whether the recipe has been unlocked or not.
- Fixed valves potentially getting stuck in a non-interactable state if the round ends immediately after one's been toggled.
Modding:
- Fixed "LockedTalents" PermanentStat locking the talent for everyone (not used in any vanilla talent).
- Clients are allowed to use colored text in their chat messages when they have the "chat spam immunity" permission. Colored text was disabled in client-sent chat messages in the previous update due to some ways in which it can be abused, but turns out there were some users relying on this functionality.
- Fixed OnDeconstructed status effect triggering when the item is not deconstructed in some cases (e.g. researching unidentified genetic material without stabilozine).
- Fixed the special locations at the end of the campaign map generating incorrectly on very short maps.
- Fixed status effects using OffsetCopiesEntityTransform not taking physics body rotation into account.
- Fixed TagAction's Team setting being ignored when tagging characters in certain ways (e.g. traitors, non-traitors, bots, human prefab tags).
-------------------------------------------------------------------------------------------------------------------------------------------------
v1.12.7.0
-------------------------------------------------------------------------------------------------------------------------------------------------

View File

@@ -21,7 +21,7 @@ You need a version of Visual Studio that supports C# 10 to compile game. If you
When installing on Windows, make sure you select ".NET desktop development" during the install process to make sure you have the required features to work with Barotrauma.
#### Linux
You will need to install the .NET 6 SDK according to the instructions laid out on Microsoft's docs: https://docs.microsoft.com/en-us/dotnet/core/install/linux
You will need to install the .NET 8 SDK according to the instructions laid out on Microsoft's docs: https://docs.microsoft.com/en-us/dotnet/core/install/linux
To edit the source code, we recommend using [Visual Studio Code](https://code.visualstudio.com/) with [Microsoft's C# extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp).