Unstable 1.8.4.0

This commit is contained in:
Markus Isberg
2025-03-12 12:56:27 +00:00
parent a4c3e868e4
commit a4a3427e4e
627 changed files with 29860 additions and 10018 deletions
@@ -23,45 +23,69 @@ namespace Barotrauma
{
partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable
{
#region Lists
/// <summary>
/// A list of every item that exists somewhere in the world. Note that there can be a huge number of items in the list,
/// and you probably shouldn't be enumerating it to find some that match some specific criteria (unless that's done very, very sparsely or during initialization).
/// </summary>
public static readonly List<Item> ItemList = new List<Item>();
private static readonly HashSet<Item> dangerousItems = new HashSet<Item>();
private static readonly HashSet<Item> _dangerousItems = new HashSet<Item>();
public static IReadOnlyCollection<Item> DangerousItems { get { return dangerousItems; } }
public static IReadOnlyCollection<Item> DangerousItems => _dangerousItems;
private static readonly List<Item> repairableItems = new List<Item>();
private static readonly List<Item> _repairableItems = new List<Item>();
/// <summary>
/// Items that have one more more Repairable component
/// </summary>
public static IReadOnlyCollection<Item> RepairableItems => repairableItems;
public static IReadOnlyCollection<Item> RepairableItems => _repairableItems;
private static readonly List<Item> cleanableItems = new List<Item>();
private static readonly List<Item> _cleanableItems = new List<Item>();
/// <summary>
/// Items that may potentially need to be cleaned up (pickable, not attached to a wall, and not inside a valid container)
/// </summary>
public static IReadOnlyCollection<Item> CleanableItems => cleanableItems;
public static IReadOnlyCollection<Item> CleanableItems => _cleanableItems;
private static readonly HashSet<Item> deconstructItems = new HashSet<Item>();
private static readonly HashSet<Item> _deconstructItems = new HashSet<Item>();
/// <summary>
/// Items that have been marked for deconstruction
/// </summary>
public static HashSet<Item> DeconstructItems => deconstructItems;
public static HashSet<Item> DeconstructItems => _deconstructItems;
private static readonly List<Item> sonarVisibleItems = new List<Item>();
private static readonly List<Item> _sonarVisibleItems = new List<Item>();
/// <summary>
/// Items whose <see cref="ItemPrefab.SonarSize"/> is larger than 0
/// </summary>
public static IReadOnlyCollection<Item> SonarVisibleItems => sonarVisibleItems;
public static IReadOnlyCollection<Item> SonarVisibleItems => _sonarVisibleItems;
private static readonly List<Item> _turretTargetItems = new List<Item>();
/// <summary>
/// Items whose <see cref="ItemPrefab.IsAITurretTarget"/> is true.
/// </summary>
public static IReadOnlyCollection<Item> TurretTargetItems => _turretTargetItems;
private static readonly List<Item> _chairItems = new List<Item>();
/// <summary>
/// Items that have the tag <see cref="Tags.ChairItem"/>. Which is an oddly specific thing, but useful as an optimization for NPC AI.
/// </summary>
public static IReadOnlyCollection<Item> ChairItems => _chairItems;
#endregion
public new ItemPrefab Prefab => base.Prefab as ItemPrefab;
public override ContentPackage ContentPackage => Prefab?.ContentPackage;
public static bool ShowLinks = true;
private readonly HashSet<Identifier> tags;
private HashSet<Identifier> tags;
private readonly bool isWire, isLogic;
@@ -103,8 +127,9 @@ namespace Barotrauma
#endif
//components that determine the functionality of the item
private readonly Dictionary<Type, ItemComponent> componentsByType = new Dictionary<Type, ItemComponent>();
private readonly Dictionary<Type, List<ItemComponent>> componentsByType = new Dictionary<Type, List<ItemComponent>>();
private readonly List<ItemComponent> components;
/// <summary>
/// Components that are Active or need to be updated for some other reason (status effects, sounds)
/// </summary>
@@ -429,6 +454,10 @@ namespace Barotrauma
{
turret.UpdateLightComponents();
}
foreach (var triggerComponent in GetComponents<TriggerComponent>())
{
triggerComponent.SetPhysicsBodyPosition();
}
}
#endif
}
@@ -491,6 +520,22 @@ namespace Barotrauma
Rect = new Rectangle(rect.X, rect.Y, newWidth, newHeight);
}
//need to update to get the position of the physics body to match the new center of the item
if (body != null)
{
if (FullyInitialized)
{
//fully intialized = scaling after the item has been created
//if this happens in the editor, refresh the transform to get the rect to match the position of the physics body
if (Screen.Selected is { IsEditor: true }) { UpdateTransform(); }
}
else
{
//scaling during loading -> move the body to the new center of the rect
body.SetTransformIgnoreContacts(ConvertUnits.ToSimUnits(base.Position), body.Rotation);
}
}
if (components != null)
{
foreach (ItemComponent component in components)
@@ -821,22 +866,14 @@ namespace Barotrauma
[Editable, Serialize("", IsPropertySaveable.Yes)]
public string Tags
{
get { return string.Join(",", tags); }
get => tags.ConvertToString();
set
{
tags.Clear();
// Always add prefab tags
base.Prefab.Tags.ForEach(t => tags.Add(t));
if (!string.IsNullOrWhiteSpace(value))
{
string[] splitTags = value.Split(',');
foreach (string tag in splitTags)
{
string[] splitTag = tag.Trim().Split(':');
splitTag[0] = splitTag[0].ToLowerInvariant();
tags.Add(string.Join(":", splitTag).ToIdentifier());
}
}
// Then add new tags
tags = tags.Union(value.ToIdentifiers()).ToHashSet();
}
}
@@ -1022,7 +1059,7 @@ namespace Barotrauma
get { return allPropertyObjects; }
}
public bool IgnoreByAI(Character character) => HasTag(Barotrauma.Tags.ItemIgnoredByAI) || OrderedToBeIgnored && character.IsOnPlayerTeam;
public bool IgnoreByAI(Character character) => HasTag(Barotrauma.Tags.IgnoredByAI) || OrderedToBeIgnored && character.IsOnPlayerTeam;
public bool OrderedToBeIgnored { get; set; }
public bool HasBallastFloraInHull
@@ -1160,7 +1197,6 @@ namespace Barotrauma
body = new PhysicsBody(subElement, ConvertUnits.ToSimUnits(Position), Scale, density, collisionCategory, collidesWith, findNewContacts: false);
body.FarseerBody.AngularDamping = subElement.GetAttributeFloat("angulardamping", 0.2f);
body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f);
body.FarseerBody.LinearDamping = subElement.GetAttributeFloat("lineardamping", 0.1f);
body.UserData = this;
break;
case "trigger":
@@ -1307,9 +1343,11 @@ namespace Barotrauma
InsertToList();
ItemList.Add(this);
if (Prefab.IsDangerous) { dangerousItems.Add(this); }
if (Repairables.Any()) { repairableItems.Add(this); }
if (Prefab.SonarSize > 0.0f) { sonarVisibleItems.Add(this); }
if (Prefab.IsDangerous) { _dangerousItems.Add(this); }
if (Repairables.Any()) { _repairableItems.Add(this); }
if (Prefab.SonarSize > 0.0f) { _sonarVisibleItems.Add(this); }
if (Prefab.IsAITurretTarget) { _turretTargetItems.Add(this); }
if (Prefab.Tags.Contains(Barotrauma.Tags.ChairItem)) { _chairItems.Add(this); }
CheckCleanable();
DebugConsole.Log("Created " + Name + " (" + ID + ")");
@@ -1326,7 +1364,7 @@ namespace Barotrauma
{
conditionMultiplierCampaign *= campaign.Settings.OxygenMultiplier;
}
if (HasTag(Barotrauma.Tags.Fuel))
if (HasTag(Barotrauma.Tags.ReactorFuel))
{
conditionMultiplierCampaign *= campaign.Settings.FuelMultiplier;
}
@@ -1489,17 +1527,24 @@ namespace Barotrauma
};
Type type = component.GetType();
if (!componentsByType.ContainsKey(type))
CacheComponent(type);
Type baseType = type.BaseType;
while (baseType != null)
{
componentsByType.Add(type, component);
Type baseType = type.BaseType;
while (baseType != null && baseType != typeof(ItemComponent))
CacheComponent(baseType);
baseType = baseType.BaseType;
}
void CacheComponent(Type t)
{
if (!componentsByType.TryGetValue(t, out List<ItemComponent> cachedComponents))
{
if (!componentsByType.ContainsKey(baseType))
{
componentsByType.Add(baseType, component);
}
baseType = baseType.BaseType;
cachedComponents = new List<ItemComponent>();
componentsByType.Add(t, cachedComponents);
}
if (!cachedComponents.Contains(component))
{
cachedComponents.Add(component);
}
}
}
@@ -1536,15 +1581,11 @@ namespace Barotrauma
public T GetComponent<T>() where T : ItemComponent
{
if (componentsByType.TryGetValue(typeof(T), out ItemComponent component))
if (componentsByType.TryGetValue(typeof(T), out List<ItemComponent> matchingComponents))
{
return (T)component;
return (T)matchingComponents.First();
}
if (typeof(T) == typeof(ItemComponent))
{
return (T)components.FirstOrDefault();
}
return default;
return null;
}
public IEnumerable<T> GetComponents<T>()
@@ -1553,8 +1594,11 @@ namespace Barotrauma
{
return components.Cast<T>();
}
if (!componentsByType.ContainsKey(typeof(T))) { return Enumerable.Empty<T>(); }
return components.Where(c => c is T).Cast<T>();
if (componentsByType.TryGetValue(typeof(T), out List<ItemComponent> matchingComponents))
{
return matchingComponents.Cast<T>();
}
return Enumerable.Empty<T>();
}
public float GetQualityModifier(Quality.StatType statType)
@@ -1565,7 +1609,7 @@ namespace Barotrauma
public void RemoveContained(Item contained)
{
ownInventory?.RemoveItem(contained);
contained.Container = null;
contained.Container = null;
}
public void SetTransform(Vector2 simPosition, float rotation, bool findNewHull = true, bool setPrevTransform = true)
@@ -1590,14 +1634,7 @@ namespace Barotrauma
try
{
#endif
if (body.PhysEnabled)
{
body.SetTransform(simPosition, rotation, setPrevTransform);
}
else
{
body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform);
}
body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform);
#if DEBUG
}
catch (Exception e)
@@ -1653,14 +1690,14 @@ namespace Barotrauma
Prefab.PreferredContainers.Any() &&
(container == null || container.HasTag(Barotrauma.Tags.AllowCleanup)))
{
if (!cleanableItems.Contains(this))
if (!_cleanableItems.Contains(this))
{
cleanableItems.Add(this);
_cleanableItems.Add(this);
}
}
else
{
cleanableItems.Remove(this);
_cleanableItems.Remove(this);
}
}
@@ -1849,9 +1886,9 @@ namespace Barotrauma
public void SetContainedItemPositions()
{
foreach (ItemComponent component in components)
foreach (var ownInventory in OwnInventories)
{
(component as ItemContainer)?.SetContainedItemPositions();
ownInventory.Container.SetContainedItemPositions();
}
}
@@ -1862,7 +1899,6 @@ namespace Barotrauma
public void AddTag(Identifier tag)
{
if (tags.Contains(tag)) { return; }
tags.Add(tag);
}
@@ -1874,19 +1910,13 @@ namespace Barotrauma
public bool HasTag(Identifier tag)
{
if (tag == null) { return true; }
return tags.Contains(tag) || base.Prefab.Tags.Contains(tag);
}
public bool HasIdentifierOrTags(IEnumerable<Identifier> identifiersOrTags)
{
if (identifiersOrTags == null) { return false; }
if (identifiersOrTags.Contains(Prefab.Identifier)) { return true; }
foreach (Identifier tag in identifiersOrTags)
{
if (HasTag(tag)) { return true; }
}
return false;
return HasTag(identifiersOrTags);
}
public void ReplaceTag(string tag, string newTag)
@@ -1908,10 +1938,9 @@ namespace Barotrauma
public bool HasTag(IEnumerable<Identifier> allowedTags)
{
if (allowedTags == null) return true;
foreach (Identifier tag in allowedTags)
{
if (tags.Contains(tag)) return true;
if (HasTag(tag)) { return true; }
}
return false;
}
@@ -1936,17 +1965,46 @@ namespace Barotrauma
}
if (string.IsNullOrEmpty(conditional.TargetItemComponent))
{
if (!conditional.Matches(this)) { return false; }
return conditional.Matches(this);
}
else
{
foreach (ItemComponent component in components)
switch (conditional.ItemComponentComparison)
{
if (component.Name != conditional.TargetItemComponent) { continue; }
if (!conditional.Matches(component)) { return false; }
case PropertyConditional.LogicalOperatorType.Or:
{
foreach (ItemComponent c in components)
{
if (MatchesComponent(c, conditional) && conditional.Matches(c))
{
// Some conditional matched, which is enough.
return true;
}
}
// Didn't find any matching components.
return false;
}
case PropertyConditional.LogicalOperatorType.And:
{
bool matchingComponentFound = false;
foreach (ItemComponent c in components)
{
if (!MatchesComponent(c, conditional)) { continue; }
matchingComponentFound = true;
if (!conditional.Matches(c))
{
// Some conditional didn't match -> fail.
return false;
}
}
// Found at least one matching component and no mismatches.
return matchingComponentFound;
}
default:
throw new NotSupportedException();
}
static bool MatchesComponent(ItemComponent comp, PropertyConditional cond) => comp.Name == cond.TargetItemComponent;
}
return true;
}
/// <summary>
@@ -1994,7 +2052,7 @@ namespace Barotrauma
}
hasTargets = true;
targets.Add(containedItem);
targets.AddRange(containedItem.AllPropertyObjects);
}
}
@@ -2013,6 +2071,21 @@ namespace Barotrauma
targets.Add(serializableTarget);
}
if (effect.HasTargetType(StatusEffect.TargetType.LinkedEntities))
{
foreach (var linkedEntity in linkedTo)
{
if (linkedEntity is Item linkedItem)
{
targets.AddRange(linkedItem.AllPropertyObjects);
}
else if (linkedEntity is ISerializableEntity serializableEntity)
{
targets.Add(serializableEntity);
}
}
}
if (!hasTargets) { return; }
if (effect.HasTargetType(StatusEffect.TargetType.Hull) && CurrentHull != null)
@@ -2064,7 +2137,7 @@ namespace Barotrauma
}
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime,bool playSound = true)
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, Vector2 impulseDirection, float deltaTime, bool playSound = true)
{
if (Indestructible || InvulnerableToDamage) { return new AttackResult(); }
@@ -2466,15 +2539,15 @@ namespace Barotrauma
if (Submarine == null && prevSub != null)
{
body.SetTransform(body.SimPosition + prevSub.SimPosition, body.Rotation);
body.SetTransformIgnoreContacts(body.SimPosition + prevSub.SimPosition, body.Rotation);
}
else if (Submarine != null && prevSub == null)
{
body.SetTransform(body.SimPosition - Submarine.SimPosition, body.Rotation);
body.SetTransformIgnoreContacts(body.SimPosition - Submarine.SimPosition, body.Rotation);
}
else if (Submarine != null && prevSub != null && Submarine != prevSub)
{
body.SetTransform(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation);
body.SetTransformIgnoreContacts(body.SimPosition + prevSub.SimPosition - Submarine.SimPosition, body.Rotation);
}
if (Submarine != prevSub)
@@ -2506,7 +2579,7 @@ namespace Barotrauma
/// </summary>
private void ApplyWaterForces()
{
if (body.Mass <= 0.0f || body.Density <= 0.0f)
if (body.Mass <= 0.0f || body.Density <= 0.0f || body.BodyType != BodyType.Dynamic)
{
return;
}
@@ -2548,7 +2621,7 @@ namespace Barotrauma
//no need to apply buoyancy if the item is still and not light enough to float
if (moving || body.Density <= 10.0f)
{
Vector2 buoyancy = -GameMain.World.Gravity * forceFactor * volume * Physics.NeutralDensity;
Vector2 buoyancy = -GameMain.World.Gravity * body.FarseerBody.GravityScale * forceFactor * volume * Physics.NeutralDensity;
body.ApplyForce(buoyancy);
}
@@ -2669,9 +2742,32 @@ namespace Barotrauma
}
SetContainedItemPositions();
}
/// <summary>
/// Simpler, non-recursive version of <see cref="GetConnectedComponents{T}"/> for getting a directly connected component of the specified type wired to the connection panel.
/// </summary>
public T GetDirectlyConnectedComponent<T>(Func<Connection, bool> connectionFilter = null) where T : ItemComponent
{
ConnectionPanel connectionPanel = GetComponent<ConnectionPanel>();
if (connectionPanel == null) { return null; }
foreach (Connection c in connectionPanel.Connections)
{
if (connectionFilter != null && !connectionFilter(c)) { continue; }
foreach (Connection recipient in c.Recipients)
{
var component = recipient.Item.GetComponent<T>();
if (component != null)
{
return component;
}
}
}
return null;
}
/// <summary>
/// Note: This function generates garbage and might be a bit too heavy to be used once per frame.
/// Searches through the connection panel and looks for connections of specific type.
/// Note: This function generates garbage and is too heavy to be used on each frame.
/// </summary>
public List<T> GetConnectedComponents<T>(bool recursive = false, bool allowTraversingBackwards = true, Func<Connection, bool> connectionFilter = null) where T : ItemComponent
{
@@ -2689,7 +2785,7 @@ namespace Barotrauma
foreach (Connection c in connectionPanel.Connections)
{
if (connectionFilter != null && !connectionFilter.Invoke(c)) { continue; }
if (connectionFilter != null && !connectionFilter(c)) { continue; }
foreach (Connection recipient in c.Recipients)
{
var component = recipient.Item.GetComponent<T>();
@@ -2710,8 +2806,7 @@ namespace Barotrauma
foreach (Connection c in connectionPanel.Connections)
{
if (alreadySearched.Contains(c)) { continue; }
alreadySearched.Add(c);
if (!alreadySearched.Add(c)) { continue; }
GetConnectedComponentsRecursive(c, alreadySearched, connectedComponents, ignoreInactiveRelays, allowTraversingBackwards);
}
}
@@ -3001,8 +3096,11 @@ namespace Barotrauma
foreach (ItemComponent ic in components)
{
bool pickHit = false, selectHit = false;
if (user.IsKeyDown(InputType.Aim))
if (ic is not Ladder && user.IsKeyDown(InputType.Aim))
{
// Don't allow selecting items while aiming.
// This was added in cdc68f30. I can't remember what was the reason for it, but it might be related to accidental shots?
// However, we shouldn't disallow picking the ladders, because doing that would make the bot get stuck while trying to get on to ladders.
pickHit = false;
selectHit = false;
}
@@ -3343,7 +3441,7 @@ namespace Barotrauma
}
else if (setTransform)
{
body.SetTransform(dropper.SimPosition, 0.0f);
body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f);
}
}
}
@@ -3366,6 +3464,9 @@ namespace Barotrauma
ParentInventory = null;
}
//force updating the item's transform when it drops (making sure the current hull and submarine refresh)
transformDirty = true;
SetContainedItemPositions();
#if CLIENT
Submarine.ForceVisibilityRecheck();
@@ -3544,6 +3645,9 @@ namespace Barotrauma
var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
SerializableProperty property = extraData.SerializableProperty;
ISerializableEntity entity = extraData.Entity;
msg.WriteVariableUInt32((uint)allProperties.Count);
if (property != null)
{
if (allProperties.Count > 1)
@@ -3658,6 +3762,12 @@ namespace Barotrauma
var allProperties = inGameEditableOnly ? GetInGameEditableProperties(ignoreConditions: true) : GetProperties<Editable>();
if (allProperties.Count == 0) { return; }
int propertyCount = (int)msg.ReadVariableUInt32();
if (propertyCount != allProperties.Count)
{
throw new Exception($"Error in {nameof(ReadPropertyChange)}. The number of properties on the item \"{Prefab.Identifier}\" does not match between the server and the client. Server: {propertyCount}, client: {allProperties.Count}.");
}
int propertyIndex = 0;
if (allProperties.Count > 1)
{
@@ -3666,7 +3776,7 @@ namespace Barotrauma
if (propertyIndex >= allProperties.Count || propertyIndex < 0)
{
throw new Exception($"Error in ReadPropertyChange. Property index out of bounds (index: {propertyIndex}, property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})");
throw new Exception($"Error in {nameof(ReadPropertyChange)}. Property index out of bounds (item: {Prefab.Identifier}, index: {propertyIndex}, property count: {allProperties.Count}, in-game editable only: {inGameEditableOnly})");
}
bool allowEditing = true;
@@ -3691,7 +3801,7 @@ namespace Barotrauma
{
//items inside circuit boxes are inaccessible by "normal" means,
//but the properties can still be edited through the circuit box UI
canAccess = !cb.Locked;
canAccess = !cb.IsLocked();
}
else
{
@@ -3709,6 +3819,12 @@ namespace Barotrauma
if (type == typeof(string))
{
string val = msg.ReadString();
var editableAttribute = property.GetAttribute<Editable>();
if (editableAttribute != null && editableAttribute.MaxLength > 0 &&
val.Length > editableAttribute.MaxLength)
{
val = val.Substring(0, editableAttribute.MaxLength);
}
logValue = val;
if (allowEditing)
{
@@ -3825,7 +3941,7 @@ namespace Barotrauma
}
logPropertyChangeCoroutine = CoroutineManager.Invoke(() =>
{
GameServer.Log($"{sender.Character?.Name ?? sender.Name} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction);
GameServer.Log($"{GameServer.CharacterLogName(sender.Character)} set the value \"{property.Name}\" of the item \"{Name}\" to \"{logValue}\".", ServerLog.MessageType.ItemInteraction);
}, delay: 1.0f);
}
#endif
@@ -4005,7 +4121,7 @@ namespace Barotrauma
}
}
if (element.GetAttributeBool("markedfordeconstruction", false)) { deconstructItems.Add(item); }
if (element.GetAttributeBool("markedfordeconstruction", false)) { _deconstructItems.Add(item); }
float prevRotation = item.Rotation;
if (element.GetAttributeBool("flippedx", false)) { item.FlipX(false); }
@@ -4113,6 +4229,178 @@ namespace Barotrauma
return item;
}
/// <summary>
/// Replaces this item with another one, intended to be called on the client from network data.
/// Does not swap connected items since it is assumed that the server will send separate entity events for them.
/// </summary>
/// <param name="replacement">The item prefab to replace this one with.</param>
/// <param name="newId">ID to assign the newly created item. Should match the one created on server.</param>
private void ReplaceFromNetwork(ItemPrefab replacement, ushort newId)
=> Replace(replacement, newId: Option.Some(newId), createEntityEvent: false);
/// <summary>
/// Replaces this item with another one, creating a network event in multiplayer and
/// swapping the connected items if the replacement is a valid swap.
/// </summary>
/// <param name="replacement">The item prefab to replace this one with.</param>
public void ReplaceWithLinkedItems(ItemPrefab replacement)
=> Replace(replacement, newId: Option.None, createEntityEvent: true);
/// <summary>
/// Replaces this item with another one, inheriting properties that are meant to be inherited when swapping items like turrets.
/// Should not be called by itself but by <see cref="ReplaceWithLinkedItems(ItemPrefab)"/> and <see cref="ReplaceFromNetwork(ItemPrefab, ushort)"/>.
/// </summary>
/// <param name="replacement">The item prefab to replace this one with.</param>
/// <param name="newId">
/// ID of the new item to swap to or Option.None to automatically assign one.
/// </param>
/// <param name="createEntityEvent">
/// Should an entity event be created to notify clients about the item swap.
/// </param>
/// <remarks>
/// When ID is set to Option.None
/// the linked items will recursively be swapped to the new ones too, but not when the ID is set to a specific value.
/// this is because it is assumed that the function is run on the client when the ID is known and the server will send
/// separate entity events for the linked items.
/// </remarks>
private void Replace(ItemPrefab replacement, Option<ushort> newId, bool createEntityEvent)
{
Vector2 centerPos = Position;
var newItem = new Item(replacement, Position, Submarine, id: newId.Fallback(NullEntityID))
{
SpriteDepth = SpriteDepth,
SpriteColor = SpriteColor,
Rotation = Rotation
};
float scaleRelativeToPrefab = Scale / Prefab.Scale;
newItem.Scale *= scaleRelativeToPrefab;
if (Prefab.SwappableItem != null && replacement.SwappableItem != null)
{
Vector2 oldRelativeOrigin = (Prefab.SwappableItem.SwapOrigin - Prefab.Size / 2) * scale;
oldRelativeOrigin.Y = -oldRelativeOrigin.Y;
oldRelativeOrigin = MathUtils.RotatePoint(oldRelativeOrigin, -RotationRad);
Vector2 oldOrigin = centerPos + oldRelativeOrigin;
Vector2 relativeOrigin = (Prefab.SwappableItem.SwapOrigin - Prefab.Size / 2) * Scale;
relativeOrigin.Y = -relativeOrigin.Y;
relativeOrigin = MathUtils.RotatePoint(relativeOrigin, -RotationRad);
Vector2 origin = new Vector2(rect.X + rect.Width / 2f, rect.Y - rect.Height / 2f) + relativeOrigin;
newItem.rect.Location -= (origin - oldOrigin).ToPoint();
}
if (!string.IsNullOrEmpty(Prefab.SwappableItem?.SpawnWithId))
{
var newContainer = newItem.GetComponent<ItemContainer>();
if (newContainer != null)
{
newContainer.SpawnWithId = Prefab.SwappableItem.SpawnWithId;
}
}
foreach (ItemComponent originalComponent in components)
{
var originalComponents = components.Where(c => c.GetType() == originalComponent.GetType()).ToList();
var newComponents = newItem.components.Where(c => c.GetType() == originalComponent.GetType()).ToList();
int originalIndex = originalComponents.IndexOf(originalComponent);
if (originalIndex >= newComponents.Count)
{
//original item has components the new one doesn't -> no need to copy anything from them
continue;
}
var newComponent = newComponents[originalIndex];
foreach (var originalProperty in originalComponent.SerializableProperties)
{
if (originalProperty.Value.OverridePrefabValues || originalProperty.Value.GetAttribute<Editable>() is { TransferToSwappedItem: true })
{
newComponent.SerializableProperties[originalProperty.Key].TrySetValue(newComponent, originalProperty.Value.GetValue(originalComponent));
}
}
}
foreach (var linked in linkedTo)
{
newItem.linkedTo.Add(linked);
if (linked.linkedTo.Contains(this))
{
linked.linkedTo.Add(newItem);
}
}
var thisConnectionPanel = GetComponent<ConnectionPanel>();
var newConnectionPanel = newItem.GetComponent<ConnectionPanel>();
if (thisConnectionPanel != null && newConnectionPanel != null)
{
foreach (var connection in thisConnectionPanel.Connections)
{
var newConnection = newConnectionPanel.Connections.FirstOrDefault(c => c.Name == connection.Name);
if (newConnection == null) { continue; }
foreach (var wire in connection.Wires)
{
int connectionIndex = wire.Connections.IndexOf(connection);
wire.RemoveConnection(this);
wire.Connect(newConnection, connectionIndex, addNode: false);
newConnection.ConnectWire(wire);
}
}
}
if (newId.IsNone() && replacement.SwappableItem != null)
{
var connectedItemsToSwap = newItem.GetConnectedItemsToSwap(replacement.SwappableItem);
foreach (var kvp in connectedItemsToSwap)
{
Item itemToSwap = kvp.Key;
ItemPrefab swapTo = kvp.Value;
itemToSwap.Replace(swapTo, newId: Option.None, createEntityEvent);
}
}
#if SERVER
if (createEntityEvent && GameMain.Server is { } server)
{
server.CreateEntityEvent(this, new SwapItemEventData(newItem.Prefab, newItem.ID));
}
#endif
Remove();
}
public Dictionary<Item, ItemPrefab> GetConnectedItemsToSwap(SwappableItem swappingTo)
{
Dictionary<Item, ItemPrefab> itemsToSwap = new();
foreach (var (requiredTag, swapTo) in swappingTo.ConnectedItemsToSwap)
{
if (MapEntityPrefab.FindByIdentifier(swapTo) is not ItemPrefab replacement) { continue; }
foreach (var linked in linkedTo)
{
if (linked is Item linkedItem && linkedItem.HasTag(requiredTag))
{
itemsToSwap.Add(linkedItem, replacement);
}
}
if (GetComponent<ConnectionPanel>() is ConnectionPanel connectionPanel)
{
foreach (Connection c in connectionPanel.Connections)
{
foreach (var connectedComponent in GetConnectedComponentsRecursive<ItemComponent>(c))
{
if (!itemsToSwap.ContainsKey(connectedComponent.Item) &&
connectedComponent.Item.HasTag(requiredTag))
{
itemsToSwap.Add(connectedComponent.Item, replacement);
}
}
}
}
}
return itemsToSwap;
}
public override XElement Save(XElement parentElement)
{
XElement element = new XElement("Item");
@@ -4121,7 +4409,7 @@ namespace Barotrauma
new XAttribute("name", Prefab.OriginalName),
new XAttribute("identifier", Prefab.Identifier),
new XAttribute("ID", ID),
new XAttribute("markedfordeconstruction", deconstructItems.Contains(this)));
new XAttribute("markedfordeconstruction", _deconstructItems.Contains(this)));
if (PendingItemSwap != null)
{
@@ -4321,11 +4609,13 @@ namespace Barotrauma
private void RemoveFromLists()
{
ItemList.Remove(this);
dangerousItems.Remove(this);
repairableItems.Remove(this);
sonarVisibleItems.Remove(this);
cleanableItems.Remove(this);
deconstructItems.Remove(this);
_dangerousItems.Remove(this);
_repairableItems.Remove(this);
_sonarVisibleItems.Remove(this);
_cleanableItems.Remove(this);
_deconstructItems.Remove(this);
_turretTargetItems.Remove(this);
_chairItems.Remove(this);
RemoveFromDroppedStack(allowClientExecute: true);
}