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

This commit is contained in:
EvilFactory
2024-12-11 10:44:53 -03:00
257 changed files with 4793 additions and 1653 deletions
@@ -21,7 +21,7 @@ namespace Barotrauma.Items.Components
private Gap linkedGap;
private bool isOpen;
private float openState;
private float openState, lastOpenState;
private readonly Sprite doorSprite, weldedSprite, brokenSprite;
private readonly bool scaleBrokenSprite, fadeBrokenSprite;
private readonly bool autoOrientGap;
@@ -218,6 +218,7 @@ namespace Barotrauma.Items.Components
get { return openState; }
set
{
lastOpenState = openState;
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
#if CLIENT
float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
@@ -329,13 +330,24 @@ namespace Barotrauma.Items.Components
private readonly LocalizedString cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar";
if (IsBroken)
{
return false;
}
if (isOpen)
{
Msg = HasAccess(character) ? "ItemMsgClose" : "ItemMsgForceCloseCrowbar";
}
else
{
Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar";
}
ParseMsg();
if (addMessage)
{
msg = msg ?? (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value;
msg ??= (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value;
}
return isBroken || base.HasRequiredItems(character, addMessage, msg);
return base.HasRequiredItems(character, addMessage, msg);
}
public override bool Pick(Character picker)
@@ -461,12 +473,12 @@ namespace Barotrauma.Items.Components
if (PredictedState == null)
{
OpenState += deltaTime * (isOpen ? OpeningSpeed : -ClosingSpeed);
isClosing = openState > 0.0f && openState < 1.0f && !isOpen;
isClosing = openState is > 0.0f and < 1.0f && !isOpen;
}
else
{
OpenState += deltaTime * ((bool)PredictedState ? OpeningSpeed : -ClosingSpeed);
isClosing = openState > 0.0f && openState < 1.0f && !(bool)PredictedState;
OpenState += deltaTime * (PredictedState.Value ? OpeningSpeed : -ClosingSpeed);
isClosing = openState is > 0.0f and < 1.0f && !PredictedState.Value;
resetPredictionTimer -= deltaTime;
if (resetPredictionTimer <= 0.0f)
@@ -479,7 +491,11 @@ namespace Barotrauma.Items.Components
if (isClosing)
{
if (OpenState < 0.9f) { PushCharactersAway(); }
//server gives the clients more leeway on moving through closing doors
//latency can often otherwise make a client get blocked by a closing door server-side even if it seemed like they made it through client-side
float pushCharactersAwayThreshold = GameMain.NetworkMember is { IsServer: true } ? 0.1f : 0.9f;
if (OpenState < pushCharactersAwayThreshold) { PushCharactersAway(); }
if (CheckSubmarinesInDoorWay())
{
PredictedState = null;
@@ -771,11 +787,11 @@ namespace Barotrauma.Items.Components
{
if (IsHorizontal)
{
body.SetTransform(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
body.SetTransformIgnoreContacts(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
}
else
{
body.SetTransform(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
body.SetTransformIgnoreContacts(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
}
}
@@ -14,13 +14,15 @@ namespace Barotrauma.Items.Components
{
partial class Holdable : Pickable, IServerSerializable, IClientSerializable
{
private readonly struct EventData : IEventData
private readonly struct AttachEventData : IEventData
{
public readonly Vector2 AttachPos;
public EventData(Vector2 attachPos)
public readonly Character Attacher;
public AttachEventData(Vector2 attachPos, Character attacher)
{
AttachPos = attachPos;
Attacher = attacher;
}
}
@@ -225,6 +227,44 @@ namespace Barotrauma.Items.Components
set;
}
/// <summary>
/// For setting the handle positions using status effects
/// </summary>
public Vector2 Handle1
{
get { return ConvertUnits.ToDisplayUnits(handlePos[0]); }
set
{
handlePos[0] = ConvertUnits.ToSimUnits(value);
if (item.FlippedX)
{
handlePos[0].X = -handlePos[0].X;
}
if (!secondHandlePosDefined)
{
Handle2 = value;
}
}
}
/// <summary>
/// For setting the handle positions using status effects
/// </summary>
public Vector2 Handle2
{
get { return ConvertUnits.ToDisplayUnits(handlePos[1]); }
set
{
handlePos[1] = ConvertUnits.ToSimUnits(value);
if (item.FlippedX)
{
handlePos[1].X = -handlePos[1].X;
}
}
}
private bool secondHandlePosDefined;
public Holdable(Item item, ContentXElement element)
: base(item, element)
{
@@ -254,9 +294,14 @@ namespace Barotrauma.Items.Components
{
int index = i - 1;
string attributeName = "handle" + i;
var attribute = element.GetAttribute(attributeName);
// If no value is defind for handle2, use the value of handle1.
var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue;
Vector2 value = previousValue;
var attribute = element.GetAttribute(attributeName);
if (attribute != null)
{
secondHandlePosDefined = i > 1;
value = ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value));
}
handlePos[index] = value;
previousValue = value;
}
@@ -755,21 +800,14 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember != null)
{
if (character != Character.Controlled)
{
return false;
}
else if (GameMain.NetworkMember.IsServer)
{
return false;
}
else
{
#if CLIENT
if (character == Character.Controlled)
{
Vector2 attachPos = ConvertUnits.ToSimUnits(GetAttachPosition(character));
item.CreateClientEvent(this, new EventData(attachPos));
#endif
item.CreateClientEvent(this, new AttachEventData(attachPos, character));
}
#endif
//don't attach at this point in MP: instead rely on the network events created above
return false;
}
else
@@ -824,9 +862,13 @@ namespace Barotrauma.Items.Components
if (user.Submarine != null)
{
//we must add some "padding" to the raycast to ensure it reaches all the way to a wall
//otherwise the cursor might be outside a wall, but the grid cell it's in might be partially inside
Vector2 padding = Submarine.GridSize * new Vector2(Math.Sign(mouseDiff.X), Math.Sign(mouseDiff.Y));
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(user.Position),
ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null)
ConvertUnits.ToSimUnits(user.Position + mouseDiff + padding), collisionCategory: Physics.CollisionWall) != null)
{
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset;
@@ -420,7 +420,7 @@ namespace Barotrauma.Items.Components
Limb targetLimb = target.UserData as Limb;
Character targetCharacter = targetLimb?.character ?? target.UserData as Character;
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
Item targetItem = target.UserData as Item ?? targetFixture.UserData as Item;
Item targetItem = target.UserData is Holdable h ? h.Item : target.UserData as Item ?? targetFixture.UserData as Item;
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
GameMain.LuaCs.Hook.Call("meleeWeapon.handleImpact", this, target);
if (Attack != null)
@@ -461,10 +461,9 @@ namespace Barotrauma.Items.Components
}
#endif
}
else if (target.UserData is Holdable holdable && holdable.CanPush)
else if (target.UserData is Holdable { CanPush: true } holdable)
{
if (holdable.Item.Removed) { return; }
Attack.DoDamage(user, holdable.Item, item.WorldPosition, 1.0f);
RestoreCollision();
hitting = false;
User = null;
@@ -202,12 +202,26 @@ namespace Barotrauma.Items.Components
#if CLIENT
if (requiredTime < float.MaxValue && picker == Character.Controlled)
{
string text = string.Empty;
if (!string.IsNullOrWhiteSpace(PickingMsg))
{
text = PickingMsg;
}
else if (this is Door door)
{
text = door.IsClosed ? "progressbar.opening" : "progressbar.closing";
}
else
{
text = "progressbar.deattaching";
}
Character.Controlled?.UpdateHUDProgressBar(
this,
item.WorldPosition,
pickTimer / requiredTime,
GUIStyle.Red, GUIStyle.Green,
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
text);
}
#endif
picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
@@ -296,7 +296,9 @@ namespace Barotrauma.Items.Components
//which doesn't support multiple attached ropes (see Holdable.GetRope and the references to it)
lastProjectile?.Item.GetComponent<Rope>()?.Snap();
}
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier)) * WeaponDamageModifier;
float rangedAttackMultiplier = character?.GetStatValue(StatTypes.RangedAttackMultiplier) ?? 0;
float damageMultiplier = (1f + item.GetQualityModifier(Quality.StatType.FirepowerMultiplier) + rangedAttackMultiplier) * WeaponDamageModifier;
projectile.Launcher = item;
ignoredBodies.Clear();
@@ -306,6 +308,9 @@ namespace Barotrauma.Items.Components
{
if (l.IsSevered) { continue; }
ignoredBodies.Add(l.body.FarseerBody);
#if SERVER
ignoredBodies.Add(l.LagCompensatedBody.FarseerBody);
#endif
}
foreach (Item heldItem in character.HeldItems)
@@ -320,7 +320,7 @@ namespace Barotrauma.Items.Components
private readonly List<FireSource> fireSourcesInRange = new List<FireSource>();
private void Repair(Vector2 rayStart, Vector2 rayEnd, float deltaTime, Character user, float degreeOfSuccess, List<Body> ignoredBodies)
{
var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall;
var collisionCategories = Physics.CollisionWall | Physics.CollisionItem | Physics.CollisionLevel | Physics.CollisionRepairableWall | Physics.CollisionItemBlocking;
if (!IgnoreCharacters)
{
collisionCategories |= Physics.CollisionCharacter;
@@ -654,8 +654,9 @@ namespace Barotrauma.Items.Components
FixCharacterProjSpecific(user, deltaTime, targetLimb.character);
return true;
}
else if (targetBody.UserData is Item targetItem)
else if (targetBody.UserData is Barotrauma.Item or Holdable)
{
Item targetItem = targetBody.UserData is Holdable holdable ? holdable.Item : (Item)targetBody.UserData;
if (!HitItems || !targetItem.IsInteractable(user)) { return false; }
var levelResource = targetItem.GetComponent<LevelResource>();
@@ -77,6 +77,9 @@ namespace Barotrauma.Items.Components
public readonly ContentXElement originalElement;
/// <summary>
/// The default delay for delayed client-side corrections (see <see cref="StartDelayedCorrection"/>.
/// </summary>
protected const float CorrectionDelay = 1.0f;
protected CoroutineHandle delayedCorrectionCoroutine;
@@ -669,8 +672,7 @@ namespace Barotrauma.Items.Components
#endif
}
protected string GetTextureDirectory(ContentXElement subElement)
=> subElement.DoesAttributeReferenceFileNameAlone("texture") ? Path.GetDirectoryName(item.Prefab.FilePath) : string.Empty;
protected string GetTextureDirectory(ContentXElement subElement) => item.Prefab.GetTexturePath(subElement, item.Prefab.ParentPrefab);
public bool HasRequiredSkills(Character character)
{
@@ -108,7 +108,7 @@ namespace Barotrauma.Items.Components
[Serialize(100, IsPropertySaveable.No, description: "How many items are placed in a row before starting a new row.")]
public int ItemsPerRow { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected.")]
[Serialize(true, IsPropertySaveable.No, description: "Should the inventory of this item be visible when the item is selected. Note that this does not prevent dragging and dropping items to the item.")]
public bool DrawInventory
{
get;
@@ -923,6 +923,8 @@ namespace Barotrauma.Items.Components
#warning There's some code duplication here and in DrawContainedItems() method, but it's not straightforward to get rid of it, because of slightly different logic and the usage of draw positions vs. positions etc. Should probably be splitted into smaller methods.
public void SetContainedItemPositions()
{
if (containedItems.Count == 0) { return; }
var rootBody = item.RootContainer?.body ?? item.body;
Vector2 transformedItemPos = GetContainedPosition(
@@ -989,8 +991,7 @@ namespace Barotrauma.Items.Components
rotation += -item.RotationRad;
}
contained.Item.body.FarseerBody.SetTransformIgnoreContacts(ref simPos, rotation);
contained.Item.body.SetPrevTransform(contained.Item.body.SimPosition, contained.Item.body.Rotation);
contained.Item.body.UpdateDrawPosition();
contained.Item.body.UpdateDrawPosition(interpolate: false);
}
catch (Exception e)
{
@@ -220,6 +220,10 @@ namespace Barotrauma.Items.Components
/// </summary>
private bool forceSelectNextFrame;
private float userCanInteractCheckTimer;
private const float UserCanInteractCheckInterval = 1.0f;
public override void Update(float deltaTime, Camera cam)
{
this.cam = cam;
@@ -238,13 +242,15 @@ namespace Barotrauma.Items.Components
}
forceSelectNextFrame = false;
userCanInteractCheckTimer -= deltaTime;
if (user == null
|| user.Removed
|| !user.IsAnySelectedItem(item)
|| (item.ParentInventory != null && !IsAttachedUser(user))
|| !user.CanInteractWith(item)
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater))
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater)
|| !CheckUserCanInteract())
{
if (user != null)
{
@@ -368,6 +374,22 @@ namespace Barotrauma.Items.Components
}
}
private bool CheckUserCanInteract()
{
//optimization: CanInteractWith is relatively heavy (can involve visibility checks for example), let's not do it every frame
if (user != null)
{
if (userCanInteractCheckTimer <= 0.0f)
{
userCanInteractCheckTimer = UserCanInteractCheckInterval;
return user.CanInteractWith(item);
}
}
//we only do the actual check every UserCanInteractCheckInterval seconds
//can mean the component can stay selected for <1s after the user no longer has access to it
return true;
}
private double lastUsed;
public override bool Use(float deltaTime, Character activator = null)
@@ -499,12 +499,13 @@ namespace Barotrauma.Items.Components
{
GameAnalyticsManager.AddDesignEvent("ItemFabricated:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + fabricatedItem.TargetItem.Identifier);
}
InvSlotType invSlot = fabricatedItem.MoveToSlot;
if (i < amountFittingContainer)
{
Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, outputContainer.Inventory, fabricatedItem.TargetItem.Health * outCondition, quality,
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
onItemSpawned(spawnedItem, tempUser, invSlot);
spawnedItem.Quality = quality;
spawnedItem.StolenDuringRound = ingredientsStolen;
spawnedItem.AllowStealing = ingredientsAllowStealing;
@@ -517,7 +518,7 @@ namespace Barotrauma.Items.Components
Entity.Spawner.AddItemToSpawnQueue(fabricatedItem.TargetItem, item.Position, item.Submarine, fabricatedItem.TargetItem.Health * outCondition, quality,
onSpawned: (Item spawnedItem) =>
{
onItemSpawned(spawnedItem, tempUser);
onItemSpawned(spawnedItem, tempUser, invSlot);
spawnedItem.Quality = quality;
spawnedItem.StolenDuringRound = ingredientsStolen;
spawnedItem.AllowStealing = ingredientsAllowStealing;
@@ -527,15 +528,28 @@ namespace Barotrauma.Items.Components
}
}
void onItemSpawned(Item spawnedItem, Character user)
void onItemSpawned(Item spawnedItem, Character user, InvSlotType slot)
{
if (user != null && user.TeamID != CharacterTeamType.None)
CharacterTeamType teamID = CharacterTeamType.None;
if (user != null)
{
teamID = user.TeamID;
}
else if (item.Submarine != null)
{
teamID = item.Submarine.TeamID;
}
if (teamID != CharacterTeamType.None)
{
foreach (WifiComponent wifiComponent in spawnedItem.GetComponents<WifiComponent>())
{
wifiComponent.TeamID = user.TeamID;
wifiComponent.TeamID = teamID;
}
}
if (slot != InvSlotType.None)
{
user?.Inventory.TryPutItem(spawnedItem, user, slot.ToEnumerable());
}
OnItemFabricated?.Invoke(spawnedItem, user);
}
if (user?.Info != null && !user.Removed)
@@ -562,7 +576,6 @@ namespace Barotrauma.Items.Components
StartFabricating(prevFabricatedItem, prevUser, addToServerLog: false);
}
}
}
/// <summary>
@@ -80,6 +80,7 @@ namespace Barotrauma.Items.Components
private set
{
if (lastUser == value) { return; }
if (Screen.Selected.IsEditor) { return; }
lastUser = value;
if (lastUser == null)
{
@@ -246,6 +247,13 @@ namespace Barotrauma.Items.Components
}
}
//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 })
{
UpdateAutoTemp(100.0f, (float)(Timing.Step * 10.0f));
}
#if CLIENT
if (PowerOn && AvailableFuel < 1)
{
@@ -8,6 +8,8 @@ namespace Barotrauma.Items.Components
{
partial class Sonar : Powered, IServerSerializable, IClientSerializable
{
public static List<Sonar> SonarList = new List<Sonar>();
public enum Mode
{
Active,
@@ -167,6 +169,7 @@ namespace Barotrauma.Items.Components
IsActive = true;
InitProjSpecific(element);
CurrentMode = Mode.Passive;
SonarList.Add(this);
}
partial void InitProjSpecific(ContentXElement element);
@@ -379,6 +382,29 @@ namespace Barotrauma.Items.Components
}
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
#if CLIENT
sonarBlip?.Remove();
pingCircle?.Remove();
directionalPingCircle?.Remove();
screenOverlay?.Remove();
screenBackground?.Remove();
lineSprite?.Remove();
foreach (var t in targetIcons.Values)
{
t.Item1.Remove();
}
targetIcons.Clear();
MineralClusters = null;
#endif
SonarList.Remove(this);
}
public void ServerEventRead(IReadMessage msg, Client c)
{
bool isActive = msg.ReadBoolean();
@@ -232,7 +232,7 @@ namespace Barotrauma.Items.Components
float maxOverVoltage = Math.Max(OverloadVoltage, 1.0f);
Overload = Voltage > maxOverVoltage;
Overload = Voltage > maxOverVoltage && GameMain.GameSession is not { RoundDuration: < 5 };
if (Overload && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
@@ -157,6 +157,13 @@ namespace Barotrauma.Items.Components
{
if (powerOut?.Grid != null) { return powerOut.Grid.Voltage; }
}
if (this is PowerTransfer && item.Condition <= 0.0f)
{
//if the junction box or other power transfer device is broken,
//it cannot be supplying any power (voltage = 0)
return 0.0f;
}
return PowerConsumption <= 0.0f ? 1.0f : voltage;
}
set
@@ -440,6 +440,14 @@ namespace Barotrauma.Items.Components
//can't launch if already launched
if (StickTarget != null || IsActive) { return false; }
#if SERVER
var owner = GameMain.Server.ConnectedClients.FirstOrDefault(c => c.Character == User);
if (owner != null)
{
Limb.SetLagCompensatedBodyPositions(owner);
}
#endif
float initialRotation = item.body.Rotation;
//if the item is being launched from an inventory, assume it's being fired by a gun that handles setting the rotation correctly
//but if the item is e.g. being thrown by a character, we need to take the direction into account
@@ -461,10 +469,10 @@ namespace Barotrauma.Items.Components
spreadIndex++;
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
Vector2 prevSimpos = item.SimPosition;
item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle);
if (Hitscan)
{
Vector2 prevSimpos = item.SimPosition;
item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle);
DoHitscan(launchDir);
if (i < HitScanCount - 1)
{
@@ -473,7 +481,6 @@ namespace Barotrauma.Items.Components
}
else
{
item.body.SetTransform(item.body.SimPosition, launchAngle);
float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
DoLaunch(launchDir * modifiedLaunchImpulse);
}
@@ -670,8 +677,6 @@ namespace Barotrauma.Items.Components
}
if (fixture.Body.UserData is VineTile) { return true; }
if (fixture.CollidesWith == Category.None) { return true; }
//only collides with characters = probably an "outsideCollisionBlocker" created by a gap
if (fixture.CollidesWith == Physics.CollisionCharacter) { return true; }
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body.UserData is Hull || fixture.UserData is Hull) { return true; }
@@ -690,6 +695,11 @@ namespace Barotrauma.Items.Components
if (item.Condition <= 0) { return true; }
if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return true; }
}
else if (fixture.Body.UserData is Gap)
{
//an "outsideCollisionBlocker" created by a gap, should never collide
return true;
}
else if (fixture.Body.UserData is Holdable { CanPush: false })
{
// Ignore holdables that can't push -> shouldn't block
@@ -724,14 +734,17 @@ namespace Barotrauma.Items.Components
return -1;
}
if (fixture.Body.UserData is VineTile) { return -1; }
if (fixture.CollidesWith == Category.None) { return -1; }
//only collides with characters = probably an "outsideCollisionBlocker" created by a gap
if (fixture.CollidesWith == Physics.CollisionCharacter) { return -1; }
if (fixture.CollidesWith == Category.None && fixture.CollisionCategories != Physics.CollisionLagCompensationBody) { return -1; }
if (fixture.Body.UserData is Item item)
{
if (item.Condition <= 0) { return -1; }
if (!item.Prefab.DamagedByProjectiles && item.GetComponent<Door>() == null) { return -1; }
}
else if (fixture.Body.UserData is Gap)
{
//an "outsideCollisionBlocker" created by a gap, should never collide
return -1;
}
if (fixture.Body.UserData as string == "ruinroom" || fixture.Body?.UserData is Hull || fixture.UserData is Hull) { return -1; }
//if doing the raycast in a submarine's coordinate space, ignore anything that's not in that sub
@@ -779,7 +792,7 @@ namespace Barotrauma.Items.Components
hits.Add(new HitscanResult(fixture, point, normal, fraction));
return 1;
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile);
}, rayStart, rayEnd, Physics.CollisionCharacter | Physics.CollisionWall | Physics.CollisionLevel | Physics.CollisionItemBlocking | Physics.CollisionProjectile | Physics.CollisionLagCompensationBody);
return hits;
}
@@ -15,6 +15,9 @@ namespace Barotrauma.Items.Components
private Vector2 detectOffset;
private float updateTimer;
[Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
public bool MotionDetected { get; set; }
[Flags]
public enum TargetType
@@ -26,14 +29,25 @@ namespace Barotrauma.Items.Components
Any = Human | Monster | Wall | Pet,
}
[Serialize(false, IsPropertySaveable.No, description: "Has the item currently detected movement. Intended to be used by StatusEffect conditionals (setting this value in XML has no effect).")]
public bool MotionDetected { get; set; }
private bool triggerFromHumans = true;
private bool triggerFromPets = true;
private bool triggerFromMonsters = true;
private TargetType _target;
[InGameEditable, Serialize(TargetType.Any, IsPropertySaveable.Yes, description: "Which kind of targets can trigger the sensor?", alwaysUseInstanceValues: true)]
public TargetType Target
{
get;
set;
get => _target;
set
{
if (_target != value)
{
_target = value;
triggerFromHumans = Target.HasFlag(TargetType.Human);
triggerFromPets = Target.HasFlag(TargetType.Pet);
triggerFromMonsters = Target.HasFlag(TargetType.Monster);
}
}
}
[Editable, Serialize("", IsPropertySaveable.Yes, description: "Does the sensor react only to certain characters (species names, groups or tags)? Doesn't have an effect, if the Target Type is incorrect.", alwaysUseInstanceValues: true)]
@@ -263,10 +277,7 @@ namespace Barotrauma.Items.Components
}
}
}
bool triggerFromHumans = Target.HasFlag(TargetType.Human);
bool triggerFromPets = Target.HasFlag(TargetType.Pet);
bool triggerFromMonsters = Target.HasFlag(TargetType.Monster);
bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
if (!hasTriggers) { return; }
foreach (Character character in Character.CharacterList)
@@ -299,9 +310,6 @@ namespace Barotrauma.Items.Components
public bool TriggersOn(Character character)
{
bool triggerFromHumans = Target.HasFlag(TargetType.Human);
bool triggerFromPets = Target.HasFlag(TargetType.Pet);
bool triggerFromMonsters = Target.HasFlag(TargetType.Monster);
bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
if (!hasTriggers) { return false; }
return TriggersOn(character, triggerFromHumans, triggerFromPets, triggerFromMonsters);
@@ -80,7 +80,6 @@ namespace Barotrauma.Items.Components
set
{
isOn = value;
CanTransfer = value;
if (!isOn)
{
currPowerConsumption = 0.0f;
@@ -96,8 +96,20 @@ namespace Barotrauma.Items.Components
[Editable, Serialize("> ", IsPropertySaveable.Yes)]
public string LineStartSymbol { get; set; }
[Editable, Serialize(false, IsPropertySaveable.No)]
public bool Readonly { get; set; }
private bool _readonly;
[Editable, Serialize(false, IsPropertySaveable.Yes)]
public bool Readonly
{
get => _readonly;
set
{
_readonly = value;
#if CLIENT
RefreshInputElements();
#endif
}
}
[Serialize(true, IsPropertySaveable.No)]
public bool AutoScrollToBottom { get; set; }
@@ -231,17 +231,22 @@ namespace Barotrauma.Items.Components
{
var should = GameMain.LuaCs.Hook.Call<bool?>("wifiSignalTransmitted", this, signal, sentFromChat);
if (should != null && should.Value)
return;
if (sentFromChat)
{
item.LastSentSignalRecipients.Clear();
}
if (should != null && should.Value) { return; }
bool chatMsgSent = false;
var receivers = GetReceiversInRange();
if (sentFromChat)
{
//if sent from chat, we need to reset the "signal chain" at this point
//so we can correctly detect which components the signal has already passed through to avoid infinite loops
//only relevant for signals originating from the chat - normally this is handled in Item.SendSignal
item.LastSentSignalRecipients.Clear();
foreach (WifiComponent receiver in receivers)
{
receiver.item.LastSentSignalRecipients.Clear();
}
}
foreach (WifiComponent wifiComp in receivers)
{
if (sentFromChat && !wifiComp.LinkToChat) { continue; }
@@ -1051,7 +1051,7 @@ namespace Barotrauma.Items.Components
}
if (TargetItems)
{
foreach (Item targetItem in Item.ItemList)
foreach (Item targetItem in Item.TurretTargetItems)
{
if (!IsValidTarget(targetItem)) { continue; }
float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority;
@@ -1395,7 +1395,7 @@ namespace Barotrauma.Items.Components
closestDistance = dist / priority;
currentTarget = closestEnemy;
}
foreach (Item targetItem in Item.ItemList)
foreach (Item targetItem in Item.TurretTargetItems)
{
if (!IsValidTarget(targetItem)) { continue; }
float priority = isSlowTurret ? targetItem.Prefab.AISlowTurretPriority : targetItem.Prefab.AITurretPriority;
@@ -1767,8 +1767,15 @@ namespace Barotrauma.Items.Components
Submarine sub = e.Submarine ?? e as Submarine;
if (sub == null) { return true; }
if (sub == Item.Submarine) { return false; }
if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon) { return false; }
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
if (sub.Info.IsOutpost || sub.Info.IsWreck || sub.Info.IsBeacon || sub.Info.IsRuin) { return false; }
if (item.Submarine == null)
{
if (sub.TeamID == FriendlyTeam) { return false; }
}
else
{
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
}
}
else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true })
{
@@ -1786,6 +1793,8 @@ namespace Barotrauma.Items.Components
customPredicate: (Fixture f) =>
{
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
if (f.CollidesWith == Physics.CollisionNone) { return false; }
if (f.Body.UserData == item) { return false; }
if (f.UserData is Hull) { return false; }
return !item.StaticFixtures.Contains(f);
});
@@ -165,10 +165,11 @@ namespace Barotrauma
{
if (element.DoesAttributeReferenceFileNameAlone("texture"))
{
var basePrefab = WearableComponent.Item.Prefab.ParentPrefab ?? WearableComponent.Item.Prefab;
string textureName = element.GetAttributeString("texture", "");
return ContentPath.FromRaw(
element.ContentPackage,
$"{Path.GetDirectoryName(WearableComponent.Item.Prefab.FilePath)}/{textureName}");
$"{Path.GetDirectoryName(basePrefab.FilePath)}/{textureName}");
}
else
{
@@ -331,13 +331,16 @@ namespace Barotrauma
}
#endif
}
public IEnumerable<Item> GetAllItems(bool checkForDuplicates)
{
for (int i = 0; i < capacity; i++)
{
foreach (var item in slots[i].Items)
var items = slots[i].Items;
// ReSharper disable once ForCanBeConvertedToForeach, because this is performance-sensitive code.
for (int j = 0; j < items.Count; j++)
{
var item = items[j];
if (item == null)
{
#if DEBUG
@@ -349,9 +352,9 @@ namespace Barotrauma
if (checkForDuplicates)
{
bool duplicateFound = false;
for (int j = 0; j < i; j++)
for (int s = 0; s < i; s++)
{
if (slots[j].Items.Contains(item))
if (slots[s].Items.Contains(item))
{
duplicateFound = true;
break;
@@ -364,7 +367,7 @@ namespace Barotrauma
yield return item;
}
}
}
}
}
private void NotifyItemComponentsOfChange()
@@ -420,12 +423,17 @@ namespace Barotrauma
return null;
}
private bool IsIndexInRange(int index)
{
return index >= 0 && index < slots.Length;
}
/// <summary>
/// Get the item stored in the specified inventory slot. If the slot contains a stack of items, returns the first item in the stack.
/// </summary>
public Item GetItemAt(int index)
{
if (index < 0 || index >= slots.Length) { return null; }
if (!IsIndexInRange(index)) { return null; }
return slots[index].FirstOrDefault();
}
@@ -434,14 +442,13 @@ namespace Barotrauma
/// </summary>
public IEnumerable<Item> GetItemsAt(int index)
{
if (index < 0 || index >= slots.Length) { return Enumerable.Empty<Item>(); }
if (!IsIndexInRange(index)) { return Enumerable.Empty<Item>(); }
return slots[index].Items;
}
public int GetItemStackSlotIndex(Item item, int index)
{
if (index < 0 || index >= slots.Length) { return -1; }
if (!IsIndexInRange(index)) { return -1; }
return slots[index].Items.IndexOf(item);
}
@@ -476,7 +483,7 @@ namespace Barotrauma
public virtual bool ItemOwnsSelf(Item item)
{
if (Owner == null) { return false; }
if (!(Owner is Item)) { return false; }
if (Owner is not Item) { return false; }
Item ownerItem = Owner as Item;
if (ownerItem == item) { return true; }
if (ownerItem.ParentInventory == null) { return false; }
@@ -519,7 +526,7 @@ namespace Barotrauma
public virtual bool CanBePutInSlot(Item item, int i, bool ignoreCondition = false)
{
if (ItemOwnsSelf(item)) { return false; }
if (i < 0 || i >= slots.Length) { return false; }
if (!IsIndexInRange(i)) { return false; }
return slots[i].CanBePut(item, ignoreCondition);
}
@@ -539,7 +546,7 @@ namespace Barotrauma
public virtual bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition = null, int? quality = null)
{
if (i < 0 || i >= slots.Length) { return false; }
if (!IsIndexInRange(i)) { return false; }
return slots[i].CanProbablyBePut(itemPrefab, condition, quality);
}
@@ -555,7 +562,7 @@ namespace Barotrauma
public virtual int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot = false)
{
if (i < 0 || i >= slots.Length) { return 0; }
if (!IsIndexInRange(i)) { return 0; }
return slots[i].HowManyCanBePut(itemPrefab, condition: condition, ignoreItemsInSlot: ignoreItemsInSlot);
}
@@ -573,7 +580,7 @@ namespace Barotrauma
public virtual bool TryPutItem(Item item, int i, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true, bool ignoreCondition = false)
{
if (i < 0 || i >= slots.Length)
if (!IsIndexInRange(i))
{
string thisItemStr = item?.Prefab.Identifier.Value ?? "null";
string ownerStr = "null";
@@ -637,7 +644,7 @@ namespace Barotrauma
protected virtual void PutItem(Item item, int i, Character user, bool removeItem = true, bool createNetworkEvent = true)
{
if (i < 0 || i >= slots.Length)
if (!IsIndexInRange(i))
{
string errorMsg = "Inventory.PutItem failed: index was out of range(" + i + ").\n" + Environment.StackTrace.CleanupStackTrace();
GameAnalyticsManager.AddErrorEventOnce("Inventory.PutItem:IndexOutOfRange", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
@@ -1097,10 +1104,16 @@ namespace Barotrauma
public bool IsInSlot(Item item, int index)
{
if (index < 0 || index >= slots.Length) { return false; }
if (!IsIndexInRange(index)) { return false; }
return slots[index].Contains(item);
}
public bool IsSlotEmpty(int index)
{
if (!IsIndexInRange(index)) { return false; }
return slots[index].Empty();
}
public void SharedRead(IReadMessage msg, List<ushort>[] receivedItemIds, out bool readyToApply)
{
byte start = msg.ReadByte();
@@ -24,42 +24,66 @@ 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 HashSet<Identifier> tags;
@@ -104,8 +128,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>
@@ -496,6 +521,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)
@@ -1303,9 +1344,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 + ")");
@@ -1487,17 +1530,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);
}
}
}
@@ -1534,15 +1584,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>()
@@ -1551,8 +1597,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)
@@ -1563,7 +1612,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)
@@ -1588,14 +1637,7 @@ namespace Barotrauma
try
{
#endif
if (!body.PhysEnabled || Submarine.Unloading)
{
body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform);
}
else
{
body.SetTransform(simPosition, rotation, setPrevTransform);
}
body.SetTransformIgnoreContacts(simPosition, rotation, setPrevTransform);
#if DEBUG
}
catch (Exception e)
@@ -1651,14 +1693,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);
}
}
@@ -1847,9 +1889,9 @@ namespace Barotrauma
public void SetContainedItemPositions()
{
foreach (ItemComponent component in components)
foreach (var ownInventory in OwnInventories)
{
(component as ItemContainer)?.SetContainedItemPositions();
ownInventory.Container.SetContainedItemPositions();
}
}
@@ -2508,15 +2550,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)
@@ -3416,7 +3458,7 @@ namespace Barotrauma
}
else if (setTransform)
{
body.SetTransform(dropper.SimPosition, 0.0f);
body.SetTransformIgnoreContacts(dropper.SimPosition, 0.0f);
}
}
}
@@ -4091,7 +4133,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); }
@@ -4379,7 +4421,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)
{
@@ -4583,11 +4625,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);
}
@@ -223,6 +223,7 @@ namespace Barotrauma
public readonly int Amount;
public readonly int? Quality;
public readonly bool HideForNonTraitors;
public readonly InvSlotType MoveToSlot;
/// <summary>
/// How many of this item the fabricator can create (< 0 = unlimited)
@@ -257,6 +258,7 @@ namespace Barotrauma
FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault);
HideForNonTraitors = element.GetAttributeBool(nameof(HideForNonTraitors), false);
MoveToSlot = element.GetAttributeEnum(nameof(MoveToSlot), InvSlotType.None);
if (element.GetAttribute(nameof(Quality)) != null)
{
@@ -1000,7 +1002,7 @@ namespace Barotrauma
ParseConfigElement(variantOf: null);
}
private string GetTexturePath(ContentXElement subElement, ItemPrefab variantOf)
public string GetTexturePath(ContentXElement subElement, ItemPrefab variantOf)
=> subElement.DoesAttributeReferenceFileNameAlone("texture")
? Path.GetDirectoryName(variantOf?.ContentFile.Path ?? ContentFile.Path)
: "";