Release 1.11.4.1 (Winter Update)

This commit is contained in:
Markus Isberg
2025-12-08 14:56:47 +00:00
parent 21e34e5cd8
commit 598966f200
121 changed files with 1614 additions and 819 deletions
@@ -87,6 +87,13 @@ namespace Barotrauma.Items.Components
"Normally there's no need to touch this setting, but if you notice the docking position is incorrect (for example due to some unusual docking port configuration without hulls or doors), you can use this to enforce the direction.")]
public DirectionType ForceDockingDirection { get; set; }
[Serialize(false, IsPropertySaveable.Yes, description: "Was the docking port docked at the end of the previous round.")]
public bool WasDocked
{
get;
set;
}
public DockingPort DockingTarget { get; private set; }
/// <summary>
@@ -280,6 +287,9 @@ namespace Barotrauma.Items.Components
OnDocked?.Invoke();
OnDocked = null;
WasDocked = true;
DockingTarget.Docked = true;
}
public void Lock(bool isNetworkMessage, bool applyEffects = true, bool moveSubs = true)
@@ -988,6 +998,8 @@ namespace Barotrauma.Items.Components
Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine);
obstructedWayPointsDisabled = false;
WasDocked = false;
DockingTarget.WasDocked = false;
DockingTarget.Undock();
DockingTarget = null;
@@ -1052,6 +1064,16 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
//PRETTY HACKY:
//the docking port was docked on the previous round, but not any more -
//must mean that whatever it was docked to (e.g. some enemy sub or respawn shuttle) no longer exists
//let's send an "on_undock" signal so circuits can react to the undocking that never "actually" happened
if (!docked && WasDocked)
{
item.SendSignal("1", "on_undock");
WasDocked = false;
}
dockingCooldown -= deltaTime;
if (DockingTarget == null)
{
@@ -1208,19 +1230,21 @@ namespace Barotrauma.Items.Components
}
}
if (!item.linkedTo.Any()) { return; }
List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
foreach (MapEntity entity in linked)
{
if (!(entity is Item linkedItem)) { continue; }
var dockingPort = linkedItem.GetComponent<DockingPort>();
if (dockingPort != null)
if (item.linkedTo.Any())
{
List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
foreach (MapEntity entity in linked)
{
Dock(dockingPort);
}
if (entity is not Item linkedItem) { continue; }
var dockingPort = linkedItem.GetComponent<DockingPort>();
if (dockingPort != null)
{
Dock(dockingPort);
}
}
}
}
public override void ReceiveSignal(Signal signal, Connection connection)
@@ -154,6 +154,13 @@ namespace Barotrauma.Items.Components
}
}
[Serialize("0,0", IsPropertySaveable.Yes)]
public Point DisallowAttachingOverSize
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Should the item be attached to a wall by default when it's placed in the submarine editor.")]
public bool AttachedByDefault
{
@@ -496,13 +503,19 @@ namespace Barotrauma.Items.Components
Vector2 diff = new Vector2(
(heldHand.SimPosition.X - arm.SimPosition.X) / 2f,
(heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f);
item.SetTransform(heldHand.SimPosition + diff, 0.0f);
//we have forced the item to be in the same sub as the dropper above,
//and are placing it to the position of the hands in "local" coordinates
//which may be outside the sub if the character is e.g. standing half-way through the airlock
// -> let's use the forceSubmarine argument ensure the item is still considered to be in the sub's coordinate space,
// or it will end up in a weird state and seemingly disappear
item.SetTransform(heldHand.SimPosition + diff, 0.0f, forceSubmarine: picker.Submarine);
}
else
{
item.SetTransform(picker.SimPosition, 0.0f);
}
}
item.SetTransform(picker.SimPosition, 0.0f, forceSubmarine: picker.Submarine);
}
}
}
picker.Inventory.RemoveItem(item);
@@ -621,17 +634,34 @@ namespace Barotrauma.Items.Components
if (disallowAttachingOverTags.Any() || !AllowAttachInsideDoors)
{
var connectedHulls = item.CurrentHull?.GetConnectedHulls(includingThis: true, searchDepth: 5, ignoreClosedGaps: true);
Vector2 size = item.Rect.Size.ToVector2() / 2;
Vector2 size = DisallowAttachingOverSize == Point.Zero ?
item.Rect.Size.ToVector2() :
DisallowAttachingOverSize.ToVector2() * item.Scale;
size /= 2f;
foreach (Item otherItem in Item.ItemList)
{
if (otherItem == item || otherItem.body is { BodyType: BodyType.Dynamic, Enabled: true }) { continue; }
if (connectedHulls != null && !connectedHulls.Contains(otherItem.CurrentHull)) { continue; }
if (disallowAttachingOverTags.None(tag => otherItem.HasTag(tag)) &&
if (disallowAttachingOverTags.None(otherItem.HasTag) &&
(otherItem.GetComponent<Door>() == null || AllowAttachInsideDoors))
{
continue;
}
Rectangle worldRect = otherItem.WorldRect;
if (otherItem.GetComponent<Holdable>() is Holdable otherHoldable)
{
if (!otherHoldable.attached) { continue; }
if (otherHoldable.DisallowAttachingOverSize != Point.Zero)
{
Vector2 scaledSize = otherHoldable.DisallowAttachingOverSize.ToVector2() * item.Scale;
worldRect = new Rectangle(
otherItem.WorldPosition.ToPoint() - new Point((int)(scaledSize.X / 2), (int)(-scaledSize.Y / 2)),
scaledSize.ToPoint());
}
}
if (attachPos.X + size.X < worldRect.X || attachPos.X - size.X > worldRect.Right) { continue; }
if (attachPos.Y - size.Y > worldRect.Y || attachPos.Y + size.Y < worldRect.Y - worldRect.Height) { continue; }
tempOverlappingItems.Add(otherItem);
@@ -538,8 +538,8 @@ namespace Barotrauma.Items.Components
}
if (targetEntity != null)
{
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, afflictionMultiplier: damageMultiplier);
ApplyStatusEffects(conditionalActionType, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, attackMultiplier: damageMultiplier);
ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb, useTarget: targetEntity, user: user, attackMultiplier: damageMultiplier);
}
if (DeleteOnUse)
@@ -920,7 +920,8 @@ namespace Barotrauma.Items.Components
}
}
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float afflictionMultiplier = 1.0f)
/// <param name="attackMultiplier">Multiplier used on afflictions caused by the status effects, except ones that <see cref="AfflictionPrefab.AffectedByAttackMultipliers">have been configured to not be affected by attack multipliers.</see></param>
public void ApplyStatusEffects(ActionType type, float deltaTime, Character character = null, Limb targetLimb = null, Entity useTarget = null, Character user = null, Vector2? worldPosition = null, float attackMultiplier = 1.0f)
{
if (statusEffectLists == null) { return; }
@@ -932,7 +933,7 @@ namespace Barotrauma.Items.Components
{
if (broken && !effect.AllowWhenBroken && effect.type != ActionType.OnBroken) { continue; }
if (user != null) { effect.SetUser(user); }
effect.AfflictionMultiplier = afflictionMultiplier;
effect.AttackMultiplier = attackMultiplier;
var c = character;
if (user != null && effect.HasTargetType(StatusEffect.TargetType.Character) && !effect.HasTargetType(StatusEffect.TargetType.UseTarget))
{
@@ -940,7 +941,7 @@ namespace Barotrauma.Items.Components
c = user;
}
item.ApplyStatusEffect(effect, type, deltaTime, c, targetLimb, useTarget, isNetworkEvent: false, checkCondition: false, worldPosition);
effect.AfflictionMultiplier = 1.0f;
effect.AttackMultiplier = 1.0f;
reducesCondition |= effect.ReducesItemCondition();
}
//if any of the effects reduce the item's condition, set the user for OnBroken effects as well
@@ -832,7 +832,7 @@ namespace Barotrauma.Items.Components
public float FabricationDegreeOfSuccess(Character character, ImmutableArray<Skill> skills)
{
if (skills.Length == 0) { return 1.0f; }
if (skills.Length == 0) { return 0.5f; }
if (character == null) { return 0.0f; }
float minDegreeOfSuccess = 1.0f;
@@ -0,0 +1,46 @@
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
namespace Barotrauma.Items.Components
{
/// <summary>
/// Makes the item inherit the condition from a linked wall or multiple - or in other words, makes it essentially treat the health of the wall as its own health.
/// The wall section with the most damage determines the condition (i.e. the item will be fully broken if there's at least one fully broken wall section).
/// </summary>
class InheritConditionFromLinkedWall(Item item, ContentXElement element) : ItemComponent(item, element)
{
private readonly List<Structure> linkedWalls = [];
public override void OnMapLoaded()
{
foreach (var linkedTo in item.linkedTo)
{
if (linkedTo is Structure structure &&
structure.HasBody)
{
linkedWalls.Add(structure);
structure.OnHealthChanged += (_, _) => UpdateCondition();
}
}
if (linkedWalls.None())
{
DebugConsole.AddWarning($"The item {item.Name} ({item.Prefab.Identifier}) is not linked to any walls with a physics body. The {nameof(InheritConditionFromLinkedWall)} component will do nothing.");
}
}
private void UpdateCondition()
{
float lowestHealthPercent = 1.0f;
foreach (var wall in linkedWalls)
{
foreach (var section in wall.Sections)
{
lowestHealthPercent = Math.Min(lowestHealthPercent, 1.0f - section.damage / wall.MaxHealth);
}
}
item.Condition = item.MaxCondition * lowestHealthPercent;
}
}
}
@@ -136,6 +136,7 @@ namespace Barotrauma.Items.Components
item.CurrentHull.GetLinkedHulls(linkedHulls, includeHiddenHulls: true);
foreach (var linkedHull in linkedHulls)
{
if (linkedHull == item.CurrentHull) { continue; }
hullWaterVolume += linkedHull.WaterVolume;
totalHullVolume += linkedHull.Volume;
}
@@ -148,7 +149,7 @@ namespace Barotrauma.Items.Components
if (!IsActive || Disabled) { return; }
if (flowPercentage <= 0f && item.CurrentHull.WaterVolume <= 0f) { return; }
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
float powerFactor = Math.Min(PowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
currFlow = flowPercentage / 100.0f * MaxFlow * powerFactor;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
@@ -247,9 +247,13 @@ namespace Barotrauma.Items.Components
}
}
bool fissionRateControlledBySignals = signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1;
bool turbineOutputRateControlledBySignals = signalControlledTargetTurbineOutput.HasValue && lastReceivedTurbineOutputSignalTime > Timing.TotalTime - 1;
//rapidly adjust the reactor in the first few seconds of the round to prevent overvoltages if the load changed between rounds
//(unless the reactor is being operated by a player)
if (GameMain.GameSession is { RoundDuration: <5 } && lastUser is not { IsPlayer: true })
if (GameMain.GameSession is { RoundDuration: < 5 } && lastUser is not { IsPlayer: true } && PowerOn && AutoTemp &&
!fissionRateControlledBySignals && !turbineOutputRateControlledBySignals)
{
UpdateAutoTemp(100.0f, (float)(Timing.Step * 10.0f));
}
@@ -263,7 +267,7 @@ namespace Barotrauma.Items.Components
float maxPowerOut = GetMaxOutput();
if (signalControlledTargetFissionRate.HasValue && lastReceivedFissionRateSignalTime > Timing.TotalTime - 1)
if (fissionRateControlledBySignals)
{
TargetFissionRate = adjustValueWithoutOverShooting(TargetFissionRate, signalControlledTargetFissionRate.Value, deltaTime * 5.0f);
#if CLIENT
@@ -274,7 +278,7 @@ namespace Barotrauma.Items.Components
{
signalControlledTargetFissionRate = null;
}
if (signalControlledTargetTurbineOutput.HasValue && lastReceivedTurbineOutputSignalTime > Timing.TotalTime - 1)
if (turbineOutputRateControlledBySignals)
{
TargetTurbineOutput = adjustValueWithoutOverShooting(TargetTurbineOutput, signalControlledTargetTurbineOutput.Value, deltaTime * 5.0f);
#if CLIENT
@@ -641,8 +641,7 @@ namespace Barotrauma.Items.Components
for (int i = 0; i < hits.Count; i++)
{
var h = hits[i];
item.SetTransform(h.Point, rotation);
item.Submarine = h.Submarine;
item.SetTransform(h.Point, rotation, forceSubmarine: h.Submarine);
item.UpdateTransform();
if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
{
@@ -103,7 +103,7 @@ namespace Barotrauma.Items.Components
var inputBuilder = ImmutableArray.CreateBuilder<CircuitBoxInputConnection>();
var outputBuilder = ImmutableArray.CreateBuilder<CircuitBoxOutputConnection>();
foreach (Connection conn in Item.Connections)
foreach (Connection conn in Item.Connections.OrderBy(static c => c.DisplayOrder))
{
if (conn.IsOutput)
{
@@ -236,9 +236,7 @@ namespace Barotrauma.Items.Components
cloneNode.ReplaceAllConnectionLabelOverrides(origNode.ConnectionLabelOverrides);
}
if (!clonedContainedItems.Any()) { return; }
foreach (var origComp in original.Components)
foreach (CircuitBoxComponent origComp in original.Components)
{
if (!clonedContainedItems.TryGetValue(origComp.Item.ID, out var clonedItem)) { continue; }
var newComponent = new CircuitBoxComponent(origComp.ID, clonedItem, origComp.Position, this, origComp.UsedResource);
@@ -661,6 +659,7 @@ namespace Barotrauma.Items.Components
}
wire.From.Connection.CircuitBoxConnections.Remove(wire.To);
wire.To.Connection.CircuitBoxConnections.Remove(wire.From);
if (wire.From is CircuitBoxInputConnection input)
{
@@ -17,6 +17,8 @@ namespace Barotrauma.Items.Components
//how many wires can be linked to this connection in total
public readonly int MaxWires = 5;
public readonly int DisplayOrder;
public readonly string Name;
private readonly LocalizedString _displayName;
public LocalizedString DisplayName
@@ -92,7 +94,7 @@ namespace Barotrauma.Items.Components
return "Connection (" + item.Name + ", " + Name + ")";
}
public Connection(ContentXElement element, ConnectionPanel connectionPanel, IdRemap idRemap)
public Connection(ContentXElement element, int connectionIndex, ConnectionPanel connectionPanel, IdRemap idRemap, bool isItemSwap)
{
#if CLIENT
@@ -117,25 +119,44 @@ namespace Barotrauma.Items.Components
IsOutput = element.Name.ToString() == "output";
Name = element.GetAttributeString("name", IsOutput ? "output" : "input");
int displayOrder;
if (element.GetAttribute("displayorderoverride") is not { } displayOrderAttr)
{
var sameElements = connectionPanel.Connections.Where(c => c.IsOutput == IsOutput);
displayOrder = !sameElements.Any() ? 0 : sameElements.Max(static c => c.DisplayOrder) + 1;
}
else
{
displayOrder = displayOrderAttr.GetAttributeInt(0);
}
DisplayOrder = displayOrder;
string displayNameTag = "", fallbackTag = "";
//if displayname is not present, attempt to find it from the prefab
if (element.GetAttribute("displayname") == null)
{
foreach (var subElement in item.Prefab.ConfigElement.Elements())
{
if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; }
if (!subElement.Name.ToString().Equals("connectionpanel", StringComparison.OrdinalIgnoreCase)) { continue; }
int prefabConnectionIndex = 0;
foreach (XElement connectionElement in subElement.Elements())
{
string prefabConnectionName = connectionElement.GetAttributeString("name", null);
if (prefabConnectionName.IsNullOrEmpty()) { continue; }
string[] aliases = connectionElement.GetAttributeStringArray("aliases", Array.Empty<string>());
if (prefabConnectionName == Name || aliases.Contains(Name))
if (prefabConnectionName == Name || aliases.Contains(Name) ||
//when swapping items, we move wires based on the order of the connections, not the names
//= we should find a connection based on the index if the name doesn't match
(isItemSwap && connectionIndex == prefabConnectionIndex))
{
displayNameTag = connectionElement.GetAttributeString("displayname", "");
fallbackTag = connectionElement.GetAttributeString("fallbackdisplayname", "");
}
prefabConnectionIndex++;
}
}
}
}
else
{
@@ -78,10 +78,10 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString())
{
case "input":
Connections.Add(new Connection(subElement, this, IdRemap.DiscardId));
Connections.Add(new Connection(subElement, connectionIndex: Connections.Count, this, IdRemap.DiscardId, isItemSwap: false));
break;
case "output":
Connections.Add(new Connection(subElement, this, IdRemap.DiscardId));
Connections.Add(new Connection(subElement, connectionIndex: Connections.Count, this, IdRemap.DiscardId, isItemSwap: false));
break;
}
}
@@ -293,10 +293,10 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString())
{
case "input":
loadedConnections.Add(new Connection(subElement, this, idRemap));
loadedConnections.Add(new Connection(subElement, connectionIndex: loadedConnections.Count, this, idRemap, isItemSwap));
break;
case "output":
loadedConnections.Add(new Connection(subElement, this, idRemap));
loadedConnections.Add(new Connection(subElement, connectionIndex: loadedConnections.Count, this, idRemap, isItemSwap));
break;
}
}
@@ -1,4 +1,5 @@
using System;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,7 +8,7 @@ namespace Barotrauma.Items.Components;
/// <summary>
/// Base class for signal components that can select between input/output connections (e.g. multiplexer and demultiplexer components)
/// </summary>
abstract class ConnectionSelectorComponent : ItemComponent
abstract partial class ConnectionSelectorComponent : ItemComponent, IServerSerializable
{
protected int selectedConnectionIndex;
protected string selectedConnectionIndexStr;
@@ -22,6 +23,8 @@ abstract class ConnectionSelectorComponent : ItemComponent
get { return selectedConnectionIndex; }
set
{
int prevIndex = selectedConnectionIndex; // store original, so we know if the state has changed and can sync it in MP
selectedConnectionIndex = Math.Max(0, value);
//don't clamp until we've determined how many connections the item has
//(can't be done until the connection panel component has been loaded too)
@@ -31,6 +34,11 @@ abstract class ConnectionSelectorComponent : ItemComponent
}
selectedConnectionName = GetConnectionName(selectedConnectionIndex);
selectedConnectionIndexStr = selectedConnectionIndex.ToString();
if (prevIndex != selectedConnectionIndex)
{
OnStateChanged();
}
}
}
@@ -55,6 +63,8 @@ abstract class ConnectionSelectorComponent : ItemComponent
{
}
partial void OnStateChanged();
protected abstract string GetConnectionName(int connectionIndex);
/// <summary>
@@ -63,10 +63,7 @@ namespace Barotrauma.Items.Components
/// This can be used to make them additionally work the other way around, periodically getting the current value of the property from the item and refreshing the UI.
/// </summary>
public float GetValueInterval { get; set; } = -1.0f;
#if CLIENT
public float GetValueTimer;
#endif
public string Name => "CustomInterfaceElement";
@@ -248,7 +245,7 @@ namespace Barotrauma.Items.Components
ciElement.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal == ciElement.ContinuousSignal);
}
customInterfaceElementList.Add(ciElement);
IsActive |= ciElement.ContinuousSignal;
IsActive |= ciElement.ContinuousSignal || ciElement.GetValueInterval > 0.0f;
}
InitProjSpecific();
@@ -348,13 +345,10 @@ namespace Barotrauma.Items.Components
//make sure the clients know about the states of the checkboxes and text fields
if (customInterfaceElementList.Any())
{
if (item.FullyInitialized)
CoroutineManager.Invoke(() =>
{
CoroutineManager.Invoke(() =>
{
if (!item.Removed) { item.CreateServerEvent(this); }
}, delay: 0.1f);
}
if (item.FullyInitialized && !item.Removed) { item.CreateServerEvent(this); }
}, delay: 0.1f);
}
#endif
}
@@ -418,6 +412,16 @@ namespace Barotrauma.Items.Components
{
foreach (CustomInterfaceElement ciElement in customInterfaceElementList)
{
if (ciElement.GetValueInterval > 0.0f)
{
ciElement.GetValueTimer -= deltaTime;
if (ciElement.GetValueTimer <= 0.0f)
{
SetSignalToPropertyValue(ciElement);
ciElement.GetValueTimer = ciElement.GetValueInterval;
}
}
if (!ciElement.ContinuousSignal && ciElement.PropertyName != "Voltage") { continue; }
//TODO: allow changing output when a tickbox is not selected
if (!string.IsNullOrEmpty(ciElement.Signal) && ciElement.Connection != null)
@@ -140,7 +140,7 @@ namespace Barotrauma.Items.Components
}
}
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates (in Hz). 0 = not at all, 1 = alternates between full brightness and off.")]
[Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f, DecimalCount = 2), Serialize(0.0f, IsPropertySaveable.Yes, description: "How much light pulsates. 0 = not at all, 1 = alternates between full brightness and off.")]
public float PulseAmount
{
get { return pulseAmount; }
@@ -268,7 +268,7 @@ namespace Barotrauma.Items.Components
public float RotationSpeedHighSkill { get; private set; }
[Serialize("0,0,0,0", IsPropertySaveable.Yes, description: "Optional screen tint color when the item is being operated (R,G,B,A)."),
Editable]
Editable(TransferToSwappedItem = true)]
public Color HudTint { get; set; }
[Header(localizedTextTag: "sp.turret.AutoOperate.propertyheader")]