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
@@ -30,13 +30,18 @@ namespace Barotrauma
private readonly Character character;
/// <summary>
/// Slot type for each inventory slot. Vanilla package has one type for each slot,
/// although it is technically possible to have multiple types for a single slot.
/// </summary>
public InvSlotType[] SlotTypes
{
get;
private set;
}
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType>() { InvSlotType.Any };
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType> { InvSlotType.Any };
public static readonly List<InvSlotType> BagSlot = new List<InvSlotType> { InvSlotType.Bag };
public static bool IsHandSlotType(InvSlotType s) => s.HasFlag(InvSlotType.LeftHand) || s.HasFlag(InvSlotType.RightHand);
@@ -356,31 +361,40 @@ namespace Barotrauma
}
if (item.GetComponent<Pickable>() == null || item.AllowedSlots.None()) { return false; }
bool inSuitableSlot = false;
bool inWrongSlot = false;
int currentSlot = -1;
for (int i = 0; i < capacity; i++)
bool inWrongSlot = false;
bool inSuitableSlot = false;
// verify item's current placement
for (int slotIndex = 0; slotIndex < capacity; slotIndex++)
{
if (slots[i].Contains(item))
if (!slots[slotIndex].Contains(item)) { continue; }
// item is at least in this slot, can be in many
currentSlot = slotIndex;
var firstMatchingSlotType = allowedSlots.FirstOrDefault(slot => slot.HasFlag(SlotTypes[slotIndex]));
if (firstMatchingSlotType == default) // if (firstMatchingSlotType == InvSlotType.None)
{
currentSlot = i;
if (allowedSlots.Any(a => a.HasFlag(SlotTypes[i])))
inWrongSlot = true;
break;
}
inSuitableSlot = true;
// can have more than one flag, such as InvSlotType.InnerClothes | InvSlotType.OuterClothes
var individualFlags = EnumExtensions.GetIndividualFlags(firstMatchingSlotType);
// if item is not in ALL required slot types
foreach (var flag in individualFlags)
{
if (flag == InvSlotType.None) { continue; }
if (!IsInLimbSlot(item, flag))
{
if ((SlotTypes[i] == InvSlotType.RightHand || SlotTypes[i] == InvSlotType.LeftHand) && !allowedSlots.Contains(SlotTypes[i]))
{
//allowed slot = InvSlotType.RightHand | InvSlotType.LeftHand
// -> make sure the item is in both hand slots
inSuitableSlot = IsInLimbSlot(item, InvSlotType.RightHand) && IsInLimbSlot(item, InvSlotType.LeftHand);
}
else
{
inSuitableSlot = true;
}
}
else if (!allowedSlots.Any(a => a.HasFlag(SlotTypes[i])))
{
inWrongSlot = true;
inSuitableSlot = false;
break;
}
}
}
@@ -447,6 +461,8 @@ namespace Barotrauma
return placedInSlot > -1;
}
public bool IsAnySlotAvailable(Item item) => GetFreeAnySlot(item, inWrongSlot: false) > -1;
@@ -335,7 +335,7 @@ namespace Barotrauma.Items.Components
item.CreateServerEvent(this);
}
#else
if (GameMain.Client != null && GameMain.Client.MidRoundSyncing &&
if (GameMain.Client != null && GameMain.Client.MidRoundSyncing && Submarine.MainSub != null &&
(item.Submarine == Submarine.MainSub || DockingTarget.item.Submarine == Submarine.MainSub))
{
Screen.Selected.Cam.Position = Submarine.MainSub.WorldPosition;
@@ -1224,11 +1224,10 @@ namespace Barotrauma.Items.Components
#if CLIENT
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
{
return;
}
if (GameMain.GameSession?.Campaign != null && !CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap))
{
return;
if (GameMain.GameSession?.Campaign != null && !CampaignMode.AllowedToManageCampaign(ClientPermissions.ManageMap))
{
return;
}
}
#endif
@@ -1254,9 +1253,12 @@ namespace Barotrauma.Items.Components
if (newDockedState != wasDocked)
{
bool tryingToToggleOutpostDocking = docked ?
DockingTarget?.Item?.Submarine?.Info?.IsOutpost ?? false :
FindAdjacentPort()?.Item?.Submarine?.Info?.IsOutpost ?? false;
var targetPort = docked ? DockingTarget : FindAdjacentPort();
bool tryingToToggleOutpostDocking =
item.Submarine is { Info.IsOutpost: false } &&
//check that the "parent submarine of the submarine" is not an outpost (that this isn't a shuttle/elevator that's part of an outpost)
item.Submarine?.Submarine is not { Info.IsOutpost: true } &&
targetPort is { item.Submarine.Info.IsOutpost: true };
//trying to dock/undock from an outpost and the signal was sent by some automated system instead of a character
// -> ask if the player really wants to dock/undock to prevent a softlock if someone's wired the docking port
// in a way that makes always makes it dock/undock immediately at the start of the roun
@@ -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;
@@ -59,6 +59,8 @@ namespace Barotrauma.Items.Components
}
}
public bool IgnoreSignals { get; private set; }
//how much "less stuck" partially doors get when opened
const float StuckReductionOnOpen = 30.0f;
@@ -216,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;
@@ -327,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)
@@ -350,6 +364,7 @@ namespace Barotrauma.Items.Components
if (!HasAccess(picker))
{
ToggleState(ActionType.OnPicked, picker);
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
}
return false;
}
@@ -458,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)
@@ -476,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;
@@ -707,6 +726,7 @@ namespace Barotrauma.Items.Components
foreach (Character c in Character.CharacterList)
{
if (!c.Enabled) { continue; }
if (c.SelectedItem?.GetComponent<Controller>() is { } controller && controller.IsAttachedUser(c)) { continue; }
if (!MathUtils.IsValid(c.SimPosition))
{
if (!characterPosErrorShown.Contains(c))
@@ -767,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);
}
}
@@ -797,23 +817,20 @@ namespace Barotrauma.Items.Components
}
partial void OnFailedToOpen();
public override bool HasAccess(Character character)
{
if (!item.IsInteractable(character)) { return false; }
if (HasIntegratedButtons)
{
return base.HasAccess(character);
}
else
{
return base.HasAccess(character) && Item.GetConnectedComponents<Controller>(true).Any(b => b.HasAccess(character));
}
if (!base.HasAccess(character)) { return false; }
if (HasIntegratedButtons) { return true; }
var buttons = Item.GetConnectedComponents<Controller>(recursive: true);
// If there are no buttons, and we can access the door, treat it accessible. Might be controlled by some mechanism, such as motion sensor.
return buttons.None() || buttons.Any(b => b.HasAccess(character));
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (IsStuck || IsJammed) { return; }
if (IsStuck || IsJammed || IgnoreSignals) { return; }
bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value;
@@ -172,7 +172,7 @@ namespace Barotrauma.Items.Components
if (item.Connections == null)
{
//no connections and can't be wired = must be powered by something like batteries
hasPower = Voltage > MinVoltage;
hasPower = HasPower;
}
else
{
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Linq;
@@ -70,6 +70,19 @@ namespace Barotrauma.Items.Components
[Editable, Serialize(false, IsPropertySaveable.Yes, "")]
public bool PreloadCharacter { get; set; }
[Editable, Serialize(false, IsPropertySaveable.Yes, "Should the \"spawn monsters\" setting affect this item in the PvP mode?")]
public bool AffectedByPvPSpawnMonstersSetting { get; set; }
/// <summary>
/// Implemented as a property and checked on the fly instead of disabling the component,
/// because the signals sent by the component might be necessary even if it can't spawn anything.
/// </summary>
private bool DisabledByByPvPSpawnMonstersSetting =>
!SpeciesName.IsNullOrEmpty() &&
AffectedByPvPSpawnMonstersSetting &&
GameMain.GameSession?.GameMode is PvPMode &&
GameMain.NetworkMember is { ServerSettings.PvPSpawnMonsters: false };
private float spawnTimer;
private float? spawnTimerGoal;
@@ -114,15 +127,24 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated)
if (DisabledByByPvPSpawnMonstersSetting)
{
SpawnCharacter(Vector2.Zero, onSpawn: (Character c) =>
CanSpawn = false;
//in most cases we could probably just disable the component here and return,
//but the state_out signal might be needed for something even if the spawning is disabled
}
else
{
if (PreloadCharacter && !Screen.Selected.IsEditor && !preloadInitiated)
{
preloadedCharacter = c;
c.DisabledByEvent = true;
});
preloadInitiated = true;
return;
SpawnCharacter(Vector2.Zero, onSpawn: (Character c) =>
{
preloadedCharacter = c;
c.DisabledByEvent = true;
});
preloadInitiated = true;
return;
}
}
base.Update(deltaTime, cam);
@@ -182,7 +204,7 @@ namespace Barotrauma.Items.Components
private bool CanSpawnMore()
{
if (!CanSpawn) { return false; }
if (!CanSpawn || DisabledByByPvPSpawnMonstersSetting) { return false; }
if (MaximumAmount > 0 && spawnedAmount >= MaximumAmount) { return false; }
if (OnlySpawnWhenCrewInRange)
@@ -300,7 +322,7 @@ namespace Barotrauma.Items.Components
}
else if (!string.IsNullOrWhiteSpace(ItemIdentifier))
{
Identifier[] allItems = ItemIdentifier.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray();
Identifier[] allItems = ItemIdentifier.ToIdentifiers().ToArray();
Identifier itemIdentifier = allItems.GetRandomUnsynced();
ItemPrefab? prefab = ItemPrefab.Find(null, itemIdentifier);
if (prefab is null) { return; }
@@ -320,7 +342,7 @@ namespace Barotrauma.Items.Components
{
if (!string.IsNullOrWhiteSpace(SpeciesName))
{
Identifier[] allSpecies = SpeciesName.Split(',').Select(s => s.Trim()).ToIdentifiers().ToArray();
Identifier[] allSpecies = SpeciesName.ToIdentifiers().ToArray();
Identifier species = allSpecies.GetRandomUnsynced();
Entity.Spawner?.AddCharacterToSpawnQueue(species, pos, onSpawn);
}
@@ -2,6 +2,7 @@
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
@@ -15,47 +16,57 @@ namespace Barotrauma.Items.Components
private AfflictionPrefab selectedEffect, selectedTaintedEffect;
[Serialize("", IsPropertySaveable.Yes)]
public string Effect
{
get;
set;
}
public string Effect { get; set; }
[Serialize("geneticmaterialdebuff", IsPropertySaveable.Yes)]
public Identifier TaintedEffect
{
get;
set;
}
[Serialize("geneticmaterialdebuff", IsPropertySaveable.Yes, description: "Either the identifier or the type for the tainted effect prefab")]
public Identifier TaintedEffect { get; set; }
private bool tainted;
[Serialize(false, IsPropertySaveable.Yes)]
public bool Tainted
{
get { return tainted; }
private set
set
{
if (!value) { return; }
tainted = true;
item.AllowDeconstruct = false;
if (!TaintedEffect.IsEmpty)
tainted = value;
if (tainted)
{
selectedTaintedEffect = AfflictionPrefab.Prefabs.Where(a =>
a.Identifier == TaintedEffect ||
a.AfflictionType == TaintedEffect).GetRandomUnsynced();
if (!TaintedEffect.IsEmpty)
{
selectedTaintedEffect = AfflictionPrefab.Prefabs.Where(a =>
a.Identifier == TaintedEffect ||
a.AfflictionType == TaintedEffect).GetRandomUnsynced();
}
}
else
{
if (targetCharacter != null)
{
var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null)
{
affliction.Strength = 0;
}
}
selectedTaintedEffect = null;
}
}
}
[Serialize(false, IsPropertySaveable.Yes)]
public bool SetTaintedOnDeath { get; private set; }
[Serialize(false, IsPropertySaveable.Yes)]
public bool CanBeUntainted { get; private set; }
//only for saving the selected tainted effect
[Serialize("", IsPropertySaveable.Yes)]
public Identifier SelectedTaintedEffect
{
get { return selectedTaintedEffect?.Identifier ?? Identifier.Empty; }
private set
{
selectedTaintedEffect = !value.IsEmpty ? AfflictionPrefab.Prefabs.Find(a => a.Identifier == value) : null;
}
private set { selectedTaintedEffect = !value.IsEmpty ? AfflictionPrefab.Prefabs.Find(a => a.Identifier == value) : null; }
}
public GeneticMaterial(Item item, ContentXElement element)
@@ -66,6 +77,7 @@ namespace Barotrauma.Items.Components
{
materialName = TextManager.Get(nameId);
}
if (!string.IsNullOrEmpty(Effect))
{
selectedEffect = AfflictionPrefab.Prefabs.Where(a =>
@@ -74,20 +86,103 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(3.0f, IsPropertySaveable.No)]
public float ConditionIncreaseOnCombineMin { get; set; }
[Serialize(0.0f, IsPropertySaveable.No)]
public float ConditionIncreaseOnCombineMin { get; set; }
[Serialize(8.0f, IsPropertySaveable.No)]
[Serialize(0.0f, IsPropertySaveable.No)]
public float ConditionIncreaseOnCombineMax { get; set; }
[Serialize(3.0f, IsPropertySaveable.No, description: "When refining, min value for condition increase bonus based on the quality of the worse gene.")]
public float ConditionIncreaseOnLowQualityCombine { get; set; }
[Serialize(25.0f, IsPropertySaveable.No, description: "When refining, max value for condition increase bonus based on the quality of the worse gene.")]
public float ConditionIncreaseOnHighQualityCombine { get; set; }
private bool SharesTypeWith(GeneticMaterial otherGeneticMaterial)
{
return GetSharedTypeOrDefault(otherGeneticMaterial) != null;
}
private ItemPrefab GetSharedTypeOrDefault(GeneticMaterial otherGeneticMaterial)
{
if (otherGeneticMaterial == null) { return default; }
return AllMaterialTypes.FirstOrDefault(materialType => otherGeneticMaterial.AllMaterialTypes.Contains(materialType));
}
private IEnumerable<ItemPrefab> AllMaterialTypes
{
get
{
yield return item.Prefab;
if (IsCombined) { yield return NestedMaterial.item.Prefab; }
}
}
private GeneticMaterial NestedMaterial
{
get
{
if (item == null || item.OwnInventory == null) { return null; }
var nestedItemWithGeneticMaterial = item.OwnInventory.AllItems.FirstOrDefault(it => it.GetComponent<GeneticMaterial>() != null);
if (nestedItemWithGeneticMaterial == null) { return null; }
return nestedItemWithGeneticMaterial.GetComponent<GeneticMaterial>();
}
}
private bool IsCombined
{
get
{
if (NestedMaterial != null) { return true; }
// check if this is the nested material
if (item.ParentInventory != null &&
item.ParentInventory.Owner is Item parentItem &&
parentItem.GetComponent<GeneticMaterial>()?.NestedMaterial == this)
{
return true;
}
return false;
}
}
private CombineResult GetCombineRefineResult(GeneticMaterial otherGeneticMaterial)
{
if (otherGeneticMaterial == null)
{
return CombineResult.None;
}
// both are combined, no more processing is possible
if (IsCombined && otherGeneticMaterial.IsCombined)
{
return CombineResult.None;
}
// neither is combined, can be either refined or combined
if (!IsCombined && !otherGeneticMaterial.IsCombined)
{
return SharesTypeWith(otherGeneticMaterial) ? CombineResult.Refined : CombineResult.Combined;
}
// one is combined, if they share a type, they can be refined
return SharesTypeWith(otherGeneticMaterial) ? CombineResult.Refined : CombineResult.None;
}
public bool CanBeCombinedWith(GeneticMaterial otherGeneticMaterial)
{
return !tainted && otherGeneticMaterial != null && !otherGeneticMaterial.tainted && item.AllowDeconstruct && otherGeneticMaterial.item.AllowDeconstruct;
return GetCombineRefineResult(otherGeneticMaterial) != CombineResult.None;
}
public override void Equip(Character character)
{
if (character == null) { return; }
IsActive = true;
if (targetCharacter != null) { return; }
@@ -99,7 +194,7 @@ namespace Barotrauma.Items.Components
float selectedEffectStrength = GetCombinedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedEffect.Instantiate(selectedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null)
if (affliction != null)
{
affliction.Strength = selectedEffectStrength;
//force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing
@@ -107,24 +202,27 @@ namespace Barotrauma.Items.Components
}
#if SERVER
item.CreateServerEvent(this);
#endif
#endif
}
if (tainted && selectedTaintedEffect != null)
{
float selectedTaintedEffectStrength = GetCombinedTaintedEffectStrength();
character.CharacterHealth.ApplyAffliction(null, selectedTaintedEffect.Instantiate(selectedTaintedEffectStrength));
var affliction = character.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (affliction != null)
{
if (affliction != null)
{
affliction.Strength = selectedTaintedEffectStrength;
//force strength to the correct value to bypass any clamping e.g. AfflictionHusk might be doing
affliction.SetStrength(selectedTaintedEffectStrength);
}
targetCharacter = character;
#if SERVER
item.CreateServerEvent(this);
#endif
}
foreach (Item containedItem in item.ContainedItems)
{
containedItem.GetComponent<GeneticMaterial>()?.Equip(character);
@@ -136,23 +234,60 @@ namespace Barotrauma.Items.Components
base.Update(deltaTime, cam);
if (targetCharacter != null)
{
if (SetTaintedOnDeath && !tainted &&
targetCharacter.IsDead && targetCharacter.CauseOfDeath is not { Type: CauseOfDeathType.Disconnected })
{
SetTainted(true);
}
var rootContainer = item.RootContainer;
if (!targetCharacter.HasEquippedItem(item) &&
if (!targetCharacter.HasEquippedItem(item) &&
(rootContainer == null || !targetCharacter.HasEquippedItem(rootContainer) || !targetCharacter.Inventory.IsInLimbSlot(rootContainer, InvSlotType.HealthInterface)))
{
item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, targetCharacter);
IsActive = false;
Character prevTargetCharacter = targetCharacter;
var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null) { affliction.Strength = GetCombinedEffectStrength(); }
var taintedAffliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (taintedAffliction != null) { taintedAffliction.Strength = GetCombinedTaintedEffectStrength(); }
//deactivate so the material is no longer updated or considered to be "in effect" in GetCombinedEffectStrength
Deactivate();
if (rootContainer != null)
{
foreach (var otherItem in rootContainer.ContainedItems)
{
if (otherItem != item && otherItem.GetComponent<GeneticMaterial>() is { IsActive: true } otherGeneticMaterial)
{
//we need to deactivate other genetic materials in the container too at this point,
//otherwise their effects might get triggered by the damage done by removing the gene
otherGeneticMaterial.Deactivate();
}
}
}
targetCharacter = null;
//do this after nullifying the effects, otherwise the damage from removing the genes could trigger the gene's own effects
item.ApplyStatusEffects(ActionType.OnSevered, 1.0f, prevTargetCharacter);
}
}
}
private void Deactivate()
{
IsActive = false;
if (targetCharacter != null)
{
var affliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedEffect);
if (affliction != null)
{
affliction.Strength = GetCombinedEffectStrength();
}
var taintedAffliction = targetCharacter.CharacterHealth.GetAllAfflictions().FirstOrDefault(a => a.Prefab == selectedTaintedEffect);
if (taintedAffliction != null)
{
taintedAffliction.Strength = GetCombinedTaintedEffectStrength();
}
}
NestedMaterial?.Deactivate();
targetCharacter = null;
}
public enum CombineResult
{
None,
@@ -160,35 +295,85 @@ namespace Barotrauma.Items.Components
Combined
}
public CombineResult Combine(GeneticMaterial otherGeneticMaterial, Character user)
public CombineResult Combine(GeneticMaterial otherGeneticMaterial, Character user, out Item itemToDestroy)
{
if (!CanBeCombinedWith(otherGeneticMaterial)) { return CombineResult.None; }
var combineRefineResult = GetCombineRefineResult(otherGeneticMaterial);
float conditionIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax);
conditionIncrease += user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f;
if (item.Prefab == otherGeneticMaterial.item.Prefab)
float randomQualityIncrease = Rand.Range(ConditionIncreaseOnCombineMin, ConditionIncreaseOnCombineMax);
float talentIncrease = user?.GetStatValue(StatTypes.GeneticMaterialRefineBonus) ?? 0.0f;
bool perfectQuality = item.IsFullCondition || otherGeneticMaterial.item.IsFullCondition;
itemToDestroy = otherGeneticMaterial.item;
if (combineRefineResult == CombineResult.Refined)
{
float taintedProbability = GetTaintedProbabilityOnRefine(otherGeneticMaterial, user);
item.Condition = Math.Max(item.Condition, otherGeneticMaterial.item.Condition) + conditionIncrease;
if (taintedProbability >= Rand.Range(0.0f, 1.0f))
float maxQuality = Math.Max(item.Condition, otherGeneticMaterial.item.Condition);
float minQuality = Math.Min(item.Condition, otherGeneticMaterial.item.Condition);
bool oneIsCombined = IsCombined || otherGeneticMaterial.IsCombined;
float minQualityProportionalIncreaseBonus = MathHelper.Lerp(ConditionIncreaseOnLowQualityCombine, ConditionIncreaseOnHighQualityCombine, Math.Clamp(minQuality / 80.0f, 0f, 1f));
float totalQualityIncrease = minQualityProportionalIncreaseBonus + randomQualityIncrease + talentIncrease;
if (oneIsCombined) { totalQualityIncrease /= 2f; }
float newQuality = maxQuality + totalQualityIncrease;
// the deconstructor wants to remove and delete the item for otherGeneticMaterial,
// we want to keep the type that's not being refined here, so we move around the items
if (!IsCombined && otherGeneticMaterial.IsCombined)
{
MakeTainted();
if (item.Prefab == otherGeneticMaterial.item.Prefab)
{
// main items share type, nest the non-shared item
item.OwnInventory?.TryPutItem(otherGeneticMaterial.NestedMaterial.item, user: null);
}
else
{
// the non-shared item is the main item in otherGeneticMaterial,
// we need to nest it...
item.OwnInventory?.TryPutItem(otherGeneticMaterial.item, user: null);
// ...and get rid of the now triple-nested item inside it
var otherNestedItem = otherGeneticMaterial.NestedMaterial.item;
otherGeneticMaterial.item.OwnInventory?.RemoveItem(otherNestedItem);
itemToDestroy = otherNestedItem;
}
}
// note: the condition of combined items represents the quality of both materials,
// and the condition of the nested item should equal that of the housing item
item.Condition = newQuality;
// this can become combined as a result of the item shuffling above
if (IsCombined) { NestedMaterial.item.Condition = newQuality; }
// if one item is 100% condition, remove taint when refining
if (CanBeUntainted && perfectQuality)
{
SetTainted(false, affectsNestedGene: true);
}
else if (GetTaintedProbabilityOnRefine(otherGeneticMaterial, user) >= Rand.Range(0.0f, 1.0f))
{
SetTainted(true);
}
return CombineResult.Refined;
}
else
else if (combineRefineResult == CombineResult.Combined)
{
item.Condition = otherGeneticMaterial.Item.Condition =
(item.Condition + otherGeneticMaterial.Item.Condition) / 2.0f + conditionIncrease;
float averageQuality = (item.Condition + otherGeneticMaterial.Item.Condition) / 2.0f;
item.Condition = otherGeneticMaterial.Item.Condition = averageQuality + randomQualityIncrease + talentIncrease;
item.OwnInventory?.TryPutItem(otherGeneticMaterial.Item, user: null);
item.AllowDeconstruct = false;
otherGeneticMaterial.Item.AllowDeconstruct = false;
if (GetTaintedProbabilityOnCombine(user) >= Rand.Range(0.0f, 1.0f))
// if one item is 100% condition, remove taint when combining
if (CanBeUntainted && perfectQuality)
{
MakeTainted();
SetTainted(false, affectsNestedGene: true);
}
else if (GetTaintedProbabilityOnCombine(user) >= Rand.Range(0.0f, 1.0f))
{
SetTainted(true);
}
return CombineResult.Combined;
}
return combineRefineResult;
}
private float GetCombinedEffectStrength()
@@ -198,11 +383,13 @@ namespace Barotrauma.Items.Components
{
var geneticMaterial = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial == null || !geneticMaterial.IsActive) { continue; }
if (geneticMaterial.selectedEffect == selectedEffect)
{
effectStrength += otherItem.ConditionPercentage / 100.0f * selectedEffect.MaxStrength;
}
}
return effectStrength;
}
@@ -213,17 +400,20 @@ namespace Barotrauma.Items.Components
{
var geneticMaterial = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial == null || !geneticMaterial.IsActive) { continue; }
if (selectedTaintedEffect != null && geneticMaterial.selectedTaintedEffect == selectedTaintedEffect)
{
taintedEffectStrength += otherItem.ConditionPercentage / 100.0f * selectedTaintedEffect.MaxStrength;
}
}
return taintedEffectStrength;
}
private float GetTaintedProbabilityOnRefine(GeneticMaterial otherGeneticMaterial, Character user)
{
if (user == null) { return 1.0f; }
float probability = MathHelper.Lerp(0.0f, 0.99f, Math.Max(item.Condition, otherGeneticMaterial.Item.Condition) / 100.0f);
probability *= MathHelper.Lerp(1.0f, 0.25f, DegreeOfSuccess(user));
return MathHelper.Clamp(probability, 0.0f, 1.0f);
@@ -232,17 +422,24 @@ namespace Barotrauma.Items.Components
private static float GetTaintedProbabilityOnCombine(Character user)
{
if (user == null) { return 1.0f; }
float probability = 1.0f - user.GetStatValue(StatTypes.GeneticMaterialTaintedProbabilityReductionOnCombine);
return MathHelper.Clamp(probability, 0.0f, 1.0f);
}
private void MakeTainted()
public void SetTainted(bool newValue, bool affectsNestedGene = false)
{
if (GameMain.NetworkMember?.IsClient ?? false) { return; }
Tainted = true;
Tainted = newValue;
#if SERVER
item.CreateServerEvent(this);
#endif
#endif
if (affectsNestedGene && NestedMaterial != null)
{
NestedMaterial.SetTainted(newValue);
}
}
public static LocalizedString TryCreateName(ItemPrefab prefab, XElement element)
@@ -258,7 +455,8 @@ namespace Barotrauma.Items.Components
}
}
}
return prefab.Name;
}
}
}
}
@@ -556,7 +556,7 @@ namespace Barotrauma.Items.Components
if (spawnProduct || spawnSeed)
{
VineTile vine = Vines.GetRandomUnsynced();
VineTile vine = Vines.GetRandomUnsynced()!;
spawnPos = vine.GetWorldPosition(planter, slot.Offset);
}
else
@@ -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,51 @@ namespace Barotrauma.Items.Components
set;
}
[Editable, Serialize("", IsPropertySaveable.Yes, translationTextTag: "ItemMsg", description: "A text displayed next to the item when it's been dropped on the floor (not attached to a wall).")]
public string MsgWhenDropped
{
get;
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 +301,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;
}
@@ -688,7 +740,19 @@ namespace Barotrauma.Items.Components
#endif
//make the item pickable with the default pick key and with no specific tools/items when it's deattached
RequiredItems.Clear();
DisplayMsg = "";
if (MsgWhenDropped.IsNullOrEmpty())
{
DisplayMsg = "";
}
else
{
DisplayMsg = TextManager.Get(MsgWhenDropped);
DisplayMsg =
DisplayMsg.Loaded ?
TextManager.ParseInputTypes(DisplayMsg) :
MsgWhenDropped;
}
PickKey = InputType.Select;
#if CLIENT
item.DrawDepthOffset = SpriteDepthWhenDropped - item.SpriteDepth;
@@ -755,21 +819,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 +881,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;
@@ -1,4 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework;
using System.Collections.Immutable;
namespace Barotrauma.Items.Components
@@ -23,7 +23,7 @@ namespace Barotrauma.Items.Components
public string OwnerTags
{
get => string.Join(',', OwnerTagSet);
set => OwnerTagSet = value.Split(',').ToIdentifiers().ToImmutableHashSet();
set => OwnerTagSet = value.ToIdentifiers().ToImmutableHashSet();
}
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
@@ -95,6 +95,10 @@ namespace Barotrauma.Items.Components
{
item.AddTag(s);
}
if (GameMain.GameSession?.GameMode is PvPMode)
{
item.AddTag($"id_{character.TeamID}".ToIdentifier());
}
if (!string.IsNullOrWhiteSpace(spawnPoint.IdCardDesc))
{
item.Description = Description = spawnPoint.IdCardDesc;
@@ -52,7 +52,11 @@ namespace Barotrauma.Items.Components
{
if (holdable.Attached)
{
GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + item.Prefab.Identifier);
//we don't need info of every collected resource, we can get a good sample size just by logging a small sample
if (GameAnalyticsManager.ShouldLogRandomSample())
{
GameAnalyticsManager.AddDesignEvent("ResourceCollected:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + item.Prefab.Identifier);
}
holdable.DeattachFromWall();
}
trigger.Enabled = false;
@@ -174,9 +174,11 @@ namespace Barotrauma.Items.Components
public override void Drop(Character dropper, bool setTransform = true)
{
//end hit first (which sets the weapon to the "held" state, with disabled physics and no special collision detection)
EndHit();
//ensure the physics body is enabled
item.body.PhysEnabled = true;
base.Drop(dropper, setTransform);
hitting = false;
hitPos = 0.0f;
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -251,10 +253,7 @@ namespace Barotrauma.Items.Components
}
if (hitPos < -MathHelper.Pi)
{
RestoreCollision();
hitting = false;
hitTargets.Clear();
hitPos = 0;
EndHit();
}
}
}
@@ -291,6 +290,14 @@ namespace Barotrauma.Items.Components
User = character;
}
private void EndHit()
{
RestoreCollision();
hitting = false;
hitTargets.Clear();
hitPos = 0;
}
private void RestoreCollision()
{
impactQueue.Clear();
@@ -327,6 +334,7 @@ namespace Barotrauma.Items.Components
if (targetLimb.character.IgnoreMeleeWeapons) { return false; }
var targetCharacter = targetLimb.character;
if (targetCharacter == picker) { return false; }
if (HitFriendlyTarget(targetCharacter)) { return false; }
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetCharacter)) { return false; }
@@ -341,7 +349,7 @@ namespace Barotrauma.Items.Components
{
if (targetCharacter == picker || targetCharacter == User) { return false; }
if (targetCharacter.IgnoreMeleeWeapons) { return false; }
targetLimb = targetCharacter.AnimController.GetLimb(LimbType.Torso); //Otherwise armor can be bypassed in strange ways
if (HitFriendlyTarget(targetCharacter)) { return false; }
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetCharacter)) { return false; }
@@ -380,6 +388,7 @@ namespace Barotrauma.Items.Components
}
else if (f2.Body.UserData is Holdable holdable && holdable.CanPush)
{
if (holdable.Item.GetRootInventoryOwner() == User) { return false; }
hitTargets.Add(holdable.Item);
}
}
@@ -387,10 +396,22 @@ namespace Barotrauma.Items.Components
{
return false;
}
impactQueue.Enqueue(f2);
return true;
// Prevent bots from hitting friendly targets.
bool HitFriendlyTarget(Character target)
{
if (User.IsPlayer) { return false; }
if (User.AIController is HumanAIController { Enabled: true } humanAI)
{
if (humanAI.ObjectiveManager.CurrentObjective is AIObjectiveCombat combat && combat.Enemy != target)
{
if (humanAI.IsFriendly(target, onlySameTeam: true)) { return true; }
}
}
return false;
}
}
private System.Text.StringBuilder serverLogger;
@@ -412,7 +433,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;
if (Attack != null)
{
@@ -452,10 +473,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;
@@ -475,8 +495,8 @@ namespace Barotrauma.Items.Components
}
if (GameMain.NetworkMember is { IsServer: true } server && targetEntity != null)
{
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: null, targetCharacter, targetLimb, useTarget: targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(conditionalActionType, targetItemComponent: this, targetCharacter, targetLimb, useTarget: targetEntity));
server.CreateEntityEvent(item, new Item.ApplyStatusEffectEventData(ActionType.OnUse, targetItemComponent: this, targetCharacter, targetLimb, useTarget: targetEntity));
serverLogger ??= new System.Text.StringBuilder();
serverLogger.Clear();
serverLogger.Append($"{picker?.LogName} used {item.Name}");
@@ -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));
@@ -243,7 +243,7 @@ namespace Barotrauma.Items.Components
IsActive = true;
float baseReloadTime = reload;
float weaponSkill = character.GetSkillLevel("weapons");
float weaponSkill = character.GetSkillLevel(Tags.WeaponsSkill);
bool applyReloadFailure = ReloadSkillRequirement > 0 && ReloadNoSkill > reload && weaponSkill < ReloadSkillRequirement;
if (applyReloadFailure)
@@ -291,9 +291,14 @@ namespace Barotrauma.Items.Components
var lastProjectile = LastProjectile;
if (lastProjectile != projectile)
{
//Note that we always snap the rope here, unlike when firing a rope from a turret.
//That's because handheld RangedWeapons have some special logic for handling the rope,
//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();
@@ -303,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)
@@ -314,6 +322,7 @@ namespace Barotrauma.Items.Components
}
}
}
projectile.Item.body.Dir = Item.body.Dir;
projectile.Shoot(character, character.AnimController.AimSourceSimPos, barrelPos, rotation + spread, ignoredBodies: ignoredBodies.ToList(), createNetworkEvent: false, damageMultiplier, LaunchImpulse);
projectile.Item.GetComponent<Rope>()?.Attach(Item, projectile.Item);
if (projectile.Item.body != null)
@@ -248,7 +248,7 @@ namespace Barotrauma.Items.Components
var barrelHull = Hull.FindHull(ConvertUnits.ToDisplayUnits(rayStartWorld), item.CurrentHull, useWorldCoordinates: true);
if (barrelHull != null && barrelHull != item.CurrentHull)
{
if (MathUtils.GetLineRectangleIntersection(ConvertUnits.ToDisplayUnits(sourcePos), ConvertUnits.ToDisplayUnits(rayStart), item.CurrentHull.Rect, out Vector2 hullIntersection))
if (MathUtils.GetLineWorldRectangleIntersection(ConvertUnits.ToDisplayUnits(sourcePos), ConvertUnits.ToDisplayUnits(rayStart), item.CurrentHull.Rect, out Vector2 hullIntersection))
{
if (!item.CurrentHull.ConnectedGaps.Any(g => g.Open > 0.0f && Submarine.RectContains(g.Rect, hullIntersection)))
{
@@ -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>();
@@ -764,7 +765,7 @@ namespace Barotrauma.Items.Components
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController humanAnim &&
Math.Abs(fromCharacterToLeak.X) < 100.0f && fromCharacterToLeak.Y < 0.0f && fromCharacterToLeak.Y > -150.0f)
{
humanAnim.Crouching = true;
humanAnim.Crouch();
}
}
if (!character.IsClimbing)
@@ -75,6 +75,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;
@@ -661,10 +664,13 @@ namespace Barotrauma.Items.Components
protected virtual void RemoveComponentSpecific()
{
#if CLIENT
HUDOverlay?.Remove();
HUDOverlay = null;
#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)
{
@@ -794,12 +800,25 @@ namespace Barotrauma.Items.Components
/// </summary>
private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard)
{
if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
if (item.Submarine is { IsRespawnShuttle: false })
{
//id cards don't work in enemy subs (except on items that only require the default "idcard" tag)
if (idCard.TeamID != CharacterTeamType.None && idCard.TeamID != item.Submarine.TeamID && relatedItem.Identifiers.Any(id => id != "idcard"))
{
return false;
if (GameMain.GameSession?.GameMode is PvPMode)
{
if (item.Submarine.TeamID != CharacterTeamType.FriendlyNPC && item.Submarine.TeamID != CharacterTeamType.None)
{
// In PvP, always allow access also to FriendlyNPC and None -> restrict access only to the enemy sub.
{
return false;
}
}
}
else
{
return false;
}
}
else if (idCard.SubmarineSpecificID != 0 && item.Submarine.SubmarineSpecificIDTag != idCard.SubmarineSpecificID)
{
@@ -108,7 +108,10 @@ 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(false, IsPropertySaveable.No, description: "Should items be drawn based on their position within the inventory?")]
public bool ItemsUseInventoryPlacement { get; set; }
[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;
@@ -139,13 +142,24 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No)]
private ImmutableHashSet<Identifier> autoInteractWithContainedTags = ImmutableHashSet<Identifier>.Empty;
[Serialize("", IsPropertySaveable.Yes, description: $"Interacting with this container will autointeract with contained items that have one of these tags. Only valid if {nameof(AutoInteractWithContained)} is set to true.")]
public string AutoInteractWithContainedTags
{
get { return autoInteractWithContainedTags.ConvertToString(); }
set
{
autoInteractWithContainedTags = value.ToIdentifiers().ToImmutableHashSet();
}
}
[Serialize(true, IsPropertySaveable.No, description: "Is the container accessible in general.")]
public bool AllowAccess { get; set; }
[Serialize(false, IsPropertySaveable.No)]
[Serialize(false, IsPropertySaveable.No, description: "Is the container only accessible when it's broken. Doesn't apply to editors.")]
public bool AccessOnlyWhenBroken { get; set; }
[Serialize(true, IsPropertySaveable.No)]
[Serialize(true, IsPropertySaveable.No, description: "Is the container accessible when dropped.")]
public bool AllowAccessWhenDropped { get; set; }
[Serialize(5, IsPropertySaveable.No, description: "How many inventory slots the inventory has per row.")]
@@ -421,12 +435,11 @@ namespace Barotrauma.Items.Components
relatedItem ??= containableItem;
foreach (StatusEffect effect in containableItem.StatusEffects)
{
activeContainedItems.Add(new ActiveContainedItem(
containedItem,
effect,
containableItem.ExcludeBroken,
containableItem.ExcludeFullCondition,
containableItem.BlameEquipperForDeath));
ActiveContainedItem activeContainedItem = new(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition, containableItem.BlameEquipperForDeath);
activeContainedItems.Add(activeContainedItem);
if (!ShouldApplyEffects(activeContainedItem)) { continue; }
activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets);
}
}
}
@@ -492,6 +505,12 @@ namespace Barotrauma.Items.Components
public void OnItemRemoved(Item containedItem)
{
foreach (ActiveContainedItem activeContainedItem in activeContainedItems)
{
if (activeContainedItem.Item != containedItem || !ShouldApplyEffects(activeContainedItem)) { continue; }
activeContainedItem.StatusEffect.Apply(ActionType.OnRemoved, deltaTime: 1, item, targets);
}
activeContainedItems.RemoveAll(i => i.Item == containedItem);
containedItems.RemoveAll(i => i.Item == containedItem);
item.SetContainedItemPositions();
@@ -657,39 +676,47 @@ namespace Barotrauma.Items.Components
return;
}
foreach (var activeContainedItem in activeContainedItems)
foreach (ActiveContainedItem activeContainedItem in activeContainedItems)
{
Item contained = activeContainedItem.Item;
if (!ShouldApplyEffects(activeContainedItem)) continue;
if (activeContainedItem.ExcludeBroken && contained.Condition <= 0.0f) { continue; }
if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { continue; }
StatusEffect effect = activeContainedItem.StatusEffect;
targets.Clear();
bool wearing = item.GetComponent<Wearable>() is Wearable { IsActive: true };
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
targets.AddRange(item.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.AddRange(contained.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
{
targets.Add(character);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
effect.AddNearbyTargets(item.WorldPosition, targets);
}
effect.Apply(ActionType.OnActive, deltaTime, item, targets);
effect.Apply(ActionType.OnContaining, deltaTime, item, targets);
if (wearing) { effect.Apply(ActionType.OnWearing, deltaTime, item, targets); }
if (item.GetComponent<Wearable>() is Wearable { IsActive: true })
{
effect.Apply(ActionType.OnWearing, deltaTime, item, targets);
}
}
}
private bool ShouldApplyEffects(ActiveContainedItem activeContainedItem)
{
Item contained = activeContainedItem.Item;
if (activeContainedItem.ExcludeBroken && contained.Condition <= 0) { return false; }
if (activeContainedItem.ExcludeFullCondition && contained.IsFullCondition) { return false; }
StatusEffect effect = activeContainedItem.StatusEffect;
targets.Clear();
if (effect.HasTargetType(StatusEffect.TargetType.This))
{
targets.AddRange(item.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Contained))
{
targets.AddRange(contained.AllPropertyObjects);
}
if (effect.HasTargetType(StatusEffect.TargetType.Character) && item.ParentInventory?.Owner is Character character)
{
targets.Add(character);
}
if (effect.HasTargetType(StatusEffect.TargetType.NearbyItems) || effect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
{
effect.AddNearbyTargets(item.WorldPosition, targets);
}
return true;
}
/// <summary>
/// Set the positions of the contained items if this item has moved/rotated enough
/// </summary>
@@ -715,25 +742,37 @@ namespace Barotrauma.Items.Components
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
return AllowAccess && (!AccessOnlyWhenBroken || Item.Condition <= 0) && base.HasRequiredItems(character, addMessage, msg);
return IsAccessible() && base.HasRequiredItems(character, addMessage, msg);
}
/// <summary>
/// Is the container currently accessible. Use this method for checking the accessibility logic, instead of using custom logic on the properties.
/// Use <see cref="HasRequiredItems"/> instead to do a transitive check, where the items of the character are also checked.
/// </summary>
public bool IsAccessible()
{
if (!AllowAccess) { return false; }
if (AccessOnlyWhenBroken)
{
if (Screen.Selected is { IsEditor: true })
{
// AccessOnlyWhenBroken doesn't apply to editors.
return true;
}
return item.Condition <= 0;
}
return true;
}
public override bool Select(Character character)
{
if (!AllowAccess) { return false; }
if (item.Container != null) { return false; }
if (AccessOnlyWhenBroken)
{
if (item.Condition > 0)
{
return false;
}
}
if (!IsAccessible()) { return false; }
if (AutoInteractWithContained && character.SelectedItem == null && Screen.Selected is not { IsEditor: true })
{
foreach (Item contained in Inventory.AllItems)
{
if (contained.TryInteract(character))
if (CanAutoInteractWithContained(contained) && contained.TryInteract(character))
{
character.FocusedItem = contained;
return false;
@@ -756,19 +795,12 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (!AllowAccess) { return false; }
if (AccessOnlyWhenBroken)
{
if (item.Condition > 0)
{
return false;
}
}
if (!IsAccessible()) { return false; }
if (AutoInteractWithContained && Screen.Selected is not { IsEditor: true })
{
foreach (Item contained in Inventory.AllItems)
{
if (contained.TryInteract(picker))
if (CanAutoInteractWithContained(contained) && contained.TryInteract(picker))
{
picker.FocusedItem = contained;
return true;
@@ -819,6 +851,12 @@ namespace Barotrauma.Items.Components
}
}
private bool CanAutoInteractWithContained(Item containedItem)
{
return AutoInteractWithContained &&
(autoInteractWithContainedTags.None() || autoInteractWithContainedTags.Any(t => containedItem.HasTag(t)));
}
private void SetContainedActive(bool active)
{
if ((ContainableItems == null || !ContainableItems.Any(c => c.SetActive)) &&
@@ -888,58 +926,16 @@ 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()
{
Vector2 transformedItemPos = ItemPos * item.Scale;
Vector2 transformedItemInterval = ItemInterval * item.Scale;
Vector2 transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
Vector2 transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
if (containedItems.Count == 0) { return; }
if (ItemPos == Vector2.Zero && ItemInterval == Vector2.Zero)
{
transformedItemPos = item.Position;
}
else
{
if (item.body == null)
{
if (item.FlippedX)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemPos.X += item.Rect.Width;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
if (item.FlippedY)
{
transformedItemPos.Y = -transformedItemPos.Y;
transformedItemPos.Y -= item.Rect.Height;
transformedItemInterval.Y = -transformedItemInterval.Y;
transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
}
transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (Math.Abs(item.Rotation) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(-item.RotationRad);
transformedItemPos = Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position;
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
}
}
else
{
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
if (item.body.Dir == -1.0f)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
transformedItemInterval = Vector2.Transform(transformedItemInterval, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemPos += item.Position;
}
}
var rootBody = item.RootContainer?.body ?? item.body;
Vector2 transformedItemPos = GetContainedPosition(
drawPosition: false,
out Vector2 transformedItemIntervalHorizontal,
out Vector2 transformedItemIntervalVertical,
out bool flippedX,
out bool flippedY);
int i = 0;
Vector2 currentItemPos = transformedItemPos;
@@ -952,19 +948,19 @@ namespace Barotrauma.Items.Components
if (item.body != null)
{
Matrix transform = Matrix.CreateRotationZ(item.body.Rotation);
pos.X *= item.body.Dir;
pos.X *= rootBody.Dir;
itemPos = Vector2.Transform(pos, transform) + item.body.Position;
}
else
{
itemPos = pos;
// This code is aped based on above. Not tested.
if (item.FlippedX)
if (flippedX)
{
itemPos.X = -itemPos.X;
itemPos.X += item.Rect.Width;
}
if (item.FlippedY)
if (flippedY)
{
itemPos.Y = -itemPos.Y;
itemPos.Y -= item.Rect.Height;
@@ -990,7 +986,7 @@ namespace Barotrauma.Items.Components
}
if (item.body != null)
{
rotation *= item.body.Dir;
rotation *= rootBody.Dir;
rotation += item.body.Rotation;
}
else
@@ -998,8 +994,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)
{
@@ -1021,6 +1016,11 @@ namespace Barotrauma.Items.Components
contained.Item.CurrentHull = item.CurrentHull;
contained.Item.SetContainedItemPositions();
foreach (var lightComponent in contained.Item.GetComponents<LightComponent>())
{
lightComponent.SetLightSourceTransform();
}
i++;
if (Math.Abs(ItemInterval.X) > 0.001f && Math.Abs(ItemInterval.Y) > 0.001f)
{
@@ -1034,11 +1034,82 @@ namespace Barotrauma.Items.Components
}
else
{
currentItemPos += transformedItemInterval;
currentItemPos += transformedItemIntervalHorizontal + transformedItemIntervalVertical;
}
}
}
private Vector2 GetContainedPosition(bool drawPosition,
out Vector2 transformedItemIntervalHorizontal, out Vector2 transformedItemIntervalVertical,
out bool flippedX, out bool flippedY)
{
Vector2 transformedItemPos = ItemPos * item.Scale;
Vector2 transformedItemInterval = ItemInterval * item.Scale;
transformedItemIntervalHorizontal = new Vector2(transformedItemInterval.X, 0.0f);
transformedItemIntervalVertical = new Vector2(0.0f, transformedItemInterval.Y);
flippedX = item.RootContainer?.FlippedX ?? (item.FlippedX && item.Prefab.CanSpriteFlipX);
flippedY = item.RootContainer?.FlippedY ?? (item.FlippedY && item.Prefab.CanSpriteFlipY);
var rootBody = item.RootContainer?.body ?? item.body;
bool bodyFlipped = rootBody is { Dir: -1 };
if (ItemPos == Vector2.Zero && ItemInterval == Vector2.Zero && !drawPosition)
{
transformedItemPos = item.Position;
}
else
{
if (item.body == null)
{
if (flippedX)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemPos.X += item.Rect.Width;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
if (flippedY)
{
transformedItemPos.Y = -transformedItemPos.Y;
transformedItemPos.Y -= item.Rect.Height;
transformedItemInterval.Y = -transformedItemInterval.Y;
transformedItemIntervalVertical.Y = -transformedItemIntervalVertical.Y;
}
transformedItemPos += new Vector2(item.Rect.X, item.Rect.Y);
if (drawPosition)
{
if (item.Submarine != null) { transformedItemPos += item.Submarine.DrawPosition; }
}
if (Math.Abs(item.RotationRad) > 0.01f)
{
Matrix transform = Matrix.CreateRotationZ(-item.RotationRad);
transformedItemPos =
drawPosition ?
Vector2.Transform(transformedItemPos - item.DrawPosition, transform) + item.DrawPosition :
Vector2.Transform(transformedItemPos - item.Position, transform) + item.Position;
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
}
}
else
{
Matrix transform = Matrix.CreateRotationZ(drawPosition ? item.body.DrawRotation : item.body.Rotation);
if (bodyFlipped)
{
transformedItemPos.X = -transformedItemPos.X;
transformedItemInterval.X = -transformedItemInterval.X;
transformedItemIntervalHorizontal.X = -transformedItemIntervalHorizontal.X;
}
transformedItemPos = Vector2.Transform(transformedItemPos, transform);
transformedItemIntervalVertical = Vector2.Transform(transformedItemIntervalVertical, transform);
transformedItemIntervalHorizontal = Vector2.Transform(transformedItemIntervalHorizontal, transform);
transformedItemPos += drawPosition ? item.body.DrawPosition : item.body.Position;
}
}
return transformedItemPos;
}
public override void OnItemLoaded()
{
Inventory.AllowSwappingContainedItems = AllowSwappingContainedItems;
@@ -1086,6 +1157,8 @@ namespace Barotrauma.Items.Components
{
SpawnAlwaysContainedItems();
}
SetContainedItemPositions();
}
private void SpawnAlwaysContainedItems()
@@ -1096,7 +1169,7 @@ namespace Barotrauma.Items.Components
foreach (string id in splitIds)
{
ItemPrefab prefab = ItemPrefab.Prefabs.Find(m => m.Identifier == id);
if (prefab != null && Inventory != null && Inventory.CanBePut(prefab))
if (prefab != null && Inventory != null && Inventory.CanProbablyBePut(prefab))
{
bool isEditor = false;
#if CLIENT
@@ -18,7 +18,8 @@ namespace Barotrauma.Items.Components
public override bool Select(Character character)
{
if (character == null || character.LockHands || character.Removed || !(character.AnimController is HumanoidAnimController)) return false;
if (character == null || character.LockHands || character.Removed ) { return false; }
if (!character.CanClimb) { return false; }
character.AnimController.StartClimbing();
return true;
}
@@ -183,6 +183,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Does the Controller require power to function (= to send signals and move the camera focus to a connected item)?")]
public bool RequirePower
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "If true, other items can be used simultaneously.")]
public bool IsSecondaryItem
{
@@ -190,6 +197,13 @@ namespace Barotrauma.Items.Components
private set;
}
[Serialize(false, IsPropertySaveable.No, description: "If enabled, the user sticks to the position of this item even if the item moves.")]
public bool ForceUserToStayAttached
{
get;
set;
}
public Controller(Item item, ContentXElement element)
: base(item, element)
{
@@ -199,25 +213,44 @@ namespace Barotrauma.Items.Components
IsActive = true;
}
/// <summary>
/// Hack for allowing characters to interact with a loader to get inside a boarding pod.
/// Doing that simply by autointeracting with the contained pod is difficult, because interacting with the loader selects it
/// _after_ the Select method of the pod is called by the autointeract logic, and the character only goes inside the pod if it's the selected item.
/// </summary>
private bool forceSelectNextFrame;
private float userCanInteractCheckTimer;
private const float UserCanInteractCheckInterval = 1.0f;
public override void Update(float deltaTime, Camera cam)
{
this.cam = cam;
UserInCorrectPosition = false;
if (!ForceUserToStayAttached) { UserInCorrectPosition = false; }
string signal = IsToggle && State ? output : falseOutput;
if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal))
if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal) && !IsOutOfPower())
{
item.SendSignal(signal, "signal_out");
item.SendSignal(signal, "trigger_out");
}
if (forceSelectNextFrame && user != null)
{
user.SelectedItem = item;
}
forceSelectNextFrame = false;
userCanInteractCheckTimer -= deltaTime;
if (user == null
|| user.Removed
|| !user.IsAnySelectedItem(item)
|| item.ParentInventory != null
|| !user.CanInteractWith(item)
|| (item.ParentInventory != null && !IsAttachedUser(user))
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater))
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater)
|| !CheckUserCanInteract())
{
if (user != null)
{
@@ -228,6 +261,17 @@ namespace Barotrauma.Items.Components
return;
}
if (ForceUserToStayAttached && Vector2.DistanceSquared(item.WorldPosition, user.WorldPosition) > 0.1f)
{
user.TeleportTo(item.WorldPosition);
user.AnimController.Collider.ResetDynamics();
foreach (var limb in user.AnimController.Limbs)
{
if (limb.Removed || limb.IsSevered) { continue; }
limb.body?.ResetDynamics();
}
}
user.AnimController.StartUsingItem();
if (userPos != Vector2.Zero)
@@ -330,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)
@@ -344,6 +404,8 @@ namespace Barotrauma.Items.Components
return false;
}
if (IsOutOfPower()) { return false; }
if (IsToggle && (activator == null || lastUsed < Timing.TotalTime - 0.1))
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
@@ -380,6 +442,8 @@ namespace Barotrauma.Items.Components
return false;
}
if (IsOutOfPower()) { return false; }
focusTarget = GetFocusTarget();
if (focusTarget == null)
@@ -417,11 +481,20 @@ namespace Barotrauma.Items.Components
return true;
}
public bool IsOutOfPower()
{
if (!RequirePower) { return false; }
var powered = item.GetComponent<Powered>();
return powered == null || powered.Voltage < powered.MinVoltage;
}
public Item GetFocusTarget()
{
var positionOut = item.Connections?.Find(c => c.Name == "position_out");
if (positionOut == null) { return null; }
if (IsOutOfPower()) { return null; }
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut);
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
@@ -447,6 +520,7 @@ namespace Barotrauma.Items.Components
public override bool Pick(Character picker)
{
if (IsOutOfPower()) { return false; }
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { return false; }
#endif
@@ -539,7 +613,16 @@ namespace Barotrauma.Items.Components
{
user = activator;
IsActive = true;
if (ForceUserToStayAttached && item.Container != null)
{
forceSelectNextFrame = true;
return false;
}
}
//allow the selection logic above to run when out of power, but allow sending signals
if (IsOutOfPower()) { return false; }
#if SERVER
item.CreateServerEvent(this);
#endif
@@ -550,6 +633,14 @@ namespace Barotrauma.Items.Components
return true;
}
/// <summary>
/// "Attached user" sticks to this item. Can be used for things such as clown crates and boarding pods.
/// </summary>
public bool IsAttachedUser(Character character)
{
return character != null && character == user && ForceUserToStayAttached;
}
public override void FlipX(bool relativeToSub)
{
if (dir != Direction.None)
@@ -14,8 +14,6 @@ namespace Barotrauma.Items.Components
private float progressTimer;
private float progressState;
private bool hasPower;
private Character user;
private float userDeconstructorSpeedMultiplier = 1.0f;
@@ -88,9 +86,8 @@ namespace Barotrauma.Items.Components
SetActive(false);
return;
}
hasPower = Voltage >= MinVoltage;
if (!hasPower) { return; }
if (!HasPower) { return; }
var repairable = item.GetComponent<Repairable>();
if (repairable != null)
@@ -243,16 +240,16 @@ namespace Barotrauma.Items.Components
if (targetItem == otherItem) { continue; }
if (deconstructProduct.RequiredOtherItem.Any(r => otherItem.HasTag(r) || r == otherItem.Prefab.Identifier))
{
var geneticMaterial1 = targetItem.GetComponent<GeneticMaterial>();
var geneticMaterial2 = otherItem.GetComponent<GeneticMaterial>();
if (geneticMaterial1 != null && geneticMaterial2 != null)
var targetGeneticMaterial = targetItem.GetComponent<GeneticMaterial>();
var otherGeneticMaterial = otherItem.GetComponent<GeneticMaterial>();
if (targetGeneticMaterial != null && otherGeneticMaterial != null)
{
var result = geneticMaterial1.Combine(geneticMaterial2, user);
var result = targetGeneticMaterial.Combine(otherGeneticMaterial, user, out Item itemToDestroy);
if (result == GeneticMaterial.CombineResult.Refined)
{
inputContainer.Inventory.RemoveItem(otherItem);
OutputContainer.Inventory.RemoveItem(otherItem);
Entity.Spawner.AddItemToRemoveQueue(otherItem);
Entity.Spawner.AddItemToRemoveQueue(itemToDestroy);
}
if (result != GeneticMaterial.CombineResult.None)
{
@@ -425,31 +422,39 @@ namespace Barotrauma.Items.Components
foreach (Item inputItem in items)
{
if (!inputItem.AllowDeconstruct) { continue; }
foreach (var deconstructItem in inputItem.Prefab.DeconstructItems)
{
// check for deconstructor compatibility (for example, 'geneticresearchstation' tag)
if (deconstructItem.RequiredDeconstructor.Length > 0)
{
if (!deconstructItem.RequiredDeconstructor.Any(r => item.HasTag(r) || item.Prefab.Identifier == r)) { continue; }
if (deconstructItem.RequiredDeconstructor.None(requiredId => item.HasTag(requiredId) || item.Prefab.Identifier == requiredId)) { continue; }
}
// check for other required items in the same deconstructor (for example, 'geneticmaterial')
if (deconstructItem.RequiredOtherItem.Length > 0 && checkRequiredOtherItems)
{
if (!deconstructItem.RequiredOtherItem.Any(r => items.Any(it => it.HasTag(r) || it.Prefab.Identifier == r))) { continue; }
// no matching item with the required id, skip
if (deconstructItem.RequiredOtherItem.None(requiredId => items.Any(it => it.HasTag(requiredId) || it.Prefab.Identifier == requiredId))) { continue; }
bool validOtherItemFound = false;
foreach (Item otherInputItem in items)
{
if (otherInputItem == inputItem) { continue; }
if (!deconstructItem.RequiredOtherItem.Any(r => otherInputItem.HasTag(r) || otherInputItem.Prefab.Identifier == r)) { continue; }
if (deconstructItem.RequiredOtherItem.None(requiredId => otherInputItem.HasTag(requiredId) || otherInputItem.Prefab.Identifier == requiredId)) { continue; }
var geneticMaterial1 = inputItem.GetComponent<GeneticMaterial>();
var geneticMaterial2 = otherInputItem.GetComponent<GeneticMaterial>();
if (geneticMaterial1 != null && geneticMaterial2 != null)
// skip if genetic materials cannot be combined (or refined)
var geneticMaterial = inputItem.GetComponent<GeneticMaterial>();
var otherGeneticMaterial = otherInputItem.GetComponent<GeneticMaterial>();
if (geneticMaterial != null && otherGeneticMaterial != null)
{
if (!geneticMaterial1.CanBeCombinedWith(geneticMaterial2)) { continue; }
if (!geneticMaterial.CanBeCombinedWith(otherGeneticMaterial)) { continue; }
}
validOtherItemFound = true;
}
if (!validOtherItemFound) { continue; }
}
yield return (inputItem, deconstructItem);
}
}
@@ -126,7 +126,7 @@ namespace Barotrauma.Items.Components
}
else
{
hasPower = Voltage > MinVoltage;
hasPower = HasPower;
}
if (lastReceivedTargetForce.HasValue)
@@ -146,7 +146,7 @@ namespace Barotrauma.Items.Components
float forceMultiplier = 0.1f;
if (User != null)
{
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel("helm") / 100));
forceMultiplier *= MathHelper.Lerp(0.5f, 2.0f, (float)Math.Sqrt(User.GetSkillLevel(Tags.HelmSkill) / 100));
}
currForce *= item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.EngineMaxSpeed, MaxForce) * forceMultiplier;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
@@ -206,7 +206,7 @@ namespace Barotrauma.Items.Components
private void StartFabricating(FabricationRecipe selectedItem, Character user, bool addToServerLog = true)
{
if (selectedItem == null) { return; }
if (!outputContainer.Inventory.CanBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
if (!outputContainer.Inventory.CanProbablyBePut(selectedItem.TargetItem, selectedItem.OutCondition * selectedItem.TargetItem.Health)) { return; }
IsActive = true;
this.user = user;
@@ -319,7 +319,7 @@ namespace Barotrauma.Items.Components
}
else
{
hasPower = Voltage >= MinVoltage;
hasPower = HasPower;
if (!hasPower)
{
@@ -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>
@@ -720,17 +733,24 @@ namespace Barotrauma.Items.Components
private readonly HashSet<Item> usedIngredients = new HashSet<Item>();
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
public bool MissingRequiredRecipe(FabricationRecipe fabricableItem, Character character)
{
if (fabricableItem == null) { return false; }
if (fabricableItem.RequiresRecipe)
if (fabricableItem.RequiresRecipe)
{
if (character == null) { return false; }
if (!AnyOneHasRecipeForItem(character, fabricableItem.TargetItem))
{
return false;
return true;
}
}
return false;
}
private bool CanBeFabricated(FabricationRecipe fabricableItem, IReadOnlyDictionary<Identifier, List<Item>> availableIngredients, Character character)
{
if (fabricableItem == null) { return false; }
if (MissingRequiredRecipe(fabricableItem, character)) { return false; }
if (fabricableItem.HideForNonTraitors)
{
@@ -80,7 +80,7 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
hasPower = Voltage > MinVoltage;
hasPower = HasPower;
if (hasPower)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime);
@@ -46,7 +46,7 @@ namespace Barotrauma.Items.Components
if (item.CurrentHull == null) { return; }
if (Voltage < MinVoltage && PowerConsumption > 0)
if (!HasPower && PowerConsumption > 0)
{
return;
}
@@ -78,7 +78,7 @@ namespace Barotrauma.Items.Components
}
}
public bool HasPower => IsActive && Voltage >= MinVoltage;
public override bool HasPower => IsActive && Voltage >= MinVoltage;
public bool IsAutoControlled => pumpSpeedLockTimer > 0.0f || isActiveLockTimer > 0.0f;
private const float TinkeringSpeedIncrease = 4.0f;
@@ -140,13 +140,11 @@ namespace Barotrauma.Items.Components
float powerFactor = Math.Min(currPowerConsumption <= 0.0f || MinVoltage <= 0.0f ? 1.0f : Voltage, MaxOverVoltageFactor);
currFlow = flowPercentage / 100.0f * item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpMaxFlow, MaxFlow) * powerFactor;
currFlow = flowPercentage / 100.0f * MaxFlow * powerFactor;
if (item.GetComponent<Repairable>() is { IsTinkering: true } repairable)
{
currFlow *= 1f + repairable.TinkeringStrength * TinkeringSpeedIncrease;
}
currFlow = item.StatManager.GetAdjustedValueMultiplicative(ItemTalentStats.PumpSpeed, currFlow);
//less effective when in a bad condition
@@ -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)
{
@@ -340,7 +348,7 @@ namespace Barotrauma.Items.Components
{
foreach (Item item in containedItems)
{
if (!item.HasTag(Tags.Fuel)) { continue; }
if (!item.HasTag(Tags.ReactorFuel)) { continue; }
if (fissionRate > 0.0f)
{
bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost &&
@@ -707,13 +715,13 @@ namespace Barotrauma.Items.Components
var containObjective = AIContainItems<Reactor>(container, character, objective, itemCount: 1, equip: true, removeEmpty: true, spawnItemIfNotFound: !character.IsOnPlayerTeam, dropItemOnDeselected: true);
containObjective.Completed += ReportFuelRodCount;
containObjective.Abandoned += ReportFuelRodCount;
character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.Fuel, 30.0f);
character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.ReactorFuel, 30.0f);
void ReportFuelRodCount()
{
if (!character.IsOnPlayerTeam) { return; }
if (character.Submarine != Submarine.MainSub) { return; }
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.Fuel) && i.Condition > 1);
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.ReactorFuel) && i.Condition > 1);
if (remainingFuelRods == 0)
{
character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f);
@@ -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);
@@ -191,8 +194,7 @@ namespace Barotrauma.Items.Components
if (currentMode == Mode.Active)
{
if ((Voltage >= MinVoltage) &&
(!UseTransducers || connectedTransducers.Count > 0))
if (HasPower && (!UseTransducers || connectedTransducers.Count > 0))
{
if (currentPingIndex != -1)
{
@@ -380,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();
@@ -19,7 +19,7 @@ namespace Barotrauma.Items.Components
{
UpdateOnActiveEffects(deltaTime);
if (Voltage >= MinVoltage)
if (HasPower)
{
sendSignalTimer += deltaTime;
if (sendSignalTimer > SendSignalInterval)
@@ -61,6 +61,7 @@ namespace Barotrauma.Items.Components
private Sonar sonar;
private Submarine controlledSub;
public Submarine ControlledSub => controlledSub;
// AI interfacing
public Vector2 AITacticalTarget { get; set; }
@@ -75,6 +76,7 @@ namespace Barotrauma.Items.Components
private double lastReceivedSteeringSignalTime;
[Serialize(defaultValue: false, isSaveable: IsPropertySaveable.Yes, AlwaysUseInstanceValues = true)]
public bool AutoPilot
{
get { return autoPilot; }
@@ -298,7 +300,7 @@ namespace Barotrauma.Items.Components
controlledSub = sonar.ConnectedTransducers.Any() ? sonar.ConnectedTransducers.First().Item.Submarine : null;
}
if (Voltage < MinVoltage) { return; }
if (!HasPower) { return; }
if (user != null && user.Removed)
{
@@ -311,7 +313,7 @@ namespace Barotrauma.Items.Components
if (user != null && controlledSub != null &&
(user.SelectedItem == item || item.linkedTo.Contains(user.SelectedItem)))
{
userSkill = user.GetSkillLevel("helm") / 100.0f;
userSkill = user.GetSkillLevel(Tags.HelmSkill) / 100.0f;
}
// override autopilot pathing while the AI rams, and go full speed ahead
@@ -131,7 +131,7 @@ namespace Barotrauma.Items.Components
SuitablePlantItem plantItem = GetSuitableItem(character);
if (!plantItem.IsNull())
if (!plantItem.IsNull() && item.GetComponent<Holdable>() is not { Attachable: true, Attached: false })
{
Msg = plantItem.Type switch
{
@@ -159,7 +159,6 @@ namespace Barotrauma.Items.Components
{
SuitablePlantItem plantItem = GetSuitableItem(character);
PickingMsg = plantItem.IsNull() ? MsgUprooting : plantItem.ProgressBarMessage;
return base.Pick(character);
}
@@ -87,7 +87,7 @@ namespace Barotrauma.Items.Components
if (Math.Abs(charge - lastSentCharge) / adjustedCapacity > 0.05f)
{
#if SERVER
if (GameMain.Server != null && (!item.Submarine?.Loading ?? true)) { item.CreateServerEvent(this); }
if (GameMain.Server != null && item.FullyInitialized) { item.CreateServerEvent(this); }
#endif
lastSentCharge = charge;
}
@@ -1,4 +1,5 @@
using Microsoft.Xna.Framework;
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -231,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))
{
@@ -378,14 +379,30 @@ namespace Barotrauma.Items.Components
foreach (Connection c in item.Connections)
{
connectionDirty[c] = true;
if (c.IsPower)
{
ChangedConnections.Add(c);
if (connectedRecipients.TryGetValue(c, out var recipients))
{
recipients.Where(c => c.IsPower).ForEach(c => ChangedConnections.Add(c));
}
}
}
}
public void SetConnectionDirty(Connection connection)
{
var connections = item.Connections;
if (connections == null || !connections.Contains(connection)) return;
if (connections == null || !connections.Contains(connection)) { return; }
connectionDirty[connection] = true;
if (connection.IsPower)
{
ChangedConnections.Add(connection);
if (connectedRecipients.TryGetValue(connection, out var recipients))
{
recipients.Where(c => c.IsPower).ForEach(c => ChangedConnections.Add(c));
}
}
}
public override void OnItemLoaded()
@@ -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
@@ -170,6 +177,8 @@ namespace Barotrauma.Items.Components
/// Can be used by status effects or sounds to check if the item has enough power to run
/// </summary>
public float RelativeVoltage => minVoltage <= 0.0f ? 1.0f : MathHelper.Clamp(Voltage / minVoltage, 0.0f, 1.0f);
public virtual bool HasPower => Voltage >= MinVoltage;
public bool PoweredByTinkering { get; set; }
@@ -208,7 +217,7 @@ namespace Barotrauma.Items.Components
{
if (!powerOnSoundPlayed && powerOnSound != null)
{
SoundPlayer.PlaySound(powerOnSound.Sound, item.WorldPosition, powerOnSound.Volume, powerOnSound.Range, hullGuess: item.CurrentHull, ignoreMuffling: powerOnSound.IgnoreMuffling, freqMult: powerOnSound.GetRandomFrequencyMultiplier());
SoundPlayer.PlaySound(powerOnSound, item.WorldPosition, hullGuess: item.CurrentHull);
powerOnSoundPlayed = true;
}
}
@@ -668,6 +677,8 @@ namespace Barotrauma.Items.Components
return
conn1.IsPower && conn2.IsPower &&
conn1.Item.Condition > 0.0f && conn2.Item.Condition > 0.0f &&
conn1.Item.GetComponent<PowerTransfer>() is not { CanTransfer: false } &&
conn2.Item.GetComponent<PowerTransfer>() is not { CanTransfer: false } &&
(conn1.Item.HasTag(Tags.JunctionBox) || conn2.Item.HasTag(Tags.JunctionBox) || conn1.Item.HasTag(Tags.DockingPort) || conn2.Item.HasTag(Tags.DockingPort) || conn1.IsOutput != conn2.IsOutput);
}
@@ -28,20 +28,24 @@ namespace Barotrauma.Items.Components
SpreadCounter = 0;
}
struct HitscanResult
readonly struct HitscanResult
{
public Fixture Fixture;
public Vector2 Point;
public Vector2 Normal;
public float Fraction;
public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction)
public readonly Fixture Fixture;
public readonly Vector2 Point;
public readonly Vector2 Normal;
public readonly float Fraction;
public readonly Submarine Submarine;
public HitscanResult(Fixture fixture, Vector2 point, Vector2 normal, float fraction, Submarine sub)
{
Fixture = fixture;
Point = point;
Normal = normal;
Fraction = fraction;
Submarine = sub;
}
}
struct Impact
{
public Fixture Fixture;
@@ -184,6 +188,20 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "")]
public bool GoThroughLightTargets
{
get;
set;
}
[Serialize(-1f, IsPropertySaveable.No, description: $"Minimum mass of targets to stick to when {nameof(StickToLightTargets)} is disabled. Defaults to half of the projectile's mass.")]
public float LightTargetMassThreshold
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Hitscan projectiles cast a ray forwards and immediately hit whatever the ray hits. "+
"It is recommended to use hitscans for very fast-moving projectiles such as bullets, because using extremely fast launch velocities may cause physics glitches.")]
public bool Hitscan
@@ -261,6 +279,13 @@ namespace Barotrauma.Items.Components
}
private float maxJointTranslationInSimUnits = -1;
[Serialize(1000.0f, IsPropertySaveable.No)]
public float JointBreakPoint
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No)]
public bool Prismatic
{
@@ -310,7 +335,7 @@ namespace Barotrauma.Items.Components
if (!subElement.Name.ToString().Equals("attack", StringComparison.OrdinalIgnoreCase)) { continue; }
Attack = new Attack(subElement, item.Name + ", Projectile", item);
}
if (item.body == null)
{
DebugConsole.ThrowError($"Error in projectile definition ({item.Name}): No body defined!",
@@ -372,8 +397,6 @@ namespace Barotrauma.Items.Components
if (Item.Removed) { return; }
launchPos = simPosition;
LaunchSub = item.Submarine;
//set the rotation of the projectile again because dropping the projectile resets the rotation
Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians), findNewHull: false);
if (DeactivationTime > 0)
{
deactivationTimer = DeactivationTime;
@@ -419,6 +442,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
@@ -440,10 +471,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)
{
@@ -452,9 +483,15 @@ namespace Barotrauma.Items.Components
}
else
{
item.body.SetTransform(item.body.SimPosition, launchAngle);
float modifiedLaunchImpulse = (LaunchImpulse + launchImpulseModifier) * (1 + Rand.Range(-ImpulseSpread, ImpulseSpread));
DoLaunch(launchDir * modifiedLaunchImpulse);
//needs to be set after DoLaunch, because dropping the item resets the rotation and dir
float afterLaunchAngle = launchAngle + (item.body.Dir * LaunchRotationRadians);
if (item.body.Dir < 0)
{
afterLaunchAngle -= MathHelper.Pi;
}
item.SetTransform(item.body.SimPosition, afterLaunchAngle, findNewHull: false);
}
}
User = character;
@@ -579,7 +616,8 @@ namespace Barotrauma.Items.Components
inSubHits[i].Fixture,
inSubHits[i].Point + submarine.SimPosition,
inSubHits[i].Normal,
inSubHits[i].Fraction);
inSubHits[i].Fraction,
sub: null);
}
hits.AddRange(inSubHits);
}
@@ -592,6 +630,7 @@ namespace Barotrauma.Items.Components
{
var h = hits[i];
item.SetTransform(h.Point, rotation);
item.Submarine = h.Submarine;
item.UpdateTransform();
if (HandleProjectileCollision(h.Fixture, h.Normal, Vector2.Zero))
{
@@ -649,8 +688,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; }
@@ -669,6 +706,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
@@ -686,7 +728,7 @@ namespace Barotrauma.Items.Components
fixture.Body.GetTransform(out FarseerPhysics.Common.Transform transform);
if (!fixture.Shape.TestPoint(ref transform, ref rayStart)) { return true; }
hits.Add(new HitscanResult(fixture, rayStart, -dir, 0.0f));
hits.Add(new HitscanResult(fixture, rayStart, -dir, 0.0f, submarine));
return true;
}, ref aabb);
@@ -703,14 +745,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
@@ -755,10 +800,10 @@ namespace Barotrauma.Items.Components
}
}
hits.Add(new HitscanResult(fixture, point, normal, fraction));
hits.Add(new HitscanResult(fixture, point, normal, fraction, submarine));
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;
}
@@ -830,7 +875,7 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
if (StickTargetRemoved() || stickJoint is PrismaticJoint pJoint && Math.Abs(pJoint.JointTranslation) > maxJointTranslationInSimUnits)
if (StickTargetRemoved() || stickJoint is PrismaticJoint pJoint && Math.Abs(pJoint.JointTranslation) > maxJointTranslationInSimUnits || !stickJoint.Enabled)
{
Unstick();
#if SERVER
@@ -858,6 +903,7 @@ namespace Barotrauma.Items.Components
{
return false;
}
if (GoThroughLightTargets && target.Body.Mass < GetLightTargetMassThreshold()) { return false; }
if (target.IsSensor) { return false; }
if (hits.Contains(target.Body)) { return false; }
if (target.Body.UserData is Submarine)
@@ -872,10 +918,7 @@ namespace Barotrauma.Items.Components
limb.body?.ApplyLinearImpulse(item.body.LinearVelocity * item.body.Mass * 0.1f, item.SimPosition);
return false;
}
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
{
return false;
}
if (ShouldIgnoreCharacterCollision(limb.character)) { return false; }
}
else if (target.Body.UserData is Item item)
{
@@ -918,6 +961,20 @@ namespace Barotrauma.Items.Components
}
}
private bool ShouldIgnoreCharacterCollision(Character character)
{
//don't hit characters "attached" to the projectile (e.g. inside a boarding pod)
if (item.GetComponent<Controller>() is { } controller && controller.User == character && controller.IsAttachedUser(controller.User))
{
return true;
}
if (!FriendlyFire && User != null && character.IsFriendly(User))
{
return true;
}
return false;
}
/// <summary>
/// Should the collision with the target submarine be ignored (e.g. did the projectile collide with the wall behind the turret when being launched)
/// </summary>
@@ -961,7 +1018,8 @@ namespace Barotrauma.Items.Components
var wallBody = Submarine.PickBody(
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) - dir,
item.body.SimPosition - ConvertUnits.ToSimUnits(sub.Position) + dir,
collisionCategory: Physics.CollisionWall);
collisionCategory: Physics.CollisionWall,
customPredicate: (Fixture f) => IgnoredBodies == null || !IgnoredBodies.Contains(f.Body));
Vector2 launchPosInCurrentCoordinateSpace = launchPos;
if (item.body.Submarine == null && LaunchSub != null)
@@ -1015,10 +1073,6 @@ namespace Barotrauma.Items.Components
}
else if (target.Body.UserData is Limb limb)
{
if (!FriendlyFire && User != null && limb.character.IsFriendly(User))
{
return false;
}
// when hitting limbs with piercing ammo, don't lose as much speed
if (MaxTargetsToHit > 1)
{
@@ -1026,6 +1080,7 @@ namespace Barotrauma.Items.Components
deflectedSpeedMultiplier = 0.8f;
}
if (limb.IsSevered || limb.character == null || limb.character.Removed) { return false; }
if (ShouldIgnoreCharacterCollision(limb.character)) { return false; }
limb.character.LastDamageSource = item;
if (Attack != null) { attackResult = Attack.DoDamageToLimb(User ?? Attacker, limb, item.WorldPosition, 1.0f); }
@@ -1057,11 +1112,11 @@ namespace Barotrauma.Items.Components
{
if (Attack != null)
{
Vector2 pos = item.WorldPosition;
if (item.Submarine == null && damageable is Structure structure && structure.Submarine != null && Vector2.DistanceSquared(item.WorldPosition, structure.WorldPosition) > 10000.0f * 10000.0f)
{
item.Submarine = structure.Submarine;
}
Vector2 pos = item.WorldPosition;
attackResult = Attack.DoDamage(User ?? Attacker, damageable, pos, 1.0f);
}
}
@@ -1154,7 +1209,7 @@ namespace Barotrauma.Items.Components
else if ( remainingHits <= 0 &&
stickJoint == null && StickTarget == null &&
StickToStructures && target.Body.UserData is Structure ||
((StickToLightTargets || target.Body.Mass > item.body.Mass * 0.5f) &&
((StickToLightTargets || target.Body.Mass >= GetLightTargetMassThreshold()) &&
(DoesStick ||
(StickToCharacters && (target.Body.UserData is Limb || target.Body.UserData is Character)) ||
(target.Body.UserData is Item i && (i.GetComponent<Door>() != null ? StickToDoors : StickToItems)))))
@@ -1218,6 +1273,11 @@ namespace Barotrauma.Items.Components
return true;
}
private float GetLightTargetMassThreshold()
{
return LightTargetMassThreshold < 0 ? item.body.Mass * 0.5f : LightTargetMassThreshold;
}
private void EnableProjectileCollisions()
{
if (item.body.CollisionCategories != Category.None)
@@ -1274,7 +1334,7 @@ namespace Barotrauma.Items.Components
MotorEnabled = true,
MaxMotorForce = 30.0f,
LimitEnabled = true,
Breakpoint = 1000.0f
Breakpoint = JointBreakPoint,
};
if (maxJointTranslationInSimUnits == -1)
@@ -752,7 +752,7 @@ namespace Barotrauma.Items.Components
}
else if (ic is Powered powered && powered is not LightComponent)
{
if (powered.Voltage >= powered.MinVoltage) { return true; }
if (powered.HasPower) { return true; }
}
}
@@ -4,6 +4,7 @@ using FarseerPhysics;
using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using System;
using System.Linq;
namespace Barotrauma.Items.Components
{
@@ -102,6 +103,20 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the rope snap when the weapon it was fired from is fired again? I.e. can there be multiple ropes coming from the weapon at the same time?")]
public bool SnapWhenWeaponFiredAgain
{
get;
set;
}
[Serialize(0.9f, IsPropertySaveable.No, description: "Multiplier for the length of the barrel when determining where the rope should start from.")]
public float BarrelLengthMultiplier
{
get;
set;
}
[Serialize(30.0f, IsPropertySaveable.No, description: "How much mass is required for the target to pull the source towards it. Static and kinematic targets are always treated heavy enough.")]
public float TargetMinMass
{
@@ -115,7 +130,14 @@ namespace Barotrauma.Items.Components
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Should the force be dynamically adjusted to make it more difficult for targets to escape the pull?")]
public bool IncreaseForceForEscapingTargets
{
get;
set;
}
private bool isReelingIn;
private bool snapped;
public bool Snapped
@@ -314,7 +336,9 @@ namespace Barotrauma.Items.Components
// Currently can only apply pull forces to the source, when it's a character, not e.g. when the item would be auto-operated by an AI. Might have to change this.
if (user != null)
{
if (!snapped)
if (!snapped &&
//user can only hold on to the rope if it was launched from a holdable item, or by something else than an item (limb?)
(projectile.Launcher == null || projectile.Launcher.GetComponent<Holdable>() != null))
{
user.AnimController.HoldToRope();
if (targetCharacter != null)
@@ -332,61 +356,70 @@ namespace Barotrauma.Items.Components
var sourceBody = GetBodyToPull(source);
if (sourceBody != null)
{
isReelingIn = user.InWater && user.IsRagdolled || !user.InWater && targetCharacter is { IsIncapacitated: false };
if (isReelingIn)
PhysicsBody targetBody = GetBodyToPull(target);
if (sourceBody.UserData is Character)
{
float pullForce = SourcePullForce;
if (!user.InWater)
isReelingIn = user.InWater && user.IsRagdolled || !user.InWater && targetCharacter is { IsIncapacitated: false };
if (isReelingIn)
{
// Apply a tiny amount to the character holding the rope, so that the connection "feels" more real.
pullForce *= 0.1f;
}
float lengthFactor = MathUtils.InverseLerp(0, MaxLength / 2, currentRopeLength);
float force = LerpForces ? MathHelper.Lerp(0, pullForce, lengthFactor) : pullForce;
sourceBody.ApplyForce(forceDir * force);
// Take the target velocity into account.
PhysicsBody targetBody = GetBodyToPull(target);
if (targetBody != null)
{
if (targetCharacter != null)
float pullForce = SourcePullForce;
if (!user.InWater)
{
if (targetBody.LinearVelocity != Vector2.Zero && sourceBody.LinearVelocity != Vector2.Zero)
// Apply a tiny amount to the character holding the rope, so that the connection "feels" more real.
pullForce *= 0.1f;
}
float lengthFactor = MathUtils.InverseLerp(0, MaxLength / 2, currentRopeLength);
float force = LerpForces ? MathHelper.Lerp(0, pullForce, lengthFactor) : pullForce;
sourceBody.ApplyForce(forceDir * force);
// Take the target velocity into account.
if (targetBody != null)
{
if (targetCharacter != null)
{
Vector2 targetDir = Vector2.Normalize(targetBody.LinearVelocity);
float movementDot = Vector2.Dot(Vector2.Normalize(sourceBody.LinearVelocity), targetDir);
if (movementDot < 0)
if (targetBody.LinearVelocity != Vector2.Zero && sourceBody.LinearVelocity != Vector2.Zero)
{
// Pushing to a different dir -> add some counter force
const float multiplier = 5;
float inverseLengthFactor = MathHelper.Lerp(1, 0, lengthFactor);
sourceBody.ApplyForce(targetBody.LinearVelocity * Math.Min(targetBody.Mass * multiplier, 250) * sourceBody.Mass * -movementDot * inverseLengthFactor);
}
float forceDot = Vector2.Dot(forceDir, targetDir);
if (forceDot > 0)
{
// Pulling to the same dir -> add extra force
float targetSpeed = targetBody.LinearVelocity.Length();
const float multiplier = 25;
sourceBody.ApplyForce(forceDir * targetSpeed * sourceBody.Mass * multiplier * forceDot * lengthFactor);
}
float colliderMainLimbDistance = Vector2.Distance(sourceBody.SimPosition, user.AnimController.MainLimb.SimPosition);
const float minDist = 1;
const float maxDist = 10;
if (colliderMainLimbDistance > minDist)
{
// Move the ragdoll closer to the collider, if it's too far (the correction force in HumanAnimController is not enough -> the ragdoll would lag behind and get teleported).
float correctionForce = MathHelper.Lerp(10.0f, NetConfig.MaxPhysicsBodyVelocity, MathUtils.InverseLerp(minDist, maxDist, colliderMainLimbDistance));
Vector2 targetPos = sourceBody.SimPosition + new Vector2((float)Math.Sin(-sourceBody.Rotation), (float)Math.Cos(-sourceBody.Rotation)) * 0.4f;
user.AnimController.MainLimb.MoveToPos(targetPos, correctionForce);
Vector2 targetDir = Vector2.Normalize(targetBody.LinearVelocity);
float movementDot = Vector2.Dot(Vector2.Normalize(sourceBody.LinearVelocity), targetDir);
if (movementDot < 0)
{
// Pushing to a different dir -> add some counter force
const float multiplier = 5;
float inverseLengthFactor = MathHelper.Lerp(1, 0, lengthFactor);
sourceBody.ApplyForce(targetBody.LinearVelocity * Math.Min(targetBody.Mass * multiplier, 250) * sourceBody.Mass * -movementDot * inverseLengthFactor);
}
float forceDot = Vector2.Dot(forceDir, targetDir);
if (forceDot > 0)
{
// Pulling to the same dir -> add extra force
float targetSpeed = targetBody.LinearVelocity.Length();
const float multiplier = 25;
sourceBody.ApplyForce(forceDir * targetSpeed * sourceBody.Mass * multiplier * forceDot * lengthFactor);
}
float colliderMainLimbDistance = Vector2.Distance(sourceBody.SimPosition, user.AnimController.MainLimb.SimPosition);
const float minDist = 1;
const float maxDist = 10;
if (colliderMainLimbDistance > minDist && sourceBody.UserData is not Submarine)
{
// Move the ragdoll closer to the collider, if it's too far (the correction force in HumanAnimController is not enough -> the ragdoll would lag behind and get teleported).
float correctionForce = MathHelper.Lerp(10.0f, NetConfig.MaxPhysicsBodyVelocity, MathUtils.InverseLerp(minDist, maxDist, colliderMainLimbDistance));
Vector2 targetPos = sourceBody.SimPosition + new Vector2((float)Math.Sin(-sourceBody.Rotation), (float)Math.Cos(-sourceBody.Rotation)) * 0.4f;
user.AnimController.MainLimb.MoveToPos(targetPos, correctionForce);
}
}
}
}
else
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
else
{
sourceBody.ApplyForce(targetBody.LinearVelocity * sourceBody.Mass);
}
}
}
}
else
{
float distance = Vector2.Distance(source.WorldPosition, target.WorldPosition);
float force = LerpForces ? MathHelper.Lerp(0, SourcePullForce, MathUtils.InverseLerp(0, MaxLength / 2, distance)) : SourcePullForce;
sourceBody.ApplyForce(forceDir * force);
}
}
}
}
@@ -433,7 +466,7 @@ namespace Barotrauma.Items.Components
if (targetRagdoll.InWater || targetRagdoll.OnGround)
{
float forceMultiplier = 1;
if (!targetCharacter.IsRagdolled && !targetCharacter.IsIncapacitated)
if (!targetCharacter.IsRagdolled && !targetCharacter.IsIncapacitated && IncreaseForceForEscapingTargets)
{
// Pulling the main collider requires higher forces when the target is trying to move away.
Vector2 targetMovement = targetCharacter.AnimController.TargetMovement;
@@ -523,6 +556,7 @@ namespace Barotrauma.Items.Components
};
}
if (targetItem.body != null) { return targetItem.body; }
if (targetItem.StaticFixtures.Any() && targetItem.Submarine != null) { return targetItem.Submarine.PhysicsBody; }
}
else if (target is Limb targetLimb)
{
@@ -1,9 +1,9 @@
using Barotrauma.Extensions;
using Barotrauma.Extensions;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -11,24 +11,32 @@ namespace Barotrauma.Items.Components
{
[Editable, Serialize(new string[0], IsPropertySaveable.Yes, description: "Signals sent when the corresponding buttons are pressed.", alwaysUseInstanceValues: true)]
public string[] Signals { get; set; }
[Editable, Serialize("", IsPropertySaveable.Yes, description: "Identifiers or tags of items that, when contained, allow the terminal buttons to be used. Multiple ones should be separated by commas.", alwaysUseInstanceValues: true)]
public string ActivatingItems { get; set; }
private int RequiredSignalCount { get; set; }
private readonly int requiredSignalCount;
private ItemContainer Container { get; set; }
private HashSet<ItemPrefab> ActivatingItemPrefabs { get; set; } = new HashSet<ItemPrefab>();
private bool AllowUsingButtons => ActivatingItemPrefabs.None() || (Container != null && Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab)));
private bool IsActivated => ActivatingItemPrefabs.None() || (Container != null && Container.Inventory.AllItems.Any(i => i != null && ActivatingItemPrefabs.Any(p => p == i.Prefab)));
private readonly IReadOnlyList<string> buttonSignalDefinitions;
public ButtonTerminal(Item item, ContentXElement element) : base(item, element)
{
RequiredSignalCount = element.GetChildElements("TerminalButton").Count(c => c.GetAttribute("style") != null);
if (RequiredSignalCount < 1)
var buttons = element.GetChildElements("TerminalButton").Where(c => c.GetAttribute("style") != null);
if (buttons.None())
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements defined for the ButtonTerminal component!",
contentPackage: element.ContentPackage);
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no TerminalButton elements with a style defined for the ButtonTerminal component!", contentPackage: element.ContentPackage);
}
requiredSignalCount = buttons.Count();
List<string> buttonSignals = new ();
foreach (ContentXElement button in buttons)
{
buttonSignals.Add(button.GetAttributeString("signal", null));
}
buttonSignalDefinitions = buttonSignals.ToImmutableList();
InitProjSpecific(element);
}
@@ -37,57 +45,10 @@ namespace Barotrauma.Items.Components
public override void OnItemLoaded()
{
base.OnItemLoaded();
if (Signals == null)
{
Signals = new string[RequiredSignalCount];
for (int i = 0; i < RequiredSignalCount; i++)
{
Signals[i] = string.Empty;
}
}
else if (Signals.Length != RequiredSignalCount)
{
string[] newSignals = new string[RequiredSignalCount];
if (Signals.Length < RequiredSignalCount)
{
Signals.CopyTo(newSignals, 0);
for (int i = Signals.Length; i < RequiredSignalCount; i++)
{
newSignals[i] = string.Empty;
}
}
else
{
for (int i = 0; i < RequiredSignalCount; i++)
{
newSignals[i] = Signals[i];
}
}
Signals = newSignals;
}
ActivatingItemPrefabs.Clear();
if (!string.IsNullOrEmpty(ActivatingItems))
{
foreach (var activatingItem in ActivatingItems.Split(','))
{
if (MapEntityPrefab.Find(null, identifier: activatingItem, showErrorMessages: false) is ItemPrefab prefab)
{
ActivatingItemPrefabs.Add(prefab);
}
else
{
ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t == activatingItem))
.ForEach(p => ActivatingItemPrefabs.Add(p));
}
}
if (ActivatingItemPrefabs.None())
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no activating item prefabs found with identifiers or tags \"{ActivatingItems}\"");
}
}
LoadSignals();
LoadActivatingItems();
var containers = item.GetComponents<ItemContainer>();
if (containers.Count() != 1)
{
@@ -97,16 +58,100 @@ namespace Barotrauma.Items.Components
Container = containers.FirstOrDefault();
OnItemLoadedProjSpecific();
// Set active so that update loop is active and we can send the state_out signal.
IsActive = true;
}
partial void OnItemLoadedProjSpecific();
private bool SendSignal(int signalIndex, Character sender, bool isServerMessage = false)
public override void Update(float deltaTime, Camera cam)
{
if (!isServerMessage && !AllowUsingButtons) { return false; }
string signal = Signals[signalIndex];
base.Update(deltaTime, cam);
item.SendSignal(IsActivated ? "1" : "0", "state_out");
}
private void LoadSignals()
{
if (Signals == null || Signals.None())
{
Signals = new string[requiredSignalCount];
for (int i = 0; i < requiredSignalCount; i++)
{
Signals[i] = string.Empty;
}
// Load signals from the button elements, if defined.
for (int i = 0; i < buttonSignalDefinitions.Count; i++)
{
Debug.Assert(Signals.Length > i);
string overrideDefinition = buttonSignalDefinitions[i];
if (overrideDefinition != null)
{
Signals[i] = overrideDefinition;
}
}
}
else if (Signals.Length != requiredSignalCount)
{
string[] newSignals = new string[requiredSignalCount];
if (Signals.Length < requiredSignalCount)
{
Signals.CopyTo(newSignals, 0);
for (int i = Signals.Length; i < requiredSignalCount; i++)
{
newSignals[i] = string.Empty;
}
}
else
{
for (int i = 0; i < requiredSignalCount; i++)
{
newSignals[i] = Signals[i];
}
}
Signals = newSignals;
}
}
private void LoadActivatingItems()
{
ActivatingItemPrefabs.Clear();
if (!string.IsNullOrEmpty(ActivatingItems))
{
foreach (string activatingItem in ActivatingItems.Split(','))
{
Identifier itemIdentifier = activatingItem.ToIdentifier();
if (MapEntityPrefab.FindByIdentifier(itemIdentifier) is ItemPrefab prefab)
{
ActivatingItemPrefabs.Add(prefab);
}
else
{
ItemPrefab.Prefabs.Where(p => p.Tags.Any(t => t == itemIdentifier))
.ForEach(p => ActivatingItemPrefabs.Add(p));
}
}
if (ActivatingItemPrefabs.None())
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\": no activating item prefabs found with identifiers or tags \"{ActivatingItems}\"");
}
}
}
public override void Reset()
{
base.Reset();
Signals = null;
LoadSignals();
LoadActivatingItems();
}
private bool SendSignal(int signalIndex, Character sender, bool ignoreState = false, string overrideSignal = null)
{
if (!ignoreState && !IsActivated) { return false; }
string signal = overrideSignal ?? Signals[signalIndex];
string connectionName = $"signal_out{signalIndex + 1}";
item.SendSignal(new Signal(signal, sender: sender), connectionName);
AchievementManager.OnButtonTerminalSignal(item, sender);
return true;
}
@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
@@ -82,8 +82,13 @@ namespace Barotrauma.Items.Components
public bool IsFull => ComponentContainer?.Inventory is { } inventory && inventory.IsFull(true);
/// <summary>
/// Works the same way as the Locked property, but isn't persistent.
/// </summary>
public bool TemporarilyLocked;
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Locked circuit boxes can only be viewed and not interacted with.")]
public bool Locked { get; set; }
public bool Locked { get; private set; }
public CircuitBox(Item item, ContentXElement element) : base(item, element)
{
@@ -635,6 +640,18 @@ namespace Barotrauma.Items.Components
OnViewUpdateProjSpecific();
}
public void RemoveWire(Wire wireItem)
{
foreach (CircuitBoxWire wire in Wires.ToImmutableArray())
{
if (wire.BackingWire.TryUnwrap(out var backingWire) && backingWire == wireItem.Item)
{
RemoveWireCollectionUnsafe(wire);
}
}
OnViewUpdateProjSpecific();
}
private void RemoveWireCollectionUnsafe(CircuitBoxWire wire)
{
foreach (CircuitBoxOutputConnection output in Outputs)
@@ -756,6 +773,8 @@ namespace Barotrauma.Items.Components
_ => true
};
public bool IsLocked() => Locked || TemporarilyLocked;
public static Option<Item> GetApplicableResourcePlayerHas(ItemPrefab prefab, Character? character)
{
if (character is null) { return Option.None; }
@@ -155,7 +155,7 @@ namespace Barotrauma.Items.Components
if (DisplayName.IsNullOrEmpty())
{
#if DEBUG
DebugConsole.ThrowError("Missing display name in connection " + item.Name + ": " + Name);
DebugConsole.ThrowError($"Could not find a display name for the connection {Name} in the item {item.Name} (submarine: {item.Submarine?.Info?.Name ?? "none"})");
#endif
DisplayName = Name;
}
@@ -0,0 +1,122 @@
using System;
using System.Collections.Generic;
using System.Linq;
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
{
protected int selectedConnectionIndex;
protected string selectedConnectionIndexStr;
protected string selectedConnectionName;
private int connectionCount = -1;
[InGameEditable,
Serialize(0, IsPropertySaveable.Yes, description: "The index of the selected connection.", alwaysUseInstanceValues: true)]
public int SelectedConnection
{
get { return selectedConnectionIndex; }
set
{
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)
if (connectionCount > -1)
{
selectedConnectionIndex = Math.Min(selectedConnectionIndex, connectionCount - 1);
}
selectedConnectionName = GetConnectionName(selectedConnectionIndex);
selectedConnectionIndexStr = selectedConnectionIndex.ToString();
}
}
[InGameEditable,
Serialize(true, IsPropertySaveable.Yes, description: "Should the selected connection go back to the first one when moving past the last one?", alwaysUseInstanceValues: true)]
public bool WrapAround
{
get;
set;
}
[InGameEditable,
Serialize(true, IsPropertySaveable.Yes, description: "Should empty connections (connections with no wires in them) be skipped over when moving the selection?", alwaysUseInstanceValues: true)]
public bool SkipEmptyConnections
{
get;
set;
}
public ConnectionSelectorComponent(Item item, ContentXElement element)
: base(item, element)
{
}
protected abstract string GetConnectionName(int connectionIndex);
/// <summary>
/// Name of the input connection that sets the selected connection.
/// </summary>
protected abstract string InputNameSetConnection { get; }
/// <summary>
/// Name of the input connection that moves the selected connection.
/// </summary>
protected abstract string InputNameMoveInput { get; }
protected abstract IEnumerable<Connection> GetConnections();
public override void OnItemLoaded()
{
connectionCount = GetConnections().Count();
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (connection.Name == InputNameSetConnection)
{
if (int.TryParse(signal.value, out int newInput))
{
SelectedConnection = newInput;
}
}
else if (connection.Name == InputNameMoveInput)
{
if (int.TryParse(signal.value, out int moveAmount))
{
if (SkipEmptyConnections)
{
for (int i = 0; i < connectionCount; i++)
{
moveInput(moveAmount);
if (item.Connections.Any(c =>
c.Name == selectedConnectionName &&
(c.Wires.Any() || c.CircuitBoxConnections.Any())))
{
break;
}
}
}
else
{
moveInput(moveAmount);
}
}
}
void moveInput(int moveAmount)
{
if (WrapAround)
{
SelectedConnection = MathUtils.PositiveModulo(selectedConnectionIndex + moveAmount, connectionCount);
}
else
{
SelectedConnection += moveAmount;
}
}
}
}
@@ -21,6 +21,14 @@ namespace Barotrauma.Items.Components
class CustomInterfaceElement : ISerializableEntity
{
public enum InputTypeOption
{
Number,
Text,
Button,
TickBox
}
public bool ContinuousSignal;
public bool State;
public string ConnectionName;
@@ -33,6 +41,7 @@ namespace Barotrauma.Items.Components
public string Signal { get; set; }
public Identifier PropertyName { get; }
public Identifier TargetItemComponent { get; }
public bool TargetOnlyParentProperty { get; }
public string NumberInputMin { get; }
@@ -44,11 +53,21 @@ namespace Barotrauma.Items.Components
public const string DefaultNumberInputMin = "0", DefaultNumberInputMax = "99", DefaultNumberInputStep = "1";
public const int DefaultNumberInputDecimalPlaces = 0;
public bool IsNumberInput { get; }
public InputTypeOption InputType { get; }
public NumberType? NumberType { get; }
public bool HasPropertyName { get; }
public bool ShouldSetProperty { get; set; }
/// <summary>
/// By default, the elements in the interface only set values of the item or send signals.
/// 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";
public Dictionary<Identifier, SerializableProperty> SerializableProperties { get; set; }
@@ -59,24 +78,26 @@ namespace Barotrauma.Items.Components
/// Pass the parent component to the constructor to access the serializable properties
/// for elements which change property values.
/// </summary>
public CustomInterfaceElement(Item item, ContentXElement element, CustomInterface parent)
public CustomInterfaceElement(Item item, ContentXElement element, CustomInterface parent, InputTypeOption inputType)
{
Label = element.GetAttributeString("text", "");
ConnectionName = element.GetAttributeString("connection", "");
PropertyName = element.GetAttributeIdentifier("propertyname", "");
PropertyName = element.GetAttributeIdentifier("propertyname", Identifier.Empty);
TargetItemComponent = element.GetAttributeIdentifier("targetitemcomponent", Identifier.Empty);
TargetOnlyParentProperty = element.GetAttributeBool("targetonlyparentproperty", false);
NumberInputMin = element.GetAttributeString("min", DefaultNumberInputMin);
NumberInputMax = element.GetAttributeString("max", DefaultNumberInputMax);
NumberInputStep = element.GetAttributeString("step", DefaultNumberInputStep);
NumberInputDecimalPlaces = element.GetAttributeInt("decimalplaces", DefaultNumberInputDecimalPlaces);
MaxTextLength = element.GetAttributeInt("maxtextlength", int.MaxValue);
GetValueInterval = element.GetAttributeFloat(nameof(GetValueInterval), -1.0f);
InputType = inputType;
HasPropertyName = !PropertyName.IsEmpty;
if (HasPropertyName)
{
string elementName = element.Name.ToString().ToLowerInvariant();
IsNumberInput = elementName == "numberinput" || elementName == "integerinput"; // backwards compatibility
if (IsNumberInput)
if (inputType == InputTypeOption.Number)
{
string numberType = element.GetAttributeString("numbertype", string.Empty);
switch (numberType)
@@ -101,22 +122,7 @@ namespace Barotrauma.Items.Components
}
else if (HasPropertyName && parent != null)
{
if (TargetOnlyParentProperty)
{
if (parent.SerializableProperties.ContainsKey(PropertyName))
{
Signal = parent.SerializableProperties[PropertyName].GetValue(parent) as string;
}
}
else
{
foreach (ISerializableEntity e in parent.item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(PropertyName)) { continue; }
Signal = e.SerializableProperties[PropertyName].GetValue(e) as string;
break;
}
}
parent.SetSignalToPropertyValue(this);
}
else
{
@@ -125,7 +131,7 @@ namespace Barotrauma.Items.Components
foreach (var subElement in element.Elements())
{
if (subElement.Name.ToString().Equals("statuseffect", System.StringComparison.OrdinalIgnoreCase))
if (subElement.Name.ToString().Equals("statuseffect", StringComparison.OrdinalIgnoreCase))
{
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName: "custom interface element (label " + Label + ")"));
}
@@ -193,6 +199,13 @@ namespace Barotrauma.Items.Components
}
}
[Serialize(false, IsPropertySaveable.Yes)]
public bool ShowInsufficientPowerWarning
{
get;
set;
}
private readonly List<CustomInterfaceElement> customInterfaceElementList = new List<CustomInterfaceElement>();
public CustomInterface(Item item, ContentXElement element)
@@ -200,36 +213,44 @@ namespace Barotrauma.Items.Components
{
foreach (var subElement in element.Elements())
{
bool continuousSignalByDefault = false;
CustomInterfaceElement.InputTypeOption inputType = CustomInterfaceElement.InputTypeOption.Number;
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "button":
inputType = CustomInterfaceElement.InputTypeOption.Button;
continuousSignalByDefault = false;
break;
case "textbox":
inputType = CustomInterfaceElement.InputTypeOption.Text;
continuousSignalByDefault = false;
break;
case "integerinput": // backwards compatibility
case "numberinput":
var button = new CustomInterfaceElement(item, subElement, this)
{
ContinuousSignal = false
};
if (string.IsNullOrEmpty(button.Label))
{
button.Label = "Signal out " + customInterfaceElementList.Count(e => !e.ContinuousSignal);
}
customInterfaceElementList.Add(button);
inputType = CustomInterfaceElement.InputTypeOption.Number;
continuousSignalByDefault = false;
break;
case "tickbox":
var tickBox = new CustomInterfaceElement(item, subElement, this)
{
ContinuousSignal = true
};
if (string.IsNullOrEmpty(tickBox.Label))
{
tickBox.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal);
}
customInterfaceElementList.Add(tickBox);
inputType = CustomInterfaceElement.InputTypeOption.TickBox;
//the default behavior of tickboxes is different for mainly backwards compatibility reasons
//(e.g. keeps sending a true/false signal depending on the state of the tickbox, while the others send a signal when the value changes)
continuousSignalByDefault = true;
break;
default:
continue;
}
var ciElement = new CustomInterfaceElement(item, subElement, this, inputType)
{
ContinuousSignal = subElement.GetAttributeBool(nameof(CustomInterfaceElement.ContinuousSignal), def: continuousSignalByDefault)
};
if (string.IsNullOrEmpty(ciElement.Label))
{
ciElement.Label = "Signal out " + customInterfaceElementList.Count(e => e.ContinuousSignal == ciElement.ContinuousSignal);
}
customInterfaceElementList.Add(ciElement);
IsActive |= ciElement.ContinuousSignal;
}
IsActive = true;
InitProjSpecific();
//load these here to ensure the UI elements (created in InitProjSpecific) are up-to-date
Labels = element.GetAttributeString("labels", "");
@@ -268,27 +289,55 @@ namespace Barotrauma.Items.Components
if (element.HasPropertyName && element.ShouldSetProperty)
{
if (element.TargetOnlyParentProperty)
{
if (SerializableProperties.ContainsKey(element.PropertyName))
{
SerializableProperties[element.PropertyName].TrySetValue(this, element.Signal);
}
}
else
{
foreach (var po in item.AllPropertyObjects)
{
if (!po.SerializableProperties.ContainsKey(element.PropertyName)) { continue; }
po.SerializableProperties[element.PropertyName].TrySetValue(po, element.Signal);
}
}
SetPropertyValueToSignal(element);
customInterfaceElementList[i].ShouldSetProperty = false;
}
}
UpdateSignalsProjSpecific();
}
private void SetPropertyValueToSignal(CustomInterfaceElement element)
{
if (element.TargetOnlyParentProperty)
{
if (SerializableProperties.ContainsKey(element.PropertyName))
{
SerializableProperties[element.PropertyName].TrySetValue(this, element.Signal);
}
}
else
{
foreach (var po in item.AllPropertyObjects)
{
if (!po.SerializableProperties.ContainsKey(element.PropertyName)) { continue; }
if (!element.TargetItemComponent.IsEmpty && po.Name != element.TargetItemComponent) { continue; }
po.SerializableProperties[element.PropertyName].TrySetValue(po, element.Signal);
}
}
}
private void SetSignalToPropertyValue(CustomInterfaceElement element)
{
if (element.TargetOnlyParentProperty)
{
if (SerializableProperties.ContainsKey(element.PropertyName))
{
element.Signal = SerializableProperties[element.PropertyName].GetValue(this)?.ToString();
}
}
else
{
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(element.PropertyName)) { continue; }
if (!element.TargetItemComponent.IsEmpty && e.Name != element.TargetItemComponent) { continue; }
element.Signal = e.SerializableProperties[element.PropertyName].GetValue(e)?.ToString();
break;
}
}
}
public override void OnItemLoaded()
{
foreach (CustomInterfaceElement ciElement in customInterfaceElementList)
@@ -333,41 +382,28 @@ namespace Barotrauma.Items.Components
{
if (tickBoxElement == null) { return; }
tickBoxElement.State = state;
tickBoxElement.Signal = state.ToString();
if (!tickBoxElement.ContinuousSignal)
{
SetPropertyValueToSignal(tickBoxElement);
}
}
private void TextChanged(CustomInterfaceElement textElement, string text)
{
if (textElement == null) { return; }
textElement.Signal = text;
if (!textElement.TargetOnlyParentProperty)
{
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(textElement.PropertyName)) { continue; }
e.SerializableProperties[textElement.PropertyName].TrySetValue(e, text);
}
}
else if (SerializableProperties.ContainsKey(textElement.PropertyName))
{
SerializableProperties[textElement.PropertyName].TrySetValue(this, text);
}
SetPropertyValueToSignal(textElement);
}
private void ValueChanged(CustomInterfaceElement numberInputElement, int value)
{
if (numberInputElement == null) { return; }
numberInputElement.Signal = value.ToString();
if (!numberInputElement.TargetOnlyParentProperty)
SetPropertyValueToSignal(numberInputElement);
foreach (StatusEffect effect in numberInputElement.StatusEffects)
{
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(numberInputElement.PropertyName)) { continue; }
e.SerializableProperties[numberInputElement.PropertyName].TrySetValue(e, value);
}
}
else if (SerializableProperties.ContainsKey(numberInputElement.PropertyName))
{
SerializableProperties[numberInputElement.PropertyName].TrySetValue(this, value);
item.ApplyStatusEffect(effect, ActionType.OnUse, 1.0f, character: item.ParentInventory?.Owner as Character);
}
}
@@ -375,25 +411,14 @@ namespace Barotrauma.Items.Components
{
if (numberInputElement == null) { return; }
numberInputElement.Signal = value.ToString();
if (!numberInputElement.TargetOnlyParentProperty)
{
foreach (ISerializableEntity e in item.AllPropertyObjects)
{
if (!e.SerializableProperties.ContainsKey(numberInputElement.PropertyName)) { continue; }
e.SerializableProperties[numberInputElement.PropertyName].TrySetValue(e, value);
}
}
else if (SerializableProperties.ContainsKey(numberInputElement.PropertyName))
{
SerializableProperties[numberInputElement.PropertyName].TrySetValue(this, value);
}
SetPropertyValueToSignal(numberInputElement);
}
public override void Update(float deltaTime, Camera cam)
{
foreach (CustomInterfaceElement ciElement in customInterfaceElementList)
{
if (!ciElement.ContinuousSignal) { continue; }
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)
{
@@ -407,6 +432,13 @@ namespace Barotrauma.Items.Components
}
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
//CustomInterface works even when broken (it should be possible to tick the checkboxes and change values,
//it's up to the other components to work or not work depending on whether the item is broken)
Update(deltaTime, cam);
}
public override XElement Save(XElement parentElement)
{
labels = customInterfaceElementList.Select(ci => ci.Label).ToArray();
@@ -0,0 +1,57 @@
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Items.Components;
/// <summary>
/// A component with one input and multiple outputs. Can be used to choose which output the signal should be passed to.
/// </summary>
sealed class DemultiplexerComponent : ConnectionSelectorComponent
{
public DemultiplexerComponent(Item item, ContentXElement element)
: base(item, element)
{
}
protected override string InputNameSetConnection => "set_output";
protected override string InputNameMoveInput => "move_output";
public override void OnItemLoaded()
{
base.OnItemLoaded();
IsActive = item.Connections != null && item.Connections.Any(c => c.Name == "selected_output_out");
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (connection.Name == "signal_in")
{
item.SendSignal(signal, selectedConnectionName);
}
else
{
base.ReceiveSignal(signal, connection);
}
}
public override void Update(float deltaTime, Camera cam)
{
item.SendSignal(selectedConnectionIndexStr, "selected_output_out");
}
protected override string GetConnectionName(int connectionIndex)
{
return "signal_out" + connectionIndex;
}
protected override IEnumerable<Connection> GetConnections()
{
if (item.GetComponent<ConnectionPanel>() is { } connectionPanel)
{
return connectionPanel.Connections.Where(c => c.IsOutput && c.Name.StartsWith("signal_out"));
}
return Enumerable.Empty<Connection>();
}
}
@@ -94,9 +94,9 @@ namespace Barotrauma.Items.Components
set
{
if (isOn == value && IsActive == value) { return; }
IsActive = isOn = value;
SetLightSourceState(value, value ? lightBrightness : 0.0f);
bool isLightOn = isOn && item.Condition > 0;
SetLightSourceState(isLightOn, isLightOn ? lightBrightness : 0.0f);
OnStateChanged();
}
}
@@ -198,6 +198,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize("0,0", IsPropertySaveable.No, description: "Offset of the light from the position of the item (in pixels).")]
public Vector2 LightOffset
{
get;
set;
}
/// <summary>
/// Returns true if the red component of the light is twice as bright as the blue and green. Can be used by StatusEffects.
/// </summary>
@@ -259,7 +266,6 @@ namespace Barotrauma.Items.Components
#endif
IsActive = IsOn;
item.AddTag("light");
}
public override void OnItemLoaded()
@@ -305,17 +311,18 @@ namespace Barotrauma.Items.Components
(statusEffectLists == null || !statusEffectLists.ContainsKey(ActionType.OnActive)) &&
(IsActiveConditionals == null || IsActiveConditionals.Count == 0))
{
if (item.body == null || item.body.Enabled ||
(item.ParentInventory is ItemInventory itemInventory && !itemInventory.Container.HideItems))
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
}
else
PhysicsBody body = ParentBody ?? item.body;
if ((body == null || !body.Enabled) &&
(item.FindParentInventory(static it => it is ItemInventory { Container.HideItems: true }) != null))
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
}
else
{
lightBrightness = 1.0f;
SetLightSourceState(true, lightBrightness);
}
isOn = true;
SetLightSourceTransformProjSpecific();
base.IsActive = false;
@@ -367,7 +374,7 @@ namespace Barotrauma.Items.Components
SetLightSourceTransformProjSpecific();
PhysicsBody body = ParentBody ?? item.body;
if (body != null && !body.Enabled && !visibleInContainer)
if ((body == null || !body.Enabled) && !visibleInContainer)
{
lightBrightness = 0.0f;
SetLightSourceState(false, 0.0f);
@@ -1,7 +1,10 @@
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
@@ -12,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
@@ -23,15 +29,34 @@ 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)]
public string TargetCharacters
{
get => targetCharacters.ConvertToString();
set => targetCharacters = value.ToIdentifiers().ToHashSet();
}
private HashSet<Identifier> targetCharacters;
[InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the sensor ignore the bodies of dead characters?", alwaysUseInstanceValues: true)]
public bool IgnoreDead
@@ -40,7 +65,6 @@ namespace Barotrauma.Items.Components
set;
}
[InGameEditable, Serialize(0.0f, IsPropertySaveable.Yes, description: "Horizontal detection range.", alwaysUseInstanceValues: true)]
public float RangeX
{
@@ -248,51 +272,37 @@ namespace Barotrauma.Items.Components
if (worldBorders.Intersects(detectRect))
{
MotionDetected = true;
return;
foreach (Structure wall in Structure.WallList)
{
if (wall.Submarine == sub &&
wall.WorldRect.Intersects(detectRect))
{
MotionDetected = true;
return;
}
}
}
}
}
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 c in Character.CharacterList)
foreach (Character character in Character.CharacterList)
{
if (IgnoreDead && c.IsDead) { continue; }
//ignore characters that have spawned a second or less ago
//makes it possible to detect when a spawned character moves without triggering the detector immediately as the ragdoll spawns and drops to the ground
if (c.SpawnTime > Timing.TotalTime - 1.0) { continue; }
if (c.IsHuman)
{
if (!triggerFromHumans) { continue; }
}
else if (c.IsPet)
{
if (!triggerFromPets) { continue; }
}
else
{
// Not a human or a pet -> monster?
if (!triggerFromMonsters) { continue; }
if (CharacterParams.CompareGroup(c.Group, CharacterPrefab.HumanGroup))
{
//characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor)
continue;
}
}
if (character.SpawnTime > Timing.TotalTime - 1.0) { continue; }
if (!TriggersOn(character)) { continue; }
//do a rough check based on the position of the character's collider first
//before the more accurate limb-based check
if (Math.Abs(c.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(c.WorldPosition.Y - detectPos.Y) > broadRangeY)
if (Math.Abs(character.WorldPosition.X - detectPos.X) > broadRangeX || Math.Abs(character.WorldPosition.Y - detectPos.Y) > broadRangeY)
{
continue;
}
foreach (Limb limb in c.AnimController.Limbs)
foreach (Limb limb in character.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; }
@@ -304,7 +314,53 @@ namespace Barotrauma.Items.Components
}
}
}
public bool TriggersOn(Character character)
{
bool hasTriggers = triggerFromHumans || triggerFromPets || triggerFromMonsters;
if (!hasTriggers) { return false; }
return TriggersOn(character, triggerFromHumans, triggerFromPets, triggerFromMonsters);
}
private bool TriggersOn(Character character, bool triggerFromHumans, bool triggerFromPets, bool triggerFromMonsters)
{
if (IgnoreDead && character.IsDead) { return false; }
if (character.IsHuman)
{
if (!triggerFromHumans) { return false; }
}
else if (character.IsPet)
{
if (!triggerFromPets) { return false; }
}
else
{
// Not a human or a pet -> monster?
if (!triggerFromMonsters) { return false; }
if (CharacterParams.CompareGroup(character.Group, CharacterPrefab.HumanGroup))
{
//characters in the "human" group aren't considered monsters (even if they were something like a friendly mudraptor)
return false;
}
}
// Check matching character, if defined.
if (targetCharacters.Any())
{
// Performance critical code -> using a foreach loop to avoid having to capture variables in lambdas.
bool matchFound = false;
foreach (Identifier target in targetCharacters)
{
if (character.MatchesSpeciesNameOrGroup(target) || character.Params.HasTag(target))
{
matchFound = true;
break;
}
}
if (!matchFound) { return false; }
}
return true;
}
public override XElement Save(XElement parentElement)
{
Vector2 prevDetectOffset = detectOffset;
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Barotrauma.Items.Components;
/// <summary>
/// A component with multiple inputs and one output. Can be used to choose which input the component passes signals to the output from.
/// </summary>
sealed class MultiplexerComponent : ConnectionSelectorComponent
{
public MultiplexerComponent(Item item, ContentXElement element)
: base(item, element)
{
}
protected override string InputNameSetConnection => "set_input";
protected override string InputNameMoveInput => "move_input";
public override void OnItemLoaded()
{
base.OnItemLoaded();
IsActive = item.Connections != null && item.Connections.Any(c => c.Name == "selected_input_out");
}
public override void Update(float deltaTime, Camera cam)
{
item.SendSignal(selectedConnectionIndexStr, "selected_input_out");
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (connection.Name.StartsWith("signal_in"))
{
if (connection.Name == selectedConnectionName)
{
item.SendSignal(signal, "signal_out");
}
}
else
{
base.ReceiveSignal(signal, connection);
}
}
protected override string GetConnectionName(int connectionIndex)
{
return "signal_in" + connectionIndex;
}
protected override IEnumerable<Connection> GetConnections()
{
if (item.GetComponent<ConnectionPanel>() is { } connectionPanel)
{
return connectionPanel.Connections.Where(c => !c.IsOutput && c.Name.StartsWith("signal_in"));
}
return Enumerable.Empty<Connection>();
}
}
@@ -1,13 +1,11 @@
using System.Xml.Linq;
namespace Barotrauma.Items.Components
namespace Barotrauma.Items.Components
{
class OxygenDetector : ItemComponent
{
public const int LowOxygenPercentage = 35;
private int prevSentOxygenValue;
private string oxygenSignal;
public string OxygenSignal { get; private set; }
public OxygenDetector(Item item, ContentXElement element)
: base (item, element)
@@ -20,13 +18,13 @@ namespace Barotrauma.Items.Components
if (item.CurrentHull == null) { return; }
int currOxygenPercentage = (int)item.CurrentHull.OxygenPercentage;
if (prevSentOxygenValue != currOxygenPercentage || oxygenSignal == null)
if (prevSentOxygenValue != currOxygenPercentage || OxygenSignal == null)
{
prevSentOxygenValue = currOxygenPercentage;
oxygenSignal = prevSentOxygenValue.ToString();
OxygenSignal = prevSentOxygenValue.ToString();
}
item.SendSignal(oxygenSignal, "signal_out");
item.SendSignal(OxygenSignal, "signal_out");
item.SendSignal(currOxygenPercentage <= LowOxygenPercentage ? "1" : "0", "low_oxygen");
}
@@ -80,7 +80,6 @@ namespace Barotrauma.Items.Components
set
{
isOn = value;
CanTransfer = value;
if (!isOn)
{
currPowerConsumption = 0.0f;
@@ -1,5 +1,4 @@
using System;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -8,7 +7,7 @@ namespace Barotrauma.Items.Components
const float FireCheckInterval = 1.0f;
private float fireCheckTimer;
private bool fireInRange;
public bool FireInRange { get; private set; }
private int maxOutputLength;
[Editable, Serialize(200, IsPropertySaveable.No, description: "The maximum length of the output strings. Warning: Large values can lead to large memory usage or networking issues.")]
@@ -80,10 +79,10 @@ namespace Barotrauma.Items.Components
fireCheckTimer -= deltaTime;
if (fireCheckTimer <= 0.0f)
{
fireInRange = IsFireInRange();
FireInRange = IsFireInRange();
fireCheckTimer = FireCheckInterval;
}
string signalOut = fireInRange ? Output : FalseOutput;
string signalOut = FireInRange ? Output : FalseOutput;
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); }
}
}
@@ -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; }
@@ -8,7 +8,7 @@ using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class WifiComponent : ItemComponent, IServerSerializable
partial class WifiComponent : ItemComponent, IServerSerializable, IClientSerializable
{
private static readonly List<WifiComponent> list = new List<WifiComponent>();
@@ -56,7 +56,6 @@ namespace Barotrauma.Items.Components
}
}
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Can the component communicate with wifi components in another team's submarine (e.g. enemy sub in Combat missions, respawn shuttle). Needs to be enabled on both the component transmitting the signal and the component receiving it.", alwaysUseInstanceValues: true)]
public bool AllowCrossTeamCommunication
{
@@ -229,14 +228,20 @@ namespace Barotrauma.Items.Components
public void TransmitSignal(Signal signal, bool sentFromChat)
{
if (sentFromChat)
{
item.LastSentSignalRecipients.Clear();
}
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; }
@@ -366,5 +371,25 @@ namespace Barotrauma.Items.Components
element.Add(new XAttribute("channelmemory", string.Join(',', channelMemory)));
return element;
}
protected void SharedEventWrite(IWriteMessage msg)
{
msg.WriteRangedInteger(Channel, MinChannel, MaxChannel);
for (int i = 0; i < ChannelMemorySize; i++)
{
msg.WriteRangedInteger(channelMemory[i], MinChannel, MaxChannel);
}
}
protected void SharedEventRead(IReadMessage msg)
{
Channel = msg.ReadRangedInteger(MinChannel, MaxChannel);
for (int i = 0; i < ChannelMemorySize; i++)
{
channelMemory[i] = msg.ReadRangedInteger(MinChannel, MaxChannel);
}
}
}
}
@@ -66,6 +66,7 @@ namespace Barotrauma.Items.Components
private float editNodeDelay;
private bool locked;
public bool Locked
{
get
@@ -83,6 +84,13 @@ namespace Barotrauma.Items.Components
public float Length { get; private set; }
[Serialize(0.3f, IsPropertySaveable.No), Editable(MinValueFloat = 0.01f, MaxValueFloat = 10.0f, DecimalCount = 2)]
public float Width
{
get;
set;
}
[Serialize(5000.0f, IsPropertySaveable.No, description: "The maximum distance the wire can extend (in pixels).")]
public float MaxLength
{
@@ -884,8 +892,13 @@ namespace Barotrauma.Items.Components
protected override void RemoveComponentSpecific()
{
if (item.Container?.GetComponent<CircuitBox>() is { } circuitBox)
{
circuitBox.RemoveWire(this);
}
ClearConnections();
base.RemoveComponentSpecific();
#if CLIENT
if (DraggingWire == this) { draggingWire = null; }
overrideSprite?.Remove();
@@ -1,11 +1,13 @@
using FarseerPhysics;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using Barotrauma.Extensions;
namespace Barotrauma.Items.Components
{
@@ -17,6 +19,7 @@ namespace Barotrauma.Items.Components
public bool DistanceBasedForce { get; set; }
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Determines if the force fluctuates over time or if it stays constant.", alwaysUseInstanceValues: true)]
public bool ForceFluctuation { get; set; }
[Serialize(1.0f, IsPropertySaveable.Yes, description: "How much the fluctuation affects the force. 1 is the maximum fluctuation, 0 is no fluctuation.", alwaysUseInstanceValues: true)]
private float ForceFluctuationStrength
{
@@ -55,7 +58,65 @@ namespace Barotrauma.Items.Components
}
public PhysicsBody PhysicsBody { get; private set; }
private float Radius { get; set; }
private float radius;
[Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
public float Radius
{
get => radius;
set
{
if (radius == value) { return; }
radius = value;
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
}
}
private float width;
[Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
public float Width
{
get => width;
set
{
if (width == value) { return; }
width = value;
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
}
}
private float height;
[Editable, Serialize(0.0f, IsPropertySaveable.Yes)]
public float Height
{
get => height;
set
{
if (height == value) { return; }
height = value;
if (PhysicsBody != null) { RefreshPhysicsBodySize(); }
}
}
private float currentRadius, currentWidth, currentHeight;
private Vector2 bodyOffset;
[Editable, Serialize("0,0", IsPropertySaveable.Yes)]
public Vector2 BodyOffset
{
get => bodyOffset;
set
{
if (bodyOffset == value) { return; }
bodyOffset = value;
if (PhysicsBody != null) { SetPhysicsBodyPosition(); }
}
}
private float RadiusInDisplayUnits { get; set; }
private bool TriggeredOnce { get; set; }
private float CurrentForceFluctuation { get; set; } = 1.0f;
@@ -75,7 +136,24 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool MoveOutsideSub { get; set; }
public override bool IsActive
{
get => base.IsActive;
set
{
base.IsActive = value;
if (!IsActive)
{
TriggerActive = false;
triggerers.Clear();
}
}
}
private readonly LevelTrigger.TriggererType triggeredBy;
private readonly Identifier triggerSpeciesOrGroup;
private readonly PropertyConditional.LogicalComparison conditionals;
private readonly HashSet<Entity> triggerers = new HashSet<Entity>();
private readonly bool triggerOnce;
private readonly List<ISerializableEntity> statusEffectTargets = new List<ISerializableEntity>();
@@ -94,11 +172,20 @@ namespace Barotrauma.Items.Components
public TriggerComponent(Item item, ContentXElement element) : base(item, element)
{
string triggeredByAttribute = element.GetAttributeString("triggeredby", "Character");
if (!Enum.TryParse(triggeredByAttribute, out triggeredBy))
string triggeredByString = element.GetAttributeString("triggeredby", "Character");
if (!Enum.TryParse(triggeredByString, out triggeredBy))
{
DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByAttribute}\" is not a valid triggerer type.",
contentPackage: element.ContentPackage);
Identifier speciesOrGroup = triggeredByString.ToIdentifier();
if (CharacterPrefab.Prefabs.Any(p => p.MatchesSpeciesNameOrGroup(speciesOrGroup)))
{
triggerSpeciesOrGroup = speciesOrGroup;
triggeredBy = LevelTrigger.TriggererType.Character;
}
else
{
DebugConsole.ThrowError($"Error in ForceComponent config: \"{triggeredByString}\" is not a valid triggerer type.",
contentPackage: element.ContentPackage);
}
}
triggerOnce = element.GetAttributeBool("triggeronce", false);
string parentDebugName = $"TriggerComponent in {item.Name}";
@@ -115,36 +202,93 @@ namespace Barotrauma.Items.Components
break;
}
}
conditionals = PropertyConditional.LoadConditionals(element);
IsActive = true;
}
public override void OnItemLoaded()
{
base.OnItemLoaded();
float radiusAttribute = originalElement.GetAttributeFloat("radius", 10.0f);
Radius = ConvertUnits.ToSimUnits(radiusAttribute * item.Scale);
PhysicsBody = new PhysicsBody(0.0f, 0.0f, Radius, 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
RefreshPhysicsBodySize();
}
private void RefreshPhysicsBodySize()
{
PhysicsBody?.Remove();
currentWidth = ConvertUnits.ToSimUnits(Width * item.Scale);
currentHeight = ConvertUnits.ToSimUnits(Height * item.Scale);
if (currentWidth > 0 && currentHeight > 0)
{
UserData = item
};
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
PhysicsBody.FarseerBody.SetIsSensor(true);
PhysicsBody = new PhysicsBody(currentWidth, currentHeight, radius: 0.0f, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
UserData = item
};
}
else
{
currentRadius = Math.Max(ConvertUnits.ToSimUnits(Radius * item.Scale), 0.01f);
PhysicsBody = new PhysicsBody(width: 0.0f, height: 0.0f, radius: currentRadius, density: 1.5f, BodyType.Static, Physics.CollisionWall, LevelTrigger.GetCollisionCategories(triggeredBy))
{
UserData = item
};
}
SetPhysicsBodyPosition();
PhysicsBody.FarseerBody.SetIsSensor(originalElement.GetAttributeBool("sensor", true));
PhysicsBody.FarseerBody.OnCollision += OnCollision;
PhysicsBody.FarseerBody.OnSeparation += OnSeparation;
RadiusInDisplayUnits = ConvertUnits.ToDisplayUnits(PhysicsBody.Radius);
}
public void SetPhysicsBodyPosition(bool ignoreContacts = true)
{
if (PhysicsBody == null) { return; }
Vector2 offset = ConvertUnits.ToSimUnits(BodyOffset * item.Scale);
if (item.FlippedX)
{
offset.X = -offset.X;
}
if (item.FlippedY)
{
offset.Y = -offset.Y;
}
if (!MathUtils.NearlyEqual(item.RotationRad, 0))
{
Matrix transform = Matrix.CreateRotationZ(-item.RotationRad);
offset = Vector2.Transform(offset, transform);
}
if (ignoreContacts)
{
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition + offset, -item.RotationRad);
}
else
{
PhysicsBody.SetTransform(item.SimPosition + offset, -item.RotationRad);
}
PhysicsBody.UpdateDrawPosition();
}
public override void FlipX(bool relativeToSub)
{
SetPhysicsBodyPosition();
}
public override void FlipY(bool relativeToSub)
{
SetPhysicsBodyPosition();
}
public override void OnMapLoaded()
{
base.OnMapLoaded();
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
SetPhysicsBodyPosition(true);
PhysicsBody.Submarine = item.Submarine;
}
private bool OnCollision(Fixture sender, Fixture other, Contact contact)
{
if (LevelTrigger.GetEntity(other) is not Entity entity) { return false; }
if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; }
if (!LevelTrigger.IsTriggeredByEntity(entity, triggeredBy, triggerSpeciesOrGroup, conditionals, mustBeOnSpecificSub: (!MoveOutsideSub, item.Submarine))) { return false; }
triggerers.Add(entity);
return true;
}
@@ -174,7 +318,12 @@ namespace Barotrauma.Items.Components
item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition), item.Rotation);
item.CurrentHull = null;
item.Submarine = null;
PhysicsBody.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
SetPhysicsBodyPosition();
PhysicsBody.Submarine = item.Submarine;
}
else if (item.body is { BodyType: BodyType.Dynamic })
{
SetPhysicsBodyPosition();
PhysicsBody.Submarine = item.Submarine;
}
@@ -187,11 +336,34 @@ namespace Barotrauma.Items.Components
{
TriggeredOnce = true;
IsActive = false;
triggerers.Clear();
}
}
TriggerActive = triggerers.Any();
if (TriggerActive && conditionals != null)
{
switch (conditionals.LogicalOperator)
{
case PropertyConditional.LogicalOperatorType.And:
{
if (triggerers.Any(t => !PropertyConditional.CheckConditionals((ISerializableEntity)t, conditionals.Conditionals, conditionals.LogicalOperator)))
{
// Some of the conditionals doesn't match
IsActive = false;
}
break;
}
case PropertyConditional.LogicalOperatorType.Or:
{
if (triggerers.None(t => !PropertyConditional.CheckConditionals((ISerializableEntity)t, conditionals.Conditionals, conditionals.LogicalOperator)))
{
// None of the conditionals match
IsActive = false;
}
break;
}
}
}
if (ForceFluctuation && TriggerActive && (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
{
@@ -208,7 +380,7 @@ namespace Barotrauma.Items.Components
foreach (Entity triggerer in triggerers)
{
LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets);
LevelTrigger.ApplyStatusEffects(statusEffects, item.WorldPosition, triggerer, deltaTime, statusEffectTargets, targetItem: Item);
if (triggerer is IDamageable damageable)
{
@@ -260,12 +432,17 @@ namespace Barotrauma.Items.Components
private void ApplyForce(PhysicsBody body, float multiplier = 1.0f)
{
Vector2 diff = ConvertUnits.ToDisplayUnits(PhysicsBody.SimPosition - body.SimPosition);
Vector2 diff = ConvertUnits.ToDisplayUnits(item.SimPosition - body.SimPosition);
if (diff.LengthSquared() < 0.0001f) { return; }
float distanceFactor = DistanceBasedForce ? LevelTrigger.GetDistanceFactor(body, PhysicsBody, RadiusInDisplayUnits) : 1.0f;
if (distanceFactor <= 0.0f) { return; }
Vector2 force = distanceFactor * (CurrentForceFluctuation * Force) * Vector2.Normalize(diff) * multiplier;
if (force.LengthSquared() < 0.01f) { return; }
if (body.Mass < 1)
{
//restrict the force if the body is very light, otherwise it can end up moving at a speed that breaks physics
force *= body.Mass;
}
body.ApplyForce(force);
}
@@ -273,14 +450,7 @@ namespace Barotrauma.Items.Components
{
if (PhysicsBody != null)
{
if (ignoreContacts)
{
PhysicsBody.SetTransformIgnoreContacts(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
else
{
PhysicsBody.SetTransform(PhysicsBody.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
}
SetPhysicsBodyPosition(ignoreContacts);
PhysicsBody.Submarine = item.Submarine;
}
}
@@ -14,6 +14,7 @@ namespace Barotrauma.Items.Components
partial class Turret : Powered, IDrawableComponent, IServerSerializable
{
private Sprite barrelSprite, railSprite;
private Sprite barrelSpriteBroken, railSpriteBroken;
private readonly List<(Sprite sprite, Vector2 position)> chargeSprites = new List<(Sprite sprite, Vector2 position)>();
private readonly List<Sprite> spinningBarrelSprites = new List<Sprite>();
@@ -89,6 +90,8 @@ namespace Barotrauma.Items.Components
private List<LightComponent> lightComponents;
private Projectile lastProjectile;
private readonly bool isSlowTurret;
public float Rotation { get; private set; }
@@ -320,9 +323,22 @@ namespace Barotrauma.Items.Components
[Serialize("", IsPropertySaveable.Yes, description: "[Auto Operate] Group or SpeciesName that the AI ignores when the turret is operated automatically."),
Editable(TransferToSwappedItem = true)]
public Identifier FriendlyTag { get; private set; }
[Serialize("OwnSub", IsPropertySaveable.Yes, description: "[Auto Operate] Team that the turret considers friendly."),
Editable(TransferToSwappedItem = true)]
public TeamType FriendlyTeamType { get; private set; }
public enum TeamType
{
OwnSub,
Team1,
Team2,
FriendlyNPC,
NoneTeam
}
#endregion
private const string SetAutoOperateConnection = "set_auto_operate";
private const string ToggleAutoOperateConnection = "toggle_auto_operate";
@@ -330,7 +346,7 @@ namespace Barotrauma.Items.Components
: base(item, element)
{
IsActive = true;
foreach (var subElement in element.Elements())
{
switch (subElement.Name.ToString().ToLowerInvariant())
@@ -341,6 +357,12 @@ namespace Barotrauma.Items.Components
case "railsprite":
railSprite = new Sprite(subElement);
break;
case "barrelspritebroken":
barrelSpriteBroken = new Sprite(subElement);
break;
case "railspritebroken":
railSpriteBroken = new Sprite(subElement);
break;
case "chargesprite":
chargeSprites.Add((new Sprite(subElement), subElement.GetAttributeVector2("chargetarget", Vector2.Zero)));
break;
@@ -376,6 +398,7 @@ namespace Barotrauma.Items.Components
base.OnMapLoaded();
if (loadedRotationLimits.HasValue) { RotationLimits = loadedRotationLimits.Value; }
if (loadedBaseRotation.HasValue) { BaseRotation = loadedBaseRotation.Value; }
if (loadedFriendlyTeamType.HasValue) { FriendlyTeamType = loadedFriendlyTeamType.Value; }
targetRotation = Rotation;
UpdateTransformedBarrelPos();
if (!AllowAutoOperateWithWiring &&
@@ -779,6 +802,12 @@ namespace Barotrauma.Items.Components
if (launchedProjectile != null || LaunchWithoutProjectile)
{
if (launchedProjectile?.Item.GetComponent<Rope>() != null &&
lastProjectile?.Item.GetComponent<Rope>() is { SnapWhenWeaponFiredAgain: true } rope)
{
rope.Snap();
}
if (projectiles.Any())
{
foreach (Projectile projectile in projectiles)
@@ -806,6 +835,8 @@ namespace Barotrauma.Items.Components
}
}
lastProjectile = launchedProjectile;
#if SERVER
if (character != null && launchedProjectile != null)
{
@@ -900,6 +931,10 @@ namespace Barotrauma.Items.Components
projectileComponent.Attack.DamageMultiplier = (1f * DamageMultiplier) + (TinkeringDamageIncrease * tinkeringStrength);
}
projectileComponent.Use(null, LaunchImpulse);
if (item.GetComponent<TriggerComponent>() is { } trigger)
{
projectileComponent.IgnoredBodies.Add(trigger.PhysicsBody.FarseerBody);
}
projectile.GetComponent<Rope>()?.Attach(item, projectile);
projectileComponent.User = user;
@@ -1027,7 +1062,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;
@@ -1117,7 +1152,7 @@ namespace Barotrauma.Items.Components
if (target is Hull targetHull)
{
Vector2 barrelDir = GetBarrelDir();
if (!MathUtils.GetLineRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
if (!MathUtils.GetLineWorldRectangleIntersection(item.WorldPosition, item.WorldPosition + barrelDir * AIRange, targetHull.WorldRect, out _))
{
return;
}
@@ -1351,7 +1386,7 @@ namespace Barotrauma.Items.Components
}
// Don't aim monsters that are inside any submarine.
if (!enemy.IsHuman && enemy.CurrentHull != null) { continue; }
if (HumanAIController.IsFriendly(character, enemy)) { continue; }
if (HumanAIController.IsFriendly(character, enemy, ignoreHuskDisguising: true)) { continue; }
// Don't shoot at captured enemies.
if (enemy.LockHands) { continue; }
float dist = Vector2.DistanceSquared(enemy.WorldPosition, item.WorldPosition);
@@ -1371,7 +1406,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;
@@ -1675,22 +1710,34 @@ namespace Barotrauma.Items.Components
return true;
}
private CharacterTeamType GetFriendlyTeam()
{
return FriendlyTeamType switch
{
TeamType.Team1 => CharacterTeamType.Team1,
TeamType.Team2 => CharacterTeamType.Team2,
TeamType.FriendlyNPC => CharacterTeamType.FriendlyNPC,
TeamType.NoneTeam => CharacterTeamType.None,
TeamType.OwnSub => item.Submarine?.TeamID ?? CharacterTeamType.None,
_ => throw new NotImplementedException(),
};
}
private bool IsValidTargetForAutoOperate(Character target, Identifier friendlyTag)
{
if (!friendlyTag.IsEmpty)
{
if (target.SpeciesName.Equals(friendlyTag) || target.Group.Equals(friendlyTag)) { return false; }
}
CharacterTeamType friendlyTeam = GetFriendlyTeam();
if (target.TeamID == friendlyTeam) { return false; }
bool isHuman = target.IsHuman || target.Group == CharacterPrefab.HumanSpeciesName;
if (isHuman)
{
if (item.Submarine != null)
{
if (item.Submarine.Info.IsOutpost) { return false; }
// Check that the target is not in the friendly team, e.g. pirate or a hostile player sub (PvP).
return !target.IsOnFriendlyTeam(item.Submarine.TeamID) && TargetHumans;
}
return TargetHumans;
return !target.IsOnFriendlyTeam(friendlyTeam) && TargetHumans;
}
else
{
@@ -1719,7 +1766,7 @@ namespace Barotrauma.Items.Components
{
if (user != null)
{
if (HumanAIController.IsFriendly(user, targetCharacter))
if (HumanAIController.IsFriendly(user, targetCharacter, ignoreHuskDisguising: true))
{
return false;
}
@@ -1739,8 +1786,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 == GetFriendlyTeam()) { return false; }
}
else
{
if (sub.TeamID == Item.Submarine.TeamID) { return false; }
}
}
else if (targetBody.UserData is not Voronoi2.VoronoiCell { IsDestructible: true })
{
@@ -1758,6 +1812,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);
});
@@ -1792,6 +1848,8 @@ namespace Barotrauma.Items.Components
barrelSprite?.Remove(); barrelSprite = null;
railSprite?.Remove(); railSprite = null;
barrelSpriteBroken?.Remove(); barrelSpriteBroken = null;
railSpriteBroken?.Remove(); railSpriteBroken = null;
#if CLIENT
crosshairSprite?.Remove(); crosshairSprite = null;
@@ -1974,11 +2032,27 @@ namespace Barotrauma.Items.Components
private Vector2? loadedRotationLimits;
private float? loadedBaseRotation;
private TeamType? loadedFriendlyTeamType;
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
loadedRotationLimits = componentElement.GetAttributeVector2("rotationlimits", RotationLimits);
loadedBaseRotation = componentElement.GetAttributeFloat("baserotation", componentElement.Parent.GetAttributeFloat("rotation", BaseRotation));
//backwards compatibility: previously None made the turret consider the team the submarine/outpost belongs as the friendly team
if (componentElement.GetAttribute("FriendlyTeam") is { } friendlyTeamAttribute)
{
CharacterTeamType friendlyTeam = XMLExtensions.ParseEnumValue(friendlyTeamAttribute.Value, defaultValue: CharacterTeamType.None, friendlyTeamAttribute);
loadedFriendlyTeamType = friendlyTeam switch
{
CharacterTeamType.None => TeamType.OwnSub,
CharacterTeamType.Team1 => TeamType.Team1,
CharacterTeamType.Team2 => TeamType.Team2,
CharacterTeamType.FriendlyNPC => TeamType.FriendlyNPC,
_ => throw new NotImplementedException()
};
}
}
public override void OnItemLoaded()
@@ -1991,6 +2065,8 @@ namespace Barotrauma.Items.Components
if (item.FlippedX) { FlipX(relativeToSub: false); }
if (item.FlippedY) { FlipY(relativeToSub: false); }
}
UpdateTransformedBarrelPos();
UpdateLightComponents();
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData extraData = null)
@@ -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
{
@@ -269,6 +270,13 @@ namespace Barotrauma
IsInitialized = true;
}
public void Remove()
{
Sprite?.Remove();
//don't use the Picker setter, because it causes the sprite to be re-initialized for "no character"
_picker = null;
}
}
}
@@ -350,7 +358,7 @@ namespace Barotrauma.Items.Components
damageModifiers = new List<DamageModifier>();
SkillModifiers = new Dictionary<Identifier, float>();
int spriteCount = element.Elements().Count(x => x.Name.ToString() == "sprite");
int spriteCount = element.Elements().Count(x => x.Name.ToString().ToLowerInvariant() == "sprite");
Variants = element.GetAttributeInt("variants", 0);
variant = Rand.Range(1, Variants + 1, Rand.RandSync.ServerAndClient);
wearableSprites = new WearableSprite[spriteCount];
@@ -569,8 +577,7 @@ namespace Barotrauma.Items.Components
foreach (WearableSprite wearableSprite in wearableSprites)
{
wearableSprite?.Sprite?.Remove();
wearableSprite.Picker = null;
wearableSprite.Remove();
}
}
@@ -1,5 +1,6 @@
#nullable enable
using Barotrauma.Extensions;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
@@ -138,7 +139,23 @@ namespace Barotrauma
// We only check vanilla item prefabs because we don't want to force modders to define all vanilla container tags.
foreach (var vanillaTag in vanillaContainerTags)
{
if (Prefabs.All(p => p.Identifier != vanillaTag))
//mission types can be appended to the respawn container tags for mission-specific respawn items,
//check those too so we don't unnecessarily log a warning
if (vanillaTag.StartsWith(Tags.RespawnContainer))
{
bool isMissionSpecificRespawnContainer = false;
foreach (var missionType in MissionPrefab.Prefabs.Select(p => p.Type).Distinct())
{
if (vanillaTag == Tags.RespawnContainer + "_" + missionType)
{
isMissionSpecificRespawnContainer = true;
break;
}
}
if (isMissionSpecificRespawnContainer) { continue; }
}
if (Prefabs.None(p => p.Identifier == vanillaTag))
{
DebugConsole.AddWarning($"Container tag \"{vanillaTag}\" is used in vanilla item prefabs but not defined in a ContainerTagPrefab.");
}
@@ -58,7 +58,12 @@ namespace Barotrauma
return true;
}
public bool CanBePut(ItemPrefab itemPrefab, float? condition = null, int? quality = null)
/// <summary>
/// Can an instance of the item prefab be put into the slot?
/// Note that if the condition and quality aren't given, they are ignored, and the method can return true even if the item can't go in the slot
/// due to it being occupied by another item with a different condition or quality (which disallows stacking).
/// </summary>
public bool CanProbablyBePut(ItemPrefab itemPrefab, float? condition = null, int? quality = null)
{
if (itemPrefab == null) { return false; }
if (items.Count > 0)
@@ -326,13 +331,16 @@ namespace Barotrauma
}
#endif
}
protected IEnumerable<Item> GetAllItems(bool checkForDuplicates)
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
@@ -344,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;
@@ -359,7 +367,7 @@ namespace Barotrauma
yield return item;
}
}
}
}
}
private void NotifyItemComponentsOfChange()
@@ -415,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();
}
@@ -429,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);
}
@@ -471,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; }
@@ -514,11 +526,16 @@ 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);
}
public bool CanBePut(ItemPrefab itemPrefab, float? condition = null, int? quality = null)
/// <summary>
/// Can an instance of the item prefab be put into the inventory?
/// Note that if the condition and quality aren't given, they are ignored, and the method can return true even if the item can't go in the inventory
/// due to the slots being occupied by another item with a different condition or quality (which disallows stacking).
/// </summary>
public bool CanProbablyBePut(ItemPrefab itemPrefab, float? condition = null, int? quality = null)
{
for (int i = 0; i < capacity; i++)
{
@@ -529,8 +546,8 @@ namespace Barotrauma
public virtual bool CanBePutInSlot(ItemPrefab itemPrefab, int i, float? condition = null, int? quality = null)
{
if (i < 0 || i >= slots.Length) { return false; }
return slots[i].CanBePut(itemPrefab, condition);
if (!IsIndexInRange(i)) { return false; }
return slots[i].CanProbablyBePut(itemPrefab, condition, quality);
}
public int HowManyCanBePut(ItemPrefab itemPrefab, float? condition = null)
@@ -545,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);
}
@@ -563,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";
@@ -627,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);
@@ -796,14 +813,15 @@ namespace Barotrauma
{
for (int j = 0; j < otherInventory.capacity; j++)
{
if (otherInventory.slots[j].Contains(item))
// in case the item is in multiple slots like OuterClothes | InnerClothes, prevent it from being added twice to the list
if (otherInventory.slots[j].Contains(item) && !stackedItems.Contains(item))
{
stackedItems.AddRange(otherInventory.slots[j].Items);
otherInventory.slots[j].RemoveAllItems();
}
}
}
else
else if (!stackedItems.Contains(item))
{
stackedItems.Add(item);
otherInventory.slots[otherIndex].RemoveItem(item);
@@ -1076,10 +1094,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();
@@ -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);
}
@@ -24,9 +24,10 @@ namespace Barotrauma
ItemStat = 9,
DroppedStack = 10,
SetHighlight = 11,
SwapItem = 12,
MinValue = 0,
MaxValue = 11
MaxValue = 12
}
public interface IEventData : NetEntityEvent.IData
@@ -150,5 +151,18 @@ namespace Barotrauma
Upgrade = upgrade;
}
}
private readonly struct SwapItemEventData : IEventData
{
public EventType EventType => EventType.SwapItem;
public readonly ItemPrefab NewItem;
public readonly ushort NewId;
public SwapItemEventData(ItemPrefab newItem, ushort newId)
{
NewItem = newItem;
NewId = newId;
}
}
}
}
@@ -55,7 +55,7 @@ namespace Barotrauma
{
if (i < 0 || i >= slots.Length) { return false; }
if (!container.CanBeContained(itemPrefab, i)) { return false; }
return itemPrefab != null && slots[i].CanBePut(itemPrefab, condition, quality) && slots[i].Items.Count < container.GetMaxStackSize(i);
return itemPrefab != null && slots[i].CanProbablyBePut(itemPrefab, condition, quality) && slots[i].Items.Count < container.GetMaxStackSize(i);
}
public override int HowManyCanBePut(ItemPrefab itemPrefab, int i, float? condition, bool ignoreItemsInSlot = false)
@@ -63,9 +63,12 @@ namespace Barotrauma
OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f));
CopyCondition = element.GetAttributeBool("copycondition", false);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
Identifier[] defaultRequiredDeconstructor = new Identifier[] { "deconstructor".ToIdentifier() };
RequiredDeconstructor = element.GetAttributeIdentifierArray("requireddeconstructor",
element.Parent?.GetAttributeIdentifierArray("requireddeconstructor", Array.Empty<Identifier>()) ?? Array.Empty<Identifier>());
element.Parent?.GetAttributeIdentifierArray("requireddeconstructor", null) ?? defaultRequiredDeconstructor);
RequiredOtherItem = element.GetAttributeIdentifierArray("requiredotheritem", Array.Empty<Identifier>());
ActivateButtonText = element.GetAttributeString("activatebuttontext", string.Empty);
InfoText = element.GetAttributeString("infotext", string.Empty);
InfoTextOnOtherItemMissing = element.GetAttributeString("infotextonotheritemmissing", string.Empty);
@@ -197,7 +200,8 @@ namespace Barotrauma
{
Tag = tag;
using MD5 md5 = MD5.Create();
UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(tag, md5);
//add "tag:" to the hash, so we don't get a hash collision between recipes configured as identifier="smth" and tag="smth"
UintIdentifier = ToolBoxCore.IdentifierToUint32Hash(("tag:" + tag).ToIdentifier(), md5);
}
public override string ToString()
@@ -216,13 +220,15 @@ namespace Barotrauma
public readonly ImmutableArray<Identifier> SuitableFabricatorIdentifiers;
public readonly float RequiredTime;
public readonly int RequiredMoney;
public readonly bool RequiresRecipe;
public readonly bool RequiresRecipe;
public readonly bool HideIfNoRecipe;
public readonly float OutCondition; //Percentage-based from 0 to 1
public readonly ImmutableArray<Skill> RequiredSkills;
public readonly uint RecipeHash;
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)
@@ -250,6 +256,7 @@ namespace Barotrauma
}
var requiredItems = new List<RequiredItem>();
RequiresRecipe = element.GetAttributeBool("requiresrecipe", false);
HideIfNoRecipe = element.GetAttributeBool("hideifnorecipe", false);
Amount = element.GetAttributeInt("amount", 1);
int limitDefault = element.GetAttributeInt("fabricationlimit", -1);
@@ -257,6 +264,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)
{
@@ -382,6 +390,7 @@ namespace Barotrauma
public readonly int Amount;
public readonly bool CampaignOnly;
public readonly bool NotCampaign;
public readonly bool NotPvP;
public readonly bool TransferOnlyOnePerContainer;
public readonly bool AllowTransfersHere = true;
@@ -399,6 +408,7 @@ namespace Barotrauma
MinCondition = element.GetAttributeFloat("mincondition", 0f);
CampaignOnly = element.GetAttributeBool("campaignonly", CampaignOnly);
NotCampaign = element.GetAttributeBool("notcampaign", NotCampaign);
NotPvP = element.GetAttributeBool("notpvp", NotPvP);
TransferOnlyOnePerContainer = element.GetAttributeBool("TransferOnlyOnePerContainer", TransferOnlyOnePerContainer);
AllowTransfersHere = element.GetAttributeBool("AllowTransfersHere", AllowTransfersHere);
@@ -508,7 +518,7 @@ namespace Barotrauma
/// </summary>
public bool IsOverride => Prefabs.IsOverride(this);
private readonly XElement originalElement;
private readonly ContentXElement originalElement;
public ContentXElement ConfigElement { get; private set; }
public ImmutableArray<DeconstructItem> DeconstructItems { get; private set; }
@@ -729,6 +739,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No, description: "Should the character who's selected the item grab it (hold their hand on it, the same way as they do when repairing)? Defaults to true on items that have an ItemContainer component.")]
public bool GrabWhenSelected { get; set; }
[Serialize(true, IsPropertySaveable.No, description: "Are AI characters allowed to deselect the item when they're idling (and wander off?).")]
public bool AllowDeselectWhenIdling { get; private set; }
private float health;
[Serialize(100.0f, IsPropertySaveable.No)]
@@ -755,6 +768,9 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No)]
public bool DamagedByExplosions { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
public bool DamagedByContainedItemExplosions { get; private set; }
[Serialize(1f, IsPropertySaveable.No)]
public float ExplosionDamageMultiplier { get; private set; }
@@ -882,7 +898,8 @@ namespace Barotrauma
int extraStackSize = inventory switch
{
ItemInventory { Owner: Item it } i => (int)it.StatManager.GetAdjustedValueAdditive(ItemTalentStats.ExtraStackSize, i.ExtraStackSize),
CharacterInventory { Owner: Character { Info: { } info } } i => i.ExtraStackSize + (int)info.GetSavedStatValueWithAll(StatTypes.InventoryExtraStackSize, Category.ToIdentifier()),
CharacterInventory { Owner: Character { Info: { } info } } i =>
i.ExtraStackSize + EnumExtensions.GetIndividualFlags(Category).Sum(c => (int)info.GetSavedStatValueWithAll(StatTypes.InventoryExtraStackSize, c.ToIdentifier())),
not null => inventory.ExtraStackSize,
null => 0
};
@@ -991,7 +1008,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)
: "";
@@ -1044,11 +1061,13 @@ namespace Barotrauma
AllowAsExtraCargo = ConfigElement.GetAttributeBool("allowasextracargo", false);
}
this.tags = ConfigElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToImmutableHashSet();
if (!Tags.Any())
List<Identifier> tags = ConfigElement.GetAttributeIdentifierArray("tags", Array.Empty<Identifier>()).ToList();
//this was previously handled in ItemComponent, moved here to make it part of the immutable tags of the item
if (ConfigElement.Descendants().Any(e => e.NameAsIdentifier() == "lightcomponent"))
{
this.tags = ConfigElement.GetAttributeIdentifierArray("Tags", Array.Empty<Identifier>()).ToImmutableHashSet();
tags.Add("light".ToIdentifier());
}
this.tags = tags.ToImmutableHashSet();
if (ConfigElement.GetAttribute("cargocontainername") != null)
{
@@ -1390,10 +1409,14 @@ namespace Barotrauma
public bool CanBeBoughtFrom(Location.StoreInfo store, out PriceInfo priceInfo)
{
priceInfo = GetPriceInfo(store);
Identifier? faction = store?.Location.Faction?.Prefab.Identifier;
Identifier? secondaryFaction = store?.Location.SecondaryFaction?.Prefab.Identifier;
return
priceInfo is { CanBeBought: true } &&
(store?.Location.LevelData?.Difficulty ?? 0) >= priceInfo.MinLevelDifficulty &&
(!priceInfo.MinReputation.Any() || priceInfo.MinReputation.Any(p => store?.Location.Faction?.Prefab.Identifier == p.Key || store?.Location.SecondaryFaction?.Prefab.Identifier == p.Key));
(priceInfo.RequiredFaction.IsEmpty || faction == priceInfo.RequiredFaction || secondaryFaction == priceInfo.RequiredFaction) &&
(!priceInfo.MinReputation.Any() || priceInfo.MinReputation.Any(p => faction == p.Key || secondaryFaction == p.Key));
}
public bool CanBeBoughtFrom(Location location)
@@ -1574,7 +1597,7 @@ namespace Barotrauma
public void InheritFrom(ItemPrefab parent)
{
ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement, CheckXML).FromPackage(ConfigElement.ContentPackage);
ConfigElement = originalElement.CreateVariantXML(parent.ConfigElement, CheckXML);
ParseConfigElement(parent);
void CheckXML(XElement originalElement, XElement variantElement, XElement result)
@@ -148,13 +148,8 @@ namespace Barotrauma
public string JoinedIdentifiers
{
get { return string.Join(",", Identifiers); }
set
{
if (value == null) return;
Identifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToImmutableHashSet();
}
get => Identifiers.ConvertToString();
set => Identifiers = value.ToIdentifiers().ToImmutableHashSet();
}
/// <summary>
@@ -164,13 +159,8 @@ namespace Barotrauma
public string JoinedExcludedIdentifiers
{
get { return string.Join(",", ExcludedIdentifiers); }
set
{
if (value == null) return;
ExcludedIdentifiers = value.Split(',').Select(s => s.Trim()).ToIdentifiers().ToImmutableHashSet();
}
get => ExcludedIdentifiers.ConvertToString();
set => ExcludedIdentifiers = value.ToIdentifiers().ToImmutableHashSet();
}
public bool MatchesItem(Item item)
@@ -321,7 +311,9 @@ namespace Barotrauma
#if CLIENT
foreach (InputType inputType in Enum.GetValues(typeof(InputType)))
{
msg = msg.Replace("[" + inputType.ToString().ToLowerInvariant() + "]", GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType));
string inputTag = $"[{inputType.ToString().ToLowerInvariant()}]";
if (!msg.Contains(inputTag)) { continue; }
msg = msg.Replace(inputTag, GameSettings.CurrentConfig.KeyMap.KeyBindText(inputType));
}
Msg = msg;
#endif