Unstable 1.1.14.0

This commit is contained in:
Markus Isberg
2023-10-02 16:43:54 +03:00
parent 94f5a93a0c
commit cf8f0de659
606 changed files with 21906 additions and 11456 deletions
@@ -26,6 +26,8 @@ namespace Barotrauma
public static readonly List<InvSlotType> AnySlot = new List<InvSlotType>() { InvSlotType.Any };
public static bool IsHandSlotType(InvSlotType s) => s.HasFlag(InvSlotType.LeftHand) || s.HasFlag(InvSlotType.RightHand);
protected bool[] IsEquipped;
/// <summary>
@@ -92,7 +94,7 @@ namespace Barotrauma
DebugConsole.ThrowError($"Character \"{character.SpeciesName}\" is configured to spawn with more items than it has inventory capacity for.");
}
#if DEBUG
else if (itemCount > capacity - 2)
else if (itemCount > capacity - 2 && !character.IsPet && capacity > 0)
{
DebugConsole.ThrowError(
$"Character \"{character.SpeciesName}\" is configured to spawn with so many items it will have less than 2 free inventory slots. " +
@@ -132,6 +134,13 @@ namespace Barotrauma
DebugConsole.ThrowError(errorMsg);
GameAnalyticsManager.AddErrorEventOnce("CharacterInventory:FailedToSpawnInitialItem", GameAnalyticsManager.ErrorSeverity.Error, errorMsg);
}
else if (!character.Enabled)
{
foreach (var heldItem in character.HeldItems)
{
if (item.body != null) { item.body.Enabled = false; }
}
}
});
}
}
@@ -207,6 +216,10 @@ namespace Barotrauma
base.RemoveItem(item);
#if CLIENT
CreateSlots();
if (character == Character.Controlled)
{
character.SelectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
}
#endif
CharacterHUD.RecreateHudTextsIfControlling(character);
//if the item was equipped and there are more items in the same stack, equip one of those items
@@ -509,14 +522,20 @@ namespace Barotrauma
if (character == Character.Controlled)
{
HintManager.OnObtainedItem(character, item);
character.SelectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
}
#endif
CharacterHUD.RecreateHudTextsIfControlling(character);
if (item.CampaignInteractionType == CampaignMode.InteractionType.Cargo)
{
item.CampaignInteractionType = CampaignMode.InteractionType.None;
item.AssignCampaignInteractionType(CampaignMode.InteractionType.None);
}
}
protected override void CreateNetworkEvent(Range slotRange)
{
GameMain.NetworkMember?.CreateEntityEvent(character, new Character.InventoryStateEventData(slotRange));
}
}
}
@@ -975,9 +975,9 @@ namespace Barotrauma.Items.Components
docked = false;
Item.Submarine.RefreshOutdoorNodes();
Item.Submarine.EnableObstructedWaypoints(DockingTarget.Item.Submarine);
obstructedWayPointsDisabled = false;
Item.Submarine.RefreshOutdoorNodes();
DockingTarget.Undock();
DockingTarget = null;
@@ -1111,8 +1111,8 @@ namespace Barotrauma.Items.Components
}
if (!obstructedWayPointsDisabled && dockingState >= 0.99f)
{
Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine);
Item.Submarine.RefreshOutdoorNodes();
Item.Submarine.DisableObstructedWayPoints(DockingTarget?.Item.Submarine);
obstructedWayPointsDisabled = true;
}
}
@@ -41,6 +41,8 @@ namespace Barotrauma.Items.Components
}
private bool isStuck;
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool IsStuck
{
get { return isStuck; }
@@ -49,7 +51,10 @@ namespace Barotrauma.Items.Components
if (isStuck == value) { return; }
isStuck = value;
#if SERVER
item.CreateServerEvent(this);
if (item.FullyInitialized)
{
item.CreateServerEvent(this);
}
#endif
}
}
@@ -105,7 +110,7 @@ namespace Barotrauma.Items.Components
public bool CanBeWelded = true;
private float stuck;
[Serialize(0.0f, IsPropertySaveable.No, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")]
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")]
public float Stuck
{
get { return stuck; }
@@ -181,7 +186,11 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")]
public bool HasIntegratedButtons { get; private set; }
[ConditionallyEditable(ConditionallyEditable.ConditionType.HasIntegratedButtons),
Serialize(true, IsPropertySaveable.No, description: "If the door has integrated buttons, should clicking on it perform the default action of opening the door? Can be used in conjunction with the \"activate_out\" output to pass a signal to a circuit without toggling the door when someone tries to open/close the door.")]
public bool ToggleWhenClicked { get; private set; }
public float OpenState
{
get { return openState; }
@@ -342,8 +351,12 @@ namespace Barotrauma.Items.Components
OnFailedToOpen();
return;
}
item.SendSignal("1", "activate_out");
lastUser = user;
SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked);
if (ToggleWhenClicked)
{
SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked);
}
}
public override bool Select(Character character)
@@ -389,8 +402,6 @@ namespace Barotrauma.Items.Components
return;
}
bool isClosing = false;
if ((!IsStuck && !IsJammed) || !isOpen)
{
@@ -18,8 +18,6 @@ namespace Barotrauma.Items.Components
const int MaxNodes = 100;
const float MaxNodeDistance = 150.0f;
private bool waitForVoltageRecalculation;
public struct Node
{
public Vector2 WorldPosition;
@@ -140,10 +138,6 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return false; }
if (character != null && !CharacterUsable) { return false; }
CurrPowerConsumption = powerConsumption;
Voltage = 0.0f;
waitForVoltageRecalculation = true;
charging = true;
timer = Duration;
IsActive = true;
@@ -159,12 +153,6 @@ namespace Barotrauma.Items.Components
#if CLIENT
frameOffset = Rand.Int(electricitySprite.FrameCount);
#endif
if (waitForVoltageRecalculation)
{
waitForVoltageRecalculation = false;
return;
}
if (timer <= 0.0f)
{
if (reloadTimer > 0.0f)
@@ -179,40 +167,49 @@ namespace Barotrauma.Items.Components
timer -= deltaTime;
if (charging)
{
if (GetAvailableInstantaneousBatteryPower() >= PowerConsumption)
bool hasPower = false;
if (item.Connections == null)
{
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
float neededPower = PowerConsumption;
while (neededPower > 0.0001f && batteries.Count > 0)
//no connections and can't be wired = must be powered by something like batteries
hasPower = Voltage > MinVoltage;
}
else
{
hasPower = GetAvailableInstantaneousBatteryPower() >= PowerConsumption;
}
if (hasPower)
{
var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f);
int batteryCount = batteries.Count();
if (batteryCount > 0)
{
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
float takePower = neededPower / batteries.Count;
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
foreach (PowerContainer battery in batteries)
float neededPower = PowerConsumption;
while (neededPower > 0.0001f)
{
neededPower -= takePower;
battery.Charge -= takePower / 3600.0f;
#if SERVER
if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); }
#endif
float takePower = neededPower / batteryCount;
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
foreach (PowerContainer battery in batteries)
{
neededPower -= takePower;
battery.Charge -= takePower / 3600.0f;
#if SERVER
if (GameMain.Server != null) { battery.Item.CreateServerEvent(battery); }
#endif
}
}
}
Discharge();
}
else if (Voltage > MinVoltage)
{
Discharge();
}
}
}
/// <summary>
/// Discharge coil only draws power when charging
/// Discharge coil doesn't consume grid power, directly takes from the batteries on its grid instead.
/// </summary>
public override float GetCurrentPowerConsumption(Connection connection = null)
public override float GetCurrentPowerConsumption(Connection conn = null)
{
return charging && IsActive ? PowerConsumption : 0;
return 0;
}
public override void UpdateBroken(float deltaTime, Camera cam)
@@ -294,10 +291,24 @@ namespace Barotrauma.Items.Components
if (!submarinesInRange.Contains(structure.Submarine)) { continue; }
if (OutdoorsOnly)
{
//check if the structure is within a hull
//add a small offset away from the sub's center so structures right at the edge of a hull are still valid
Vector2 offset = Vector2.Normalize(structure.WorldPosition - structure.Submarine.WorldPosition);
if (Hull.FindHull(structure.Position + offset * Submarine.GridSize, useWorldCoordinates: false) != null) { continue; }
//check if there's a hull at either side of the wall
Vector2 normal = new Vector2(
(float)-Math.Sin(structure.IsHorizontal ? -structure.BodyRotation : MathHelper.PiOver2 - structure.BodyRotation),
(float)Math.Cos(structure.IsHorizontal ? -structure.BodyRotation : MathHelper.PiOver2 - structure.BodyRotation));
Vector2 structurePos = structure.Position;
float offsetAmount = Submarine.GridSize.X * 2;
if (structure.HasBody)
{
structurePos = ConvertUnits.ToDisplayUnits(structure.Bodies.First().Position);
offsetAmount = Math.Max(
offsetAmount,
structure.IsHorizontal ? structure.BodyHeight : structure.BodyWidth);
}
if (Hull.FindHull(structurePos + normal * offsetAmount, useWorldCoordinates: false) != null &&
Hull.FindHull(structurePos - normal * offsetAmount, useWorldCoordinates: false) != null)
{
continue;
}
}
}
@@ -586,7 +597,7 @@ namespace Barotrauma.Items.Components
case "trigger_in":
if (signal.value != "0")
{
item.Use(1.0f, null);
item.Use(1.0f);
}
break;
}
@@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components
var (x, y, z, w) = Parent.GrowthWeights;
float[] weights = { x, y, z, w };
value = pool.RandomElementByWeight(i => weights[i]);
value = pool.GetRandomByWeight(i => weights[i], Rand.RandSync.Unsynced);
}
return (TileSide) (1 << value);
@@ -416,8 +416,8 @@ namespace Barotrauma.Items.Components
set => health = Math.Clamp(value, 0, MaxHealth);
}
public bool Decayed;
public bool FullyGrown;
public bool Decayed { get; set; }
public bool FullyGrown { get; set; }
private const int maxProductDelay = 10,
maxVineGrowthDelay = 10;
@@ -562,7 +562,7 @@ namespace Barotrauma.Items.Components
if (spawnProduct && ProducedItems.Any())
{
SpawnItem(Item, ProducedItems.RandomElementByWeight(it => it.Probability), spawnPos);
SpawnItem(Item, ProducedItems.GetRandomByWeight(it => it.Probability, Rand.RandSync.Unsynced), spawnPos);
return;
}
@@ -211,6 +211,9 @@ namespace Barotrauma.Items.Components
#endif
public bool DisableHeadRotation { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "If true, this item can't be used if the character is also holding a ranged weapon.")]
public bool DisableWhenRangedWeaponEquipped { get; set; }
[ConditionallyEditable(ConditionallyEditable.ConditionType.Attachable, MinValueFloat = 0.0f, MaxValueFloat = 0.999f, DecimalCount = 3), Serialize(0.55f, IsPropertySaveable.No, description: "Sprite depth that's used when the item is NOT attached to a wall.")]
public float SpriteDepthWhenDropped
{
@@ -343,11 +346,9 @@ namespace Barotrauma.Items.Components
DeattachFromWall();
}
if (setTransform)
{
if (Pusher != null) { Pusher.Enabled = false; }
if (item.body != null) { item.body.Enabled = true; }
}
if (Pusher != null) { Pusher.Enabled = false; }
if (item.body != null) { item.body.Enabled = true; }
IsActive = false;
attachTargetCell = null;
@@ -616,6 +617,7 @@ namespace Barotrauma.Items.Components
{
//set to submarine-relative position
item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false);
body.SetTransformIgnoreContacts(item.SimPosition, 0.0f);
}
item.Submarine = attachTarget.Submarine;
}
@@ -688,6 +690,7 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character character = null)
{
if (UsageDisabledByRangedWeapon(character)) { return false; }
if (!attachable || item.body == null) { return character == null || (character.IsKeyDown(InputType.Aim) && characterUsable); }
if (character != null)
{
@@ -792,19 +795,27 @@ namespace Barotrauma.Items.Components
Vector2 userPos = useWorldCoordinates ? user.WorldPosition : user.Position;
Vector2 attachPos = userPos + mouseDiff;
//offset the position by half the size of the grid to get the item to adhere to the grid in the same way as in the sub editor
//in the sub editor, we align the top-left corner of the item with the grid
//but here the origin of the item is placed at the attach position, so we need to offset it
Vector2 offset = new Vector2(
-(item.Rect.Width / 2) % Submarine.GridSize.X,
(item.Rect.Height / 2) % Submarine.GridSize.Y);
if (user.Submarine != null)
{
if (Submarine.PickBody(
ConvertUnits.ToSimUnits(user.Position),
ConvertUnits.ToSimUnits(user.Position + mouseDiff), collisionCategory: Physics.CollisionWall) != null)
{
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction;
attachPos = userPos + mouseDiff * Submarine.LastPickedFraction + offset;
//round down if we're placing on the right side and vice versa: ensures we don't round the position inside a wall
return
new Vector2(
mouseDiff.X > 0 ? (float)Math.Floor(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.X / Submarine.GridSize.X) * Submarine.GridSize.X,
mouseDiff.Y > 0 ? (float)Math.Floor(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.X : (float)Math.Ceiling(attachPos.Y / Submarine.GridSize.Y) * Submarine.GridSize.Y);
(mouseDiff.X > 0 ? MathF.Floor(attachPos.X / Submarine.GridSize.X) : MathF.Ceiling(attachPos.X / Submarine.GridSize.X)) * Submarine.GridSize.X,
(mouseDiff.Y > 0 ? MathF.Floor(attachPos.Y / Submarine.GridSize.Y) : MathF.Ceiling(attachPos.Y / Submarine.GridSize.Y)) * Submarine.GridSize.Y)
- offset;
}
}
else if (Level.Loaded != null)
@@ -827,10 +838,9 @@ namespace Barotrauma.Items.Components
}
}
return
new Vector2(
MathUtils.RoundTowardsClosest(attachPos.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(attachPos.Y, Submarine.GridSize.Y));
return new Vector2(
MathUtils.RoundTowardsClosest(attachPos.X + offset.X, Submarine.GridSize.X),
MathUtils.RoundTowardsClosest(attachPos.Y + offset.Y, Submarine.GridSize.Y)) - offset;
}
private Voronoi2.VoronoiCell GetAttachTargetCell(float maxDist)
@@ -903,7 +913,7 @@ namespace Barotrauma.Items.Components
{
scaledHandlePos[0] = handlePos[0] * item.Scale;
scaledHandlePos[1] = handlePos[1] * item.Scale;
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim;
bool aim = picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero && picker.CanAim && !UsageDisabledByRangedWeapon(picker);
if (aim)
{
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swingPos, aimPos + swingPos, aim, holdAngle, aimAngle);
@@ -963,6 +973,15 @@ namespace Barotrauma.Items.Components
}
}
protected bool UsageDisabledByRangedWeapon(Character character)
{
if (DisableWhenRangedWeaponEquipped && character != null)
{
if (character.HeldItems.Any(it => it.GetComponent<RangedWeapon>() != null)) { return true; }
}
return false;
}
public override void ReceiveSignal(Signal signal, Connection connection)
{
//do nothing
@@ -1005,14 +1024,12 @@ namespace Barotrauma.Items.Components
}
else
{
if (item.ParentInventory != null)
if (body != null)
{
if (body != null)
{
item.body = body;
body.Enabled = false;
}
}
body.SetTransformIgnoreContacts(item.SimPosition, item.Rotation);
item.body = body;
body.Enabled = item.ParentInventory == null;
}
DeattachFromWall();
}
}
@@ -37,7 +37,20 @@ namespace Barotrauma.Items.Components
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public string OwnerName { get; set; }
private string ownerNameLocalized;
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public string OwnerNameLocalized
{
get { return ownerNameLocalized; }
set
{
if (value.IsNullOrWhiteSpace()) { return; }
ownerNameLocalized = value;
OwnerName = TextManager.Get(value).Fallback(value).Value;
}
}
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public Identifier OwnerJobId { get; set; }
@@ -67,6 +80,9 @@ namespace Barotrauma.Items.Components
[Serialize("0,0", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public Vector2 OwnerSheetIndex { get; set; }
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool SpawnPointTagsGiven { get; set; }
public IdCard(Item item, ContentXElement element) : base(item, element) { }
public void Initialize(WayPoint spawnPoint, Character character)
@@ -135,7 +135,7 @@ namespace Barotrauma.Items.Components
private void CreateTriggerBody()
{
System.Diagnostics.Debug.Assert(trigger == null, "LevelResource trigger already created!");
var body = item.body ?? holdable.Body;
var body = item.body ?? holdable?.Body;
if (body != null && Attached)
{
trigger = new PhysicsBody(body.Width, body.Height, body.Radius,
@@ -189,7 +189,7 @@ namespace Barotrauma.Items.Components
impactQueue.Clear();
return;
}
if (picker == null && !picker.HeldItems.Contains(item))
if (picker == null || !picker.HeldItems.Contains(item))
{
impactQueue.Clear();
IsActive = false;
@@ -218,7 +218,8 @@ namespace Barotrauma.Items.Components
AnimController ac = picker.AnimController;
if (!hitting)
{
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim;
bool aim = item.RequireAimToUse && picker.AllowInput && picker.IsKeyDown(InputType.Aim) && reloadTimer <= 0 && picker.CanAim &&
!UsageDisabledByRangedWeapon(picker);
if (aim)
{
UpdateSwingPos(deltaTime, out Vector2 swingPos);
@@ -364,7 +365,7 @@ namespace Barotrauma.Items.Components
}
hitTargets.Add(targetStructure);
}
else if (f2.Body.UserData is Item targetItem)
else if ((f2.Body.UserData as Item ?? f2.UserData as Item) is Item targetItem)
{
if (AllowHitMultiple)
{
@@ -410,7 +411,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;
Item targetItem = target.UserData as Item ?? targetFixture.UserData as Item;
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
if (Attack != null)
{
@@ -73,6 +73,7 @@ namespace Barotrauma.Items.Components
{
//return if someone is already trying to pick the item
if (pickTimer > 0.0f) { return false; }
if (PickingTime >= float.MaxValue) { return false; }
if (picker == null || picker.Inventory == null) { return false; }
if (!picker.Inventory.AccessibleWhenAlive && !picker.Inventory.AccessibleByOwner) { return false; }
@@ -112,10 +113,16 @@ namespace Barotrauma.Items.Components
}
}
public virtual bool OnPicked(Character picker)
{
return OnPicked(picker, pickDroppedStack: true);
}
public virtual bool OnPicked(Character picker, bool pickDroppedStack)
{
//if the item has multiple Pickable components (e.g. Holdable and Wearable, check that we don't equip it in hands when the item is worn or vice versa)
if (item.GetComponents<Pickable>().Count() > 0)
if (item.GetComponents<Pickable>().Any())
{
bool alreadyEquipped = false;
for (int i = 0; i < picker.Inventory.Capacity; i++)
@@ -132,11 +139,13 @@ namespace Barotrauma.Items.Components
}
if (alreadyEquipped) { return false; }
}
var droppedStack = pickDroppedStack ? item.DroppedStack.ToList() : null;
if (picker.Inventory.TryPutItemWithAutoEquipCheck(item, picker, allowedSlots))
{
if (!picker.HeldItems.Contains(item) && item.body != null) { item.body.Enabled = false; }
this.picker = picker;
for (int i = item.linkedTo.Count - 1; i >= 0; i--)
{
item.linkedTo[i].RemoveLinked(item);
@@ -147,14 +156,22 @@ namespace Barotrauma.Items.Components
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
#if CLIENT
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItem);
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItem); }
PlaySound(ActionType.OnPicked, picker);
#endif
if (pickDroppedStack)
{
foreach (var droppedItem in droppedStack)
{
if (droppedItem == item) { continue; }
droppedItem.GetComponent<Pickable>().OnPicked(picker, pickDroppedStack: false);
}
}
return true;
}
#if CLIENT
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
if (!GameMain.Instance.LoadingScreenOpen && picker == Character.Controlled) { SoundPlayer.PlayUISound(GUISoundType.PickItemFail); }
#endif
return false;
@@ -183,12 +200,15 @@ namespace Barotrauma.Items.Components
}
#if CLIENT
Character.Controlled?.UpdateHUDProgressBar(
this,
item.WorldPosition,
pickTimer / requiredTime,
GUIStyle.Red, GUIStyle.Green,
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
if (requiredTime < float.MaxValue)
{
Character.Controlled?.UpdateHUDProgressBar(
this,
item.WorldPosition,
pickTimer / requiredTime,
GUIStyle.Red, GUIStyle.Green,
!string.IsNullOrWhiteSpace(PickingMsg) ? PickingMsg : this is Door ? "progressbar.opening" : "progressbar.deattaching");
}
#endif
picker.AnimController.UpdateUseItem(!picker.IsClimbing, item.WorldPosition + new Vector2(0.0f, 100.0f) * ((pickTimer / 10.0f) % 0.1f));
pickTimer += CoroutineManager.DeltaTime;
@@ -197,12 +217,14 @@ namespace Barotrauma.Items.Components
}
StopPicking(picker);
bool isNotRemote = true;
if (!item.Removed)
{
bool isNotRemote = true;
#if CLIENT
isNotRemote = !picker.IsRemotePlayer;
isNotRemote = !picker.IsRemotePlayer;
#endif
if (isNotRemote) OnPicked(picker);
if (isNotRemote) { OnPicked(picker); }
}
yield return CoroutineStatus.Success;
}
@@ -608,6 +608,7 @@ namespace Barotrauma.Items.Components
float closestDist = float.MaxValue;
foreach (Limb limb in targetCharacter.AnimController.Limbs)
{
if (limb.Removed || limb.IgnoreCollisions || limb.Hidden || limb.IsSevered) { continue; }
float dist = Vector2.DistanceSquared(item.SimPosition, limb.SimPosition);
if (dist < closestDist)
{
@@ -716,7 +717,7 @@ namespace Barotrauma.Items.Components
private readonly float repairTimeOut = 5;
public override bool CrewAIOperate(float deltaTime, Character character, AIObjectiveOperateItem objective)
{
if (!(objective.OperateTarget is Gap leak))
if (objective.OperateTarget is not Gap leak)
{
Reset();
return true;
@@ -806,36 +807,50 @@ namespace Barotrauma.Items.Components
// Press the trigger only when the tool is approximately facing the target.
Vector2 fromItemToLeak = leak.WorldPosition - item.WorldPosition;
var angle = VectorExtensions.Angle(VectorExtensions.Forward(item.body.TransformedRotation), fromItemToLeak);
bool repair = true;
if (angle < MathHelper.PiOver4)
{
if (Submarine.PickBody(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionWall, allowInsideFixture: true)?.UserData is Item i)
{
var door = i.GetComponent<Door>();
// Hit a door, abandon so that we don't weld it shut.
return door != null && !door.CanBeTraversed;
{
if (i.GetComponent<Door>() is Door door && !door.CanBeTraversed )
{
// Hit a door, don't repair so that we don't weld it shut.
if (door.Stuck > 90)
{
// Almost stuck -> just abandon.
return false;
}
if (door.Stuck > 50)
{
repair = false;
}
}
}
// Check that we don't hit any friendlies
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
if (repair)
{
if (hit.UserData is Character c)
// Check that we don't hit any friendlies
if (Submarine.PickBodies(item.SimPosition, leak.SimPosition, collisionCategory: Physics.CollisionCharacter).None(hit =>
{
if (c == character) { return false; }
return HumanAIController.IsFriendly(character, c);
if (hit.UserData is Character c)
{
if (c == character) { return false; }
return HumanAIController.IsFriendly(character, c);
}
return false;
}))
{
character.SetInput(InputType.Shoot, false, true);
Use(deltaTime, character);
}
return false;
}))
}
repairTimer += deltaTime;
if (repairTimer > repairTimeOut)
{
character.SetInput(InputType.Shoot, false, true);
Use(deltaTime, character);
repairTimer += deltaTime;
if (repairTimer > repairTimeOut)
{
#if DEBUG
DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow);
DebugConsole.NewMessage($"{character.Name}: timed out while welding a leak in {leak.FlowTargetHull.DisplayName}.", color: Color.Yellow);
#endif
Reset();
return true;
}
Reset();
return true;
}
}
}
@@ -912,7 +927,7 @@ namespace Barotrauma.Items.Components
// A general purpose system could be better, but it would most likely require changes in the way we define the status effects in xml.
foreach (ISerializableEntity target in currentTargets)
{
if (target is not Door door) { continue; }
if (target is not Door door) { continue; }
if (!door.CanBeWelded || !door.Item.IsInteractable(user)) { continue; }
foreach (var propertyEffect in effect.PropertyEffects)
{
@@ -22,7 +22,7 @@ namespace Barotrauma.Items.Components
/// </summary>
Vector2 DrawSize { get; }
void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1);
void Draw(SpriteBatch spriteBatch, bool editing, float itemDepth = -1, Color? overrideColor = null);
#endif
}
@@ -111,7 +111,6 @@ namespace Barotrauma.Items.Components
private bool drawable = true;
#warning TODO: misnomer - should be IsActiveConditionalLogicalOperator
[Serialize(PropertyConditional.LogicalOperatorType.And, IsPropertySaveable.No)]
public PropertyConditional.LogicalOperatorType IsActiveConditionalComparison
{
@@ -159,6 +158,12 @@ namespace Barotrauma.Items.Components
protected set;
}
[Serialize(false, IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)]
public bool LockGuiFramePosition { get; set; }
[Serialize("0,0", IsPropertySaveable.Yes), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork, onlyInEditors: false)]
public Point GuiFrameOffset { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "Can the item be selected by interacting with it.")]
public bool CanBeSelected
{
@@ -251,6 +256,9 @@ namespace Barotrauma.Items.Components
/// </summary>
public float Speed => item.Speed;
public readonly record struct ItemUseInfo(Item Item, Character User);
public readonly NamedEvent<ItemUseInfo> OnUsed = new();
public readonly bool InheritStatusEffects;
public ItemComponent(Item item, ContentXElement element)
@@ -339,6 +347,7 @@ namespace Barotrauma.Items.Components
switch (subElement.Name.ToString().ToLowerInvariant())
{
case "activeconditional":
case "isactiveconditional":
case "isactive":
IsActiveConditionals ??= new List<PropertyConditional>();
IsActiveConditionals.AddRange(PropertyConditional.FromXElement(subElement));
@@ -487,7 +496,7 @@ namespace Barotrauma.Items.Components
case "trigger_in":
if (signal.value != "0")
{
item.Use(1.0f, signal.sender);
item.Use(1.0f, user: signal.sender);
}
break;
case "toggle":
@@ -524,7 +533,7 @@ namespace Barotrauma.Items.Components
}
item.ParentInventory.RemoveItem(item);
}
Entity.Spawner.AddItemToRemoveQueue(item);
RemoveItem(item);
}
else
{
@@ -540,12 +549,23 @@ namespace Barotrauma.Items.Components
}
this.Item.ParentInventory.RemoveItem(this.Item);
}
Entity.Spawner.AddItemToRemoveQueue(this.Item);
RemoveItem(this.Item);
}
else
{
this.Item.Condition += transferAmount;
}
static void RemoveItem(Item item)
{
if (Screen.Selected is { IsEditor: true })
{
item?.Remove();
}
else
{
Entity.Spawner?.AddItemToRemoveQueue(item);
}
}
}
else
{
@@ -746,7 +766,7 @@ namespace Barotrauma.Items.Components
/// </summary>
private bool CheckIdCardAccess(RelatedItem relatedItem, IdCard idCard)
{
if (item.Submarine != null)
if (item.Submarine != null && item.Submarine != GameMain.NetworkMember?.RespawnManager?.RespawnShuttle)
{
//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"))
@@ -906,7 +926,16 @@ namespace Barotrauma.Items.Components
ParseMsg();
OverrideRequiredItems(componentElement);
}
#if CLIENT
if (GuiFrame != null)
{
GuiFrame.RectTransform.ScreenSpaceOffset = GuiFrameOffset;
if (guiFrameDragHandle != null)
{
guiFrameDragHandle.Enabled = !LockGuiFramePosition;
}
}
#endif
if (item.Submarine != null) { SerializableProperty.UpgradeGameVersion(this, originalElement, item.Submarine.Info.GameVersion); }
}
@@ -922,6 +951,11 @@ namespace Barotrauma.Items.Components
public virtual void OnScaleChanged() { }
/// <summary>
/// Called when the item has an ItemContainer and the contents inside of it changed.
/// </summary>
public virtual void OnInventoryChanged() { }
public static ItemComponent Load(ContentXElement element, Item item, bool errorMessages = true)
{
Type type;
@@ -1,12 +1,12 @@
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using Barotrauma.Abilities;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
@@ -14,11 +14,11 @@ namespace Barotrauma.Items.Components
{
readonly record struct ActiveContainedItem(Item Item, StatusEffect StatusEffect, bool ExcludeBroken, bool ExcludeFullCondition);
readonly record struct DrawableContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation);
readonly record struct ContainedItem(Item Item, bool Hide, Vector2? ItemPos, float Rotation);
class SlotRestrictions
{
public readonly int MaxStackSize;
public int MaxStackSize;
public List<RelatedItem> ContainableItems;
public readonly bool AutoInject;
@@ -55,7 +55,7 @@ namespace Barotrauma.Items.Components
private readonly List<ActiveContainedItem> activeContainedItems = new List<ActiveContainedItem>();
private readonly List<DrawableContainedItem> drawableContainedItems = new List<DrawableContainedItem>();
private readonly List<ContainedItem> containedItems = new List<ContainedItem>();
private List<ushort>[] itemIds;
@@ -128,6 +128,9 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(true, IsPropertySaveable.Yes, description: "When this item is equipped, and you 'quick use' (double click / equip button) another equippable item, should the game attempt to move that item inside this one?")]
public bool QuickUseMovesItemsInside { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "If set to true, interacting with this item will make the character interact with the contained item(s), automatically picking them up if they can be picked up.")]
public bool AutoInteractWithContained
{
@@ -141,17 +144,28 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.No)]
public bool AccessOnlyWhenBroken { get; set; }
[Serialize(true, IsPropertySaveable.No)]
public bool AllowAccessWhenDropped { get; set; }
[Serialize(5, IsPropertySaveable.No, description: "How many inventory slots the inventory has per row.")]
public int SlotsPerRow { get; set; }
private readonly HashSet<string> containableRestrictions = new HashSet<string>();
private readonly HashSet<Identifier> containableRestrictions = new HashSet<Identifier>();
[Editable, Serialize("", IsPropertySaveable.Yes, description: "Define items (by identifiers or tags) that bots should place inside this container. If empty, no restrictions are applied.")]
public string ContainableRestrictions
{
get { return string.Join(",", containableRestrictions); }
set
{
StringFormatter.ParseCommaSeparatedStringToCollection(value, containableRestrictions);
containableRestrictions.Clear();
if (!value.IsNullOrEmpty())
{
foreach (var str in value.Split(','))
{
if (str.IsNullOrWhiteSpace()) { continue; }
containableRestrictions.Add(str.ToIdentifier());
}
}
}
}
@@ -197,7 +211,6 @@ namespace Barotrauma.Items.Components
[Serialize(false, IsPropertySaveable.No)]
public bool RemoveContainedItemsOnDeconstruct { get; set; }
/// <summary>
/// Can be used by status effects to lock the inventory
/// </summary>
@@ -207,11 +220,28 @@ namespace Barotrauma.Items.Components
set { Inventory.Locked = value; }
}
/// <summary>
/// Can be used by status effects
/// </summary>
public int ContainedItemCount
{
get => Inventory.AllItems.Count();
}
/// <summary>
/// Can be used by status effects
/// </summary>
public int ContainedNonBrokenItemCount
{
get => Inventory.AllItems.Count(it => it.Condition > 0.0f);
}
private readonly ImmutableArray<SlotRestrictions> slotRestrictions;
readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
private Vector2 prevContainedItemPositions;
private float prevContainedItemRefreshRotation;
private Vector2 prevContainedItemRefreshPosition;
private float autoInjectCooldown = 1.0f;
const float AutoInjectInterval = 1.0f;
@@ -330,6 +360,16 @@ namespace Barotrauma.Items.Components
{
slotRestrictions[i].ContainableItems = ContainableItems;
}
#if CLIENT
if (element.GetChildElement("clearsubcontainerrestrictions") != null)
{
for (int i = capacity - MainContainerCapacity; i < capacity; i++)
{
slotRestrictions[i].MaxStackSize = MaxStackSize;
slotIcons[i] = null;
}
}
#endif
}
public int GetMaxStackSize(int slotIndex)
@@ -363,12 +403,31 @@ namespace Barotrauma.Items.Components
}
var relatedItem = FindContainableItem(containedItem);
drawableContainedItems.RemoveAll(d => d.Item == containedItem);
drawableContainedItems.Add(new DrawableContainedItem(containedItem,
Hide: relatedItem?.Hide ?? false,
ItemPos: relatedItem?.ItemPos,
Rotation: relatedItem?.Rotation ?? 0.0f));
drawableContainedItems.Sort((DrawableContainedItem it1, DrawableContainedItem it2) => Inventory.FindIndex(it1.Item).CompareTo(Inventory.FindIndex(it2.Item)));
var containedItemInfo = new ContainedItem(containedItem,
Hide: relatedItem?.Hide ?? false,
ItemPos: relatedItem?.ItemPos,
Rotation: relatedItem?.Rotation ?? 0.0f);
containedItems.RemoveAll(d => d.Item == containedItem);
if (hideItems)
{
//if the items aren't visible, the draw order doesn't matter and we can skip the sorting
containedItems.Add(containedItemInfo);
}
else
{
int containedIndex = 0;
while (containedIndex < containedItems.Count)
{
if (index <= Inventory.FindIndex(containedItems[containedIndex].Item))
{
break;
}
containedIndex++;
}
//sort drawables by their order in the inventory
containedItems.Insert(containedIndex, containedItemInfo);
}
if (item.GetComponent<Planter>() != null)
{
@@ -384,6 +443,14 @@ namespace Barotrauma.Items.Components
// Set the contained items active if there's an item inserted inside the container. Enables e.g. the rifle flashlight when it's attached to the rifle (put inside of it).
SetContainedActive(true);
}
if (containedItem.FlippedX)
{
containedItem.FlipX(relativeToSub: false);
}
if (containedItem.FlippedY)
{
containedItem.FlipY(relativeToSub: false);
}
item.SetContainedItemPositions();
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
OnContainedItemsChanged.Invoke(this);
@@ -397,7 +464,8 @@ namespace Barotrauma.Items.Components
public void OnItemRemoved(Item containedItem)
{
activeContainedItems.RemoveAll(i => i.Item == containedItem);
drawableContainedItems.RemoveAll(i => i.Item == containedItem);
containedItems.RemoveAll(i => i.Item == containedItem);
item.SetContainedItemPositions();
//deactivate if the inventory is empty
IsActive = activeContainedItems.Count > 0 || Inventory.AllItems.Any(it => it.body != null);
CharacterHUD.RecreateHudTextsIfFocused(item, containedItem);
@@ -406,12 +474,14 @@ namespace Barotrauma.Items.Components
public bool CanBeContained(Item item)
{
if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; }
return slotRestrictions.Any(s => s.MatchesItem(item));
}
public bool CanBeContained(Item item, int index)
{
if (index < 0 || index >= capacity) { return false; }
if (!AllowAccessWhenDropped && this.item.body is { Enabled: true }) { return false; }
return slotRestrictions[index].MatchesItem(item);
}
@@ -463,11 +533,7 @@ namespace Barotrauma.Items.Components
if (item.ParentInventory is CharacterInventory ownerInventory)
{
if (Vector2.DistanceSquared(prevContainedItemPositions, item.Position) > 10.0f)
{
SetContainedItemPositions();
prevContainedItemPositions = item.Position;
}
SetContainedItemPositionsIfNeeded();
if (AutoInject || slotRestrictions.Any(s => s.AutoInject))
{
@@ -508,11 +574,12 @@ namespace Barotrauma.Items.Components
}
}
else if (item.body != null &&
item.body.Enabled &&
item.body.FarseerBody.Awake)
else if (item.body != null && item.body.Enabled)
{
SetContainedItemPositions();
if (item.body.FarseerBody.Awake)
{
SetContainedItemPositionsIfNeeded();
}
}
else if (activeContainedItems.Count == 0)
{
@@ -553,6 +620,20 @@ namespace Barotrauma.Items.Components
}
}
/// <summary>
/// Set the positions of the contained items if this item has moved/rotated enough
/// </summary>
private void SetContainedItemPositionsIfNeeded()
{
if (Vector2.DistanceSquared(prevContainedItemRefreshPosition, item.Position) > 10.0f ||
Math.Abs(prevContainedItemRefreshRotation - item.body?.Rotation ?? item.RotationRad) > 0.01f)
{
SetContainedItemPositions();
prevContainedItemRefreshPosition = item.Position;
prevContainedItemRefreshRotation = item.body?.Rotation ?? item.RotationRad;
}
}
public override void UpdateBroken(float deltaTime, Camera cam)
{
//update when the item is broken too to get OnContaining effects to execute and contained item positions to update
@@ -662,10 +743,19 @@ namespace Barotrauma.Items.Components
{
SetContainedActive(true);
}
else
{
SetContainedActive(false);
}
}
private void SetContainedActive(bool active)
{
if ((ContainableItems == null || !ContainableItems.Any(c => c.SetActive)) &&
(AllSubContainableItems == null || !AllSubContainableItems.Any(c => c.SetActive)))
{
return;
}
foreach (Item containedItem in Inventory.AllItems)
{
RelatedItem containableItem = FindContainableItem(containedItem);
@@ -722,7 +812,7 @@ namespace Barotrauma.Items.Components
case "trigger_in":
if (signal.value != "0")
{
item.Use(1.0f, signal.sender);
item.Use(1.0f, user: signal.sender);
}
break;
}
@@ -786,7 +876,7 @@ namespace Barotrauma.Items.Components
int i = 0;
Vector2 currentItemPos = transformedItemPos;
foreach (DrawableContainedItem contained in drawableContainedItems)
foreach (ContainedItem contained in containedItems)
{
Vector2 itemPos = currentItemPos;
if (contained.ItemPos.HasValue)
@@ -62,18 +62,58 @@ namespace Barotrauma.Items.Components
public IEnumerable<LimbPos> LimbPositions { get { return limbPositions; } }
[Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a 0/1 signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out 1 when interacted with.", alwaysUseInstanceValues: true)]
[Editable, Serialize(false, IsPropertySaveable.No, description: "When enabled, the item will continuously send out a signal and interacting with it will flip the signal (making the item behave like a switch). When disabled, the item will simply send out a signal when interacted with.", alwaysUseInstanceValues: true)]
public bool IsToggle
{
get;
set;
}
[Editable, Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)]
private string output;
[ConditionallyEditable(ConditionallyEditable.ConditionType.HasConnectionPanel, onlyInEditors: false),
Serialize("1", IsPropertySaveable.Yes, description: "The signal sent when the controller is being activated or is toggled on. If empty, no signal is sent.", alwaysUseInstanceValues: true)]
public string Output
{
get { return output; }
set
{
if (value == null || value == output) { return; }
output = value;
//reactivate if signal isn't empty (we may not have been previously sending a signal, but might now)
if (!value.IsNullOrEmpty()) { IsActive = true; }
}
}
private string falseOutput;
[ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: false),
Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the controller is toggled off. If empty, no signal is sent. Only valid if IsToggle is true.", alwaysUseInstanceValues: true)]
public string FalseOutput
{
get { return falseOutput; }
set
{
if (value == null || value == falseOutput) { return; }
falseOutput = value;
//reactivate if signal isn't empty (we may not have been previously sending a signal, but might now)
if (!value.IsNullOrEmpty()) { IsActive = true; }
}
}
private bool state;
[ConditionallyEditable(ConditionallyEditable.ConditionType.IsToggleableController, onlyInEditors: true),
Serialize(false, IsPropertySaveable.No, description: "Whether the item is toggled on/off. Only valid if IsToggle is set to true.", alwaysUseInstanceValues: true)]
public bool State
{
get;
set;
get { return state; }
set
{
if (state != value)
{
state = value;
string newOutput = state ? output : falseOutput;
IsActive = !string.IsNullOrEmpty(newOutput);
}
}
}
[Serialize(true, IsPropertySaveable.No, description: "Should the HUD (inventory, health bar, etc) be hidden when this item is selected.")]
@@ -164,10 +204,11 @@ namespace Barotrauma.Items.Components
this.cam = cam;
UserInCorrectPosition = false;
if (IsToggle)
string signal = IsToggle && State ? output : falseOutput;
if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal))
{
item.SendSignal(State ? "1" : "0", "signal_out");
item.SendSignal(State ? "1" : "0", "trigger_out");
item.SendSignal(signal, "signal_out");
item.SendSignal(signal, "trigger_out");
}
if (user == null
@@ -183,7 +224,7 @@ namespace Barotrauma.Items.Components
CancelUsing(user);
user = null;
}
if (!IsToggle || item.Connections == null) { IsActive = false; }
if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; }
return;
}
@@ -313,9 +354,9 @@ namespace Barotrauma.Items.Components
#endif
}
}
else
else if (!string.IsNullOrEmpty(output))
{
item.SendSignal(new Signal("1", sender: user), "trigger_out");
item.SendSignal(new Signal(output, sender: user), "trigger_out");
}
lastUsed = Timing.TotalTime;
@@ -419,9 +460,9 @@ namespace Barotrauma.Items.Components
#endif
}
}
else
else if (!string.IsNullOrEmpty(output))
{
item.SendSignal(new Signal("1", sender: picker), "signal_out");
item.SendSignal(new Signal(output, sender: picker), "signal_out");
}
#if CLIENT
PlaySound(ActionType.OnUse, picker);
@@ -480,6 +521,7 @@ namespace Barotrauma.Items.Components
IsActive = false;
CancelUsing(user);
user = null;
return false;
}
else if (user.IsBot && !activator.IsBot)
{
@@ -500,8 +542,11 @@ namespace Barotrauma.Items.Components
}
#if SERVER
item.CreateServerEvent(this);
#endif
item.SendSignal(new Signal("1", sender: user), "signal_out");
#endif
if (!string.IsNullOrEmpty(output))
{
item.SendSignal(new Signal(output, sender: user), "signal_out");
}
return true;
}
@@ -92,7 +92,7 @@ namespace Barotrauma.Items.Components
repairable.LastActiveTime = (float)Timing.TotalTime + 10.0f;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
progressTimer += deltaTime * Math.Min(powerConsumption <= 0.0f ? 1 : Voltage, MaxOverVoltageFactor);
@@ -24,6 +24,14 @@ namespace Barotrauma.Items.Components
/// </summary>
private float targetForce;
/// <summary>
/// Power demand of a marine engine is proportional with the cube of the square root of the thrusting force.
/// In practice meaning lower thrust is more effective at conserving power than it would be if the relationship between thrust and power consumption was linear.
/// Reverse exponent defined for use with overvoltage calculation: Supplying 2x power will result in 59% more force, 26% more speed, therefore 2x power.
/// </summary>
private const float ForceToPowerExponent = 3f / 2f;
private const float PowerToForceExponent = 1.0f / ForceToPowerExponent;
private float maxForce;
private readonly Attack propellerDamage;
@@ -130,7 +138,7 @@ namespace Barotrauma.Items.Components
if (Math.Abs(Force) > 1.0f)
{
float voltageFactor = MinVoltage <= 0.0f ? 1.0f : Math.Min(Voltage, MaxOverVoltageFactor);
float currForce = force * voltageFactor;
float currForce = force * MathF.Pow(voltageFactor, PowerToForceExponent);
float condition = item.MaxCondition <= 0.0f ? 0.0f : item.Condition / item.MaxCondition;
// Broken engine makes more noise.
float noise = Math.Abs(currForce) * MathHelper.Lerp(1.5f, 1f, condition);
@@ -180,7 +188,7 @@ namespace Barotrauma.Items.Components
return 0;
}
currPowerConsumption = Math.Abs(targetForce) / 100.0f * powerConsumption;
currPowerConsumption = MathF.Pow(Math.Abs(targetForce) / 100.0f, ForceToPowerExponent) * powerConsumption;
//engines consume more power when in a bad condition
item.GetComponent<Repairable>()?.AdjustPowerConsumption(ref currPowerConsumption);
return currPowerConsumption;
@@ -92,6 +92,8 @@ namespace Barotrauma.Items.Components
private readonly Dictionary<uint, int> fabricationLimits = new Dictionary<uint, int>();
public Action<Item, Character> OnItemFabricated;
public Fabricator(Item item, ContentXElement element)
: base(item, element)
{
@@ -128,6 +130,11 @@ namespace Barotrauma.Items.Components
}
if (recipeInvalid) { continue; }
if (fabricationRecipes.TryGetValue(recipe.RecipeHash, out var duplicateRecipe))
{
DebugConsole.ThrowError($"Error in the fabrication recipe for \"{itemPrefab.Name}\". Duplicate recipe in \"{duplicateRecipe.TargetItem.Identifier}\".");
continue;
}
fabricationRecipes.Add(recipe.RecipeHash, recipe);
if (recipe.FabricationLimitMax >= 0)
{
@@ -327,7 +334,7 @@ namespace Barotrauma.Items.Components
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
float fabricationSpeedIncrease = 1f + tinkeringStrength * TinkeringSpeedIncrease;
@@ -387,37 +394,35 @@ namespace Barotrauma.Items.Components
if (GameMain.NetworkMember is null || GameMain.NetworkMember.IsServer)
{
List<Item> foundAvailableItems = new List<Item>();
foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems)
List<Item> chosenIngredients = new List<Item>();
var suitableIngredients = GetSortedSuitableIngredients();
foreach (var requiredItem in fabricatedItem.RequiredItems)
{
for (int usedPrefabsAmount = 0; usedPrefabsAmount < requiredItem.Amount; usedPrefabsAmount++)
for (int i = 0; i < requiredItem.Amount; i++)
{
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
foreach (var suitableIngredient in suitableIngredients)
{
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
if (!requiredItem.MatchesItem(suitableIngredient)) { continue; }
if (chosenIngredients.Contains(suitableIngredient)) { continue; }
var availableItems = availableIngredients[requiredPrefab.Identifier];
var availableItem = availableItems.FirstOrDefault(potentialPrefab => requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage));
if (availableItem == null) { continue; }
ingredientsStolen |= availableItem.StolenDuringRound;
if (!availableItem.AllowStealing)
ingredientsStolen |= suitableIngredient.StolenDuringRound;
if (!suitableIngredient.AllowStealing)
{
ingredientsAllowStealing = false;
}
//Leave it behind with reduced condition if it has enough to stay above 0
if (requiredItem.UseCondition && availableItem.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f)
if (requiredItem.UseCondition && suitableIngredient.ConditionPercentage - requiredItem.MinCondition * 100 > 0.0f)
{
availableItem.Condition -= availableItem.Prefab.Health * requiredItem.MinCondition;
suitableIngredient.Condition -= suitableIngredient.Prefab.Health * requiredItem.MinCondition;
continue;
}
if (availableItem.OwnInventory != null)
if (suitableIngredient.OwnInventory != null)
{
foreach (Item containedItem in availableItem.OwnInventory.AllItemsMod)
foreach (Item containedItem in suitableIngredient.OwnInventory.AllItemsMod)
{
if (availableItem.GetComponent<ItemContainer>()?.RemoveContainedItemsOnDeconstruct ?? false)
if (suitableIngredient.GetComponent<ItemContainer>()?.RemoveContainedItemsOnDeconstruct ?? false)
{
Entity.Spawner.AddItemToRemoveQueue(containedItem);
}
@@ -427,15 +432,13 @@ namespace Barotrauma.Items.Components
}
}
}
foundAvailableItems.Add(availableItem);
availableItems.Remove(availableItem);
chosenIngredients.Add(suitableIngredient);
break;
}
}
}
var fabricationIngredients = new AbilityFabricationItemIngredients(foundAvailableItems);
var fabricationIngredients = new AbilityFabricationItemIngredients(chosenIngredients);
user?.CheckTalents(AbilityEffectType.OnItemFabricatedIngredients, fabricationIngredients);
foreach (Item availableItem in fabricationIngredients.Items)
@@ -459,7 +462,7 @@ namespace Barotrauma.Items.Components
{
character.CheckTalents(AbilityEffectType.OnAllyItemFabricatedAmount, fabricationitemAmount);
}
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount);
user.CheckTalents(AbilityEffectType.OnItemFabricatedAmount, fabricationitemAmount);
quality = GetFabricatedItemQuality(fabricatedItem, user);
}
@@ -510,7 +513,7 @@ namespace Barotrauma.Items.Components
}
}
static void onItemSpawned(Item spawnedItem, Character user)
void onItemSpawned(Item spawnedItem, Character user)
{
if (user != null && user.TeamID != CharacterTeamType.None)
{
@@ -519,6 +522,7 @@ namespace Barotrauma.Items.Components
wifiComponent.TeamID = user.TeamID;
}
}
OnItemFabricated?.Invoke(spawnedItem, user);
}
if (user?.Info != null && !user.Removed)
{
@@ -623,6 +627,11 @@ namespace Barotrauma.Items.Components
}
}
if (fabricableItem.HideForNonTraitors)
{
if (character is not { IsTraitor: true }) { return false; }
}
if (fabricableItem.RequiredMoney > 0)
{
switch (GameMain.GameSession?.GameMode)
@@ -757,6 +766,19 @@ namespace Barotrauma.Items.Components
itemList.AddRange(user.Inventory.AllItems);
linkedInventories.Add(user.Inventory);
}
foreach (Character c in Character.CharacterList)
{
//take materials from characters who've selected a linked container too
//(e.g. cabinet that's set to display alongside the fabricator UI)
if (c.SelectedItem != null &&
c.Inventory != null &&
linkedInventories.Contains(c.SelectedItem.OwnInventory) &&
!linkedInventories.Contains(c.Inventory))
{
itemList.AddRange(c.Inventory.AllItems);
linkedInventories.Add(c.Inventory);
}
}
availableIngredients.Clear();
foreach (Item item in itemList)
{
@@ -765,34 +787,51 @@ namespace Barotrauma.Items.Components
{
availableIngredients[itemIdentifier] = new List<Item>(itemList.Count);
}
//order by condition (prefer using worst-condition items)
int index = 0;
while (index < availableIngredients[itemIdentifier].Count &&
compare(item, availableIngredients[itemIdentifier][index], inputContainer.Inventory) < 0)
{
index++;
}
static int compare(Item item1, Item item2, Inventory inputInventory)
{
bool item1InInputInventory = item1.ParentInventory == inputInventory;
bool item2InInputInventory = item2.ParentInventory == inputInventory;
//prefer items in the input inventory
if (item1InInputInventory != item2InInputInventory)
{
return item1InInputInventory ? 1 : -1;
}
else
{
float condition1 = MathUtils.IsValid(item1.Condition) ? item1.Condition : 0;
float condition2 = MathUtils.IsValid(item2.Condition) ? item2.Condition : 0;
//prefer items in worse condition
return Math.Sign(condition2 - condition1);
}
}
availableIngredients[itemIdentifier].Insert(index, item);
availableIngredients[itemIdentifier].Add(item);
}
foreach (var itemId in availableIngredients.Keys)
{
availableIngredients[itemId] = SortIngredients(availableIngredients[itemId]).ToList();
}
}
private IEnumerable<Item> SortIngredients(IEnumerable<Item> items)
{
return items
.OrderByDescending(getIngredientContainerPriority)
.ThenBy(it => it.Prefab.DefaultPrice?.Price ?? 0)
.ThenBy(it => MathUtils.IsValid(it.Condition) ? it.Condition : 0)
.ThenByDescending(it => it.ParentInventory?.FindIndex(it) ?? 0);
int getIngredientContainerPriority(Item item)
{
if (item.ParentInventory == InputContainer.Inventory)
{
return 3;
}
else if (item.ParentInventory is CharacterInventory)
{
return 2;
}
return 1;
}
}
private IEnumerable<Item> GetSortedSuitableIngredients()
{
List<Item> suitableIngredients = new List<Item>();
foreach (FabricationRecipe.RequiredItem requiredItem in fabricatedItem.RequiredItems)
{
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
{
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
var availableItems = availableIngredients[requiredPrefab.Identifier];
suitableIngredients.AddRange(
availableItems.Where(potentialItem => requiredItem.IsConditionSuitable(potentialItem.ConditionPercentage)));
}
}
return SortIngredients(suitableIngredients);
}
/// <summary>
@@ -801,43 +840,34 @@ namespace Barotrauma.Items.Components
/// </summary>
private void MoveIngredientsToInputContainer(FabricationRecipe targetItem)
{
//required ingredients that are already present in the input container
List<Item> usedItems = new List<Item>();
List<Item> chosenIngredients = new List<Item>();
var suitableIngredients = GetSortedSuitableIngredients();
targetItem.RequiredItems.ForEach(requiredItem => {
foreach (var requiredItem in targetItem.RequiredItems)
{
for (int i = 0; i < requiredItem.Amount; i++)
{
foreach (ItemPrefab requiredPrefab in requiredItem.ItemPrefabs)
foreach (var suitableIngredient in suitableIngredients)
{
if (!availableIngredients.ContainsKey(requiredPrefab.Identifier)) { continue; }
if (!requiredItem.MatchesItem(suitableIngredient)) { continue; }
if (chosenIngredients.Contains(suitableIngredient)) { continue; }
var availablePrefabs = availableIngredients[requiredPrefab.Identifier];
var availablePrefab = availablePrefabs.FirstOrDefault(potentialPrefab =>
//in another inventory, we need to move the item
if (suitableIngredient.ParentInventory != inputContainer.Inventory)
{
return !usedItems.Contains(potentialPrefab) && requiredItem.IsConditionSuitable(potentialPrefab.ConditionPercentage);
});
if (availablePrefab == null) { continue; }
availablePrefabs.Remove(availablePrefab);
if (availablePrefab.ParentInventory == inputContainer.Inventory)
{
//already in input container, all good
usedItems.Add(availablePrefab);
}
else //in another inventory, we need to move the item
{
if (!inputContainer.Inventory.CanBePut(availablePrefab))
if (!inputContainer.Inventory.CanBePut(suitableIngredient))
{
var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !usedItems.Contains(it));
var unneededItem = inputContainer.Inventory.AllItems.FirstOrDefault(it => !chosenIngredients.Contains(it));
unneededItem?.Drop(null);
}
inputContainer.Inventory.TryPutItem(availablePrefab, user: null);
inputContainer.Inventory.TryPutItem(suitableIngredient, user: null);
}
chosenIngredients.Add(suitableIngredient);
break;
}
}
});
}
RefreshAvailableIngredients();
}
@@ -884,6 +914,13 @@ namespace Barotrauma.Items.Components
}
savedFabricatedItem = null;
}
protected override void RemoveComponentSpecific()
{
base.RemoveComponentSpecific();
OnItemFabricated = null;
}
class AbilityFabricatorSkillGain : AbilityObject, IAbilityValue, IAbilitySkillIdentifier
{
public AbilityFabricatorSkillGain(Identifier skillIdentifier, float skillAmount)
@@ -83,7 +83,7 @@ namespace Barotrauma.Items.Components
hasPower = Voltage > MinVoltage;
if (hasPower)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
}
}
@@ -132,7 +132,7 @@ namespace Barotrauma.Items.Components
UpdateProjSpecific(deltaTime);
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (item.CurrentHull == null) { return; }
@@ -203,6 +203,8 @@ namespace Barotrauma.Items.Components
set;
}
public bool MeltedDownThisRound { get; private set; }
public Reactor(Item item, ContentXElement element)
: base(item, element)
{
@@ -282,7 +284,7 @@ namespace Barotrauma.Items.Components
}
prevAvailableFuel = AvailableFuel;
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
//use a smoothed "correct output" instead of the actual correct output based on the load
//so the player doesn't have to keep adjusting the rate impossibly fast when the load fluctuates heavily
@@ -338,7 +340,7 @@ namespace Barotrauma.Items.Components
{
foreach (Item item in containedItems)
{
if (!item.HasTag("reactorfuel")) { continue; }
if (!item.HasTag(Tags.Fuel)) { continue; }
if (fissionRate > 0.0f)
{
bool isConnectedToFriendlyOutpost = Level.IsLoadedOutpost &&
@@ -648,6 +650,7 @@ namespace Barotrauma.Items.Components
item.Condition = 0.0f;
fireTimer = 0.0f;
meltDownTimer = 0.0f;
MeltedDownThisRound = true;
var containedItems = item.OwnInventory?.AllItems;
if (containedItems != null)
@@ -704,13 +707,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, "reactorfuel".ToIdentifier(), 30.0f);
character.Speak(TextManager.Get("DialogReactorFuel").Value, null, 0.0f, Tags.Fuel, 30.0f);
void ReportFuelRodCount()
{
if (!character.IsOnPlayerTeam) { return; }
if (character.Submarine != Submarine.MainSub) { return; }
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag("reactorfuel") && i.Condition > 1);
int remainingFuelRods = Submarine.MainSub.GetItems(false).Count(i => i.HasTag(Tags.Fuel) && i.Condition > 1);
if (remainingFuelRods == 0)
{
character.Speak(TextManager.Get("DialogOutOfFuelRods").Value, null, 0.0f, "outoffuelrods".ToIdentifier(), 30.0f);
@@ -300,7 +300,7 @@ namespace Barotrauma.Items.Components
user = null;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
float userSkill = 0.0f;
if (user != null && controlledSub != null &&
@@ -508,7 +508,7 @@ namespace Barotrauma.Items.Components
var closeCells = Level.Loaded.GetCells(controlledSub.WorldPosition, 4);
foreach (VoronoiCell cell in closeCells)
{
if (cell.DoesDamage)
if (cell.DoesDamage || cell.Body is { BodyType: BodyType.Dynamic })
{
foreach (GraphEdge edge in cell.Edges)
{
@@ -525,7 +525,7 @@ namespace Barotrauma.Items.Components
newAvoidStrength += avoid;
debugDrawObstacles.Add(new ObstacleDebugInfo(edge, edge.Center, 1.0f, avoid, cell.Translation));
if (dot > 0.0f)
if (dot > 0.0f && cell.DoesDamage)
{
showIceSpireWarning = true;
}
@@ -138,6 +138,8 @@ namespace Barotrauma.Items.Components
set { flipIndicator = value; }
}
public bool OutputDisabled { get; private set; }
public float RechargeRatio => RechargeSpeed / MaxRechargeSpeed;
public const float aiRechargeTargetRatio = 0.5f;
@@ -162,19 +164,19 @@ namespace Barotrauma.Items.Components
public override void Update(float deltaTime, Camera cam)
{
adjustedCapacity = GetCapacity();
if (item.Connections == null)
{
IsActive = false;
return;
}
adjustedCapacity = GetCapacity();
isRunning = true;
float chargeRatio = charge / adjustedCapacity;
if (chargeRatio > 0.0f)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
}
float loadReading = 0;
@@ -242,6 +244,7 @@ namespace Barotrauma.Items.Components
/// <returns>Minimum and maximum power output for the connection</returns>
public override PowerRange MinMaxPowerOut(Connection connection, float load = 0)
{
if (OutputDisabled) { return PowerRange.Zero; }
if (connection == powerOut)
{
float maxOutput;
@@ -275,6 +278,7 @@ namespace Barotrauma.Items.Components
/// <returns></returns>
public override float GetConnectionPowerOut(Connection connection, float power, PowerRange minMaxPower, float load)
{
if (OutputDisabled) { return 0; }
//Only power out connection can provide power and Max poweroutput can't be negative
if (connection == powerOut && minMaxPower.Max > 0)
{
@@ -367,22 +371,27 @@ namespace Barotrauma.Items.Components
public override void ReceiveSignal(Signal signal, Connection connection)
{
if (connection.IsPower) { return; }
if (connection.Name == "set_rate")
switch (connection.Name)
{
if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed))
{
if (!MathUtils.IsValid(tempSpeed)) { return; }
float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f);
RechargeSpeed = rechargeRate * MaxRechargeSpeed;
#if CLIENT
if (rechargeSpeedSlider != null)
case "disable_output":
OutputDisabled = signal.value != "0";
break;
case "set_rate":
if (float.TryParse(signal.value, NumberStyles.Any, CultureInfo.InvariantCulture, out float tempSpeed))
{
rechargeSpeedSlider.BarScroll = rechargeRate;
}
if (!MathUtils.IsValid(tempSpeed)) { return; }
float rechargeRate = MathHelper.Clamp(tempSpeed / 100.0f, 0.0f, 1.0f);
RechargeSpeed = rechargeRate * MaxRechargeSpeed;
#if CLIENT
if (rechargeSpeedSlider != null)
{
rechargeSpeedSlider.BarScroll = rechargeRate;
}
#endif
}
}
break;
}
}
@@ -197,7 +197,7 @@ namespace Barotrauma.Items.Components
isBroken = false;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
float powerReadingOut = 0;
float loadReadingOut = ExtraLoad;
@@ -1,6 +1,7 @@
using System;
using Microsoft.Xna.Framework;
using System.Collections.Generic;
using System.Linq;
#if CLIENT
using Barotrauma.Sounds;
#endif
@@ -193,13 +194,13 @@ namespace Barotrauma.Items.Components
{
//if the item consumes no power, ignore the voltage requirement and
//apply OnActive statuseffects as long as this component is active
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
return;
}
if (Voltage > minVoltage)
{
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
}
#if CLIENT
if (Voltage > minVoltage)
@@ -666,7 +667,7 @@ namespace Barotrauma.Items.Components
return
conn1.IsPower && conn2.IsPower &&
conn1.Item.Condition > 0.0f && conn2.Item.Condition > 0.0f &&
(conn1.Item.HasTag("junctionbox") || conn2.Item.HasTag("junctionbox") || conn1.Item.HasTag("dock") || conn2.Item.HasTag("dock") || conn1.IsOutput != conn2.IsOutput);
(conn1.Item.HasTag(Tags.JunctionBox) || conn2.Item.HasTag(Tags.JunctionBox) || conn1.Item.HasTag(Tags.DockingPort) || conn2.Item.HasTag(Tags.DockingPort) || conn1.IsOutput != conn2.IsOutput);
}
/// <summary>
@@ -682,6 +683,7 @@ namespace Barotrauma.Items.Components
if (!recipient.IsPower || !recipient.IsOutput) { continue; }
var battery = recipient.Item?.GetComponent<PowerContainer>();
if (battery == null || battery.Item.Condition <= 0.0f) { continue; }
if (battery.OutputDisabled) { continue; }
float maxOutputPerFrame = battery.MaxOutPut / 60.0f;
float framesPerMinute = 3600.0f;
availablePower += Math.Min(battery.Charge * framesPerMinute, maxOutputPerFrame);
@@ -689,23 +691,19 @@ namespace Barotrauma.Items.Components
return availablePower;
}
/// <summary>
/// Returns a list of batteries directly connected to the item
/// </summary>
protected List<PowerContainer> GetDirectlyConnectedBatteries()
protected IEnumerable<PowerContainer> GetDirectlyConnectedBatteries()
{
List<PowerContainer> batteries = new List<PowerContainer>();
if (item.Connections == null || powerIn == null) { return batteries; }
foreach (Connection recipient in powerIn.Recipients)
if (item.Connections != null && powerIn != null)
{
if (!recipient.IsPower || !recipient.IsOutput) { continue; }
var battery = recipient.Item?.GetComponent<PowerContainer>();
if (battery != null)
foreach (Connection recipient in powerIn.Recipients)
{
batteries.Add(battery);
if (!recipient.IsPower || !recipient.IsOutput) { continue; }
if (recipient.Item?.GetComponent<PowerContainer>() is PowerContainer battery)
{
yield return battery;
}
}
}
return batteries;
}
protected override void RemoveComponentSpecific()
@@ -56,6 +56,16 @@ namespace Barotrauma.Items.Components
}
}
enum StickTargetType
{
Structure,
Limb,
Item,
Submarine,
LevelWall,
Unknown
}
public const float WaterDragCoefficient = 0.1f;
private readonly Queue<Impact> impactQueue = new Queue<Impact>();
@@ -353,7 +363,7 @@ namespace Barotrauma.Items.Components
if (Item.Removed) { return; }
launchPos = simPosition;
//set the rotation of the projectile again because dropping the projectile resets the rotation
Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians));
Item.SetTransform(simPosition, rotation + (Item.body.Dir * LaunchRotationRadians), findNewHull: false);
if (DeactivationTime > 0)
{
deactivationTimer = DeactivationTime;
@@ -442,27 +452,49 @@ namespace Barotrauma.Items.Components
{
hits.Clear();
Vector2 prevVelocity = item.body.LinearVelocity;
if (item.AiTarget != null)
{
item.AiTarget.SightRange = item.AiTarget.MaxSightRange;
item.AiTarget.SoundRange = item.AiTarget.MaxSoundRange;
}
//do not create a network event about dropping the projectile at this point,
//otherwise clients would fail to launch the correct projectile when firing the weapon
var prevInventory = item.ParentInventory;
if (prevInventory != null && GameMain.NetworkMember is { IsServer: true })
{
//update the state of the inventory after a delay,
//in case a client failed to launch the projectile for some reason
CoroutineManager.Invoke(() =>
{
if (item.Removed) { return; }
prevInventory.CreateNetworkEvent();
}, delay: CorrectionDelay / 2);
}
item.Drop(null, createNetworkEvent: false);
Item.WaterDragCoefficient = WaterDragCoefficient;
launchPos = item.SimPosition;
item.body.Enabled = true;
item.body.Enabled = true;
if (item.body.BodyType == BodyType.Kinematic)
{
item.body.LinearVelocity = impulse;
}
else
else if (impulse.LengthSquared() > 0.001f)
{
impulse *= item.body.Mass;
item.body.ApplyLinearImpulse(impulse, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.95f);
}
else
{
//if no impulse is defined, maintain the projectile's original velocity (if any)
//can be used to make throwable items behave as projectiles
item.body.LinearVelocity = prevVelocity;
}
item.body.FarseerBody.OnCollision += OnProjectileCollision;
item.body.FarseerBody.IsBullet = true;
@@ -483,7 +515,7 @@ namespace Barotrauma.Items.Components
float rotation = item.body.Rotation;
Vector2 simPositon = item.SimPosition;
Vector2 rayStartWorld = item.WorldPosition;
item.Drop(null);
item.Drop(null, createNetworkEvent: false);
Item.WaterDragCoefficient = WaterDragCoefficient;
item.body.Enabled = true;
@@ -666,6 +698,7 @@ namespace Barotrauma.Items.Components
if (fixture.Body.UserData is VoronoiCell) { return -1; }
if (fixture.Body.UserData is Entity entity && entity.Submarine != submarine) { return -1; }
if (fixture.Body.UserData is Limb limb && limb.character?.Submarine != submarine) { return -1; }
if (fixture.Body == Level.Loaded?.TopBarrier || fixture.Body == Level.Loaded?.BottomBarrier) { return -1; }
}
// Ignore holdables that can't push -> shouldn't block
@@ -891,8 +924,17 @@ namespace Barotrauma.Items.Components
return false;
}
Vector2 dir = item.body.LinearVelocity.LengthSquared() < 0.001f ?
contact.Manifold.LocalNormal : Vector2.Normalize(item.body.LinearVelocity);
Vector2 normalizedVel;
Vector2 dir;
if (item.body.LinearVelocity.LengthSquared() < 0.001f)
{
normalizedVel = Vector2.Zero;
dir = contact.Manifold.LocalNormal;
}
else
{
normalizedVel = dir = Vector2.Normalize(item.body.LinearVelocity);
}
//do a raycast in the sub's coordinate space to see if it hit a structure
var wallBody = Submarine.PickBody(
@@ -901,7 +943,7 @@ namespace Barotrauma.Items.Components
collisionCategory: Physics.CollisionWall);
if (wallBody?.FixtureList?.First() != null && (wallBody.UserData is Structure || wallBody.UserData is Item) &&
//ignore the hit if it's behind the position the item was launched from, and the projectile is travelling in the opposite direction
Vector2.Dot(item.body.SimPosition - launchPos, dir) > 0)
Vector2.Dot((item.body.SimPosition + normalizedVel) - launchPos, dir) > 0)
{
target = wallBody.FixtureList.First();
if (hits.Contains(target.Body))
@@ -1136,7 +1178,13 @@ namespace Barotrauma.Items.Components
removePending = true;
item.HiddenInGame = true;
item.body.FarseerBody.Enabled = false;
Entity.Spawner?.AddItemToRemoveQueue(item);
//delete with a brief delay so we don't delete the item
//before a client has managed to launch it (can happen with hitscan projectile)
CoroutineManager.Invoke(() =>
{
if (item.Removed) { return; }
Entity.Spawner?.AddItemToRemoveQueue(item);
}, delay: 0.5f);
}
return true;
@@ -1181,6 +1229,11 @@ namespace Barotrauma.Items.Components
IgnoredBodies?.Clear();
}
public bool IsAttachedTo(PhysicsBody body)
{
return stickJoint != null && (stickJoint.BodyA == body?.FarseerBody || stickJoint.BodyB == body?.FarseerBody);
}
private void StickToTarget(Body targetBody, Vector2 axis)
{
if (stickJoint != null) { return; }
@@ -1,12 +1,11 @@
using Microsoft.Xna.Framework;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
partial class RemoteController : ItemComponent
{
[Serialize("", IsPropertySaveable.No, description: "Tag or identifier of the item that should be controlled.")]
public string Target
public Identifier Target
{
get;
private set;
@@ -14,7 +14,11 @@ namespace Barotrauma.Items.Components
private readonly LocalizedString header;
private float deteriorationTimer;
private float deteriorateAlwaysResetTimer;
public float ForceDeteriorationTimer
{
get;
private set;
}
private int updateDeteriorationCounter;
private const int UpdateDeteriorationInterval = 10;
@@ -62,6 +66,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(60f, IsPropertySaveable.Yes, description: "How long will the item spontaneously deteriorate after being sabotaged.")]
public float SabotageDeteriorationDuration
{
get;
set;
}
[Serialize(80.0f, IsPropertySaveable.Yes, description: "The condition of the item has to be below this for it to become repairable. Percentages of max condition."), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f)]
public float RepairThreshold
{
@@ -83,13 +94,6 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "If set to true, the deterioration timer will always run regardless if the item is being used or not.")]
public bool DeteriorateAlways
{
get;
set;
}
private float skillRequirementMultiplier;
[Serialize(1.0f, IsPropertySaveable.Yes)]
@@ -394,20 +398,19 @@ namespace Barotrauma.Items.Components
item.SendSignal(conditionSignal, "condition_out");
if (ForceDeteriorationTimer > 0.0f)
{
ForceDeteriorationTimer -= deltaTime;
if (ForceDeteriorationTimer <= 0.0f)
{
#if SERVER
//let the clients know the deterioration delay
item.CreateServerEvent(this);
#endif
}
}
if (CurrentFixer == null)
{
if (deteriorateAlwaysResetTimer > 0.0f)
{
deteriorateAlwaysResetTimer -= deltaTime;
if (deteriorateAlwaysResetTimer <= 0.0f)
{
DeteriorateAlways = false;
#if SERVER
//let the clients know the deterioration delay
item.CreateServerEvent(this);
#endif
}
}
updateDeteriorationCounter++;
if (updateDeteriorationCounter >= UpdateDeteriorationInterval)
{
@@ -534,8 +537,7 @@ namespace Barotrauma.Items.Components
}
deteriorationTimer = 0.0f;
deteriorateAlwaysResetTimer = item.Condition / DeteriorationSpeed;
DeteriorateAlways = true;
ForceDeteriorationTimer = SabotageDeteriorationDuration;
item.Condition = item.MaxCondition * (MinSabotageCondition / 100);
wasGoodCondition = false;
}
@@ -553,7 +555,8 @@ namespace Barotrauma.Items.Components
if (item.Condition <= 0.0f) { return; }
if (!ShouldDeteriorate()) { return; }
if (deteriorationTimer > 0.0f)
//forced deterioration doesn't tick down the timer for spontaneous deterioration
if (deteriorationTimer > 0.0f && ForceDeteriorationTimer <= 0.0f)
{
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
@@ -568,6 +571,7 @@ namespace Barotrauma.Items.Components
if (item.ConditionPercentage > MinDeteriorationCondition)
{
float deteriorationSpeed = item.StatManager.GetAdjustedValue(ItemTalentStats.DetoriationSpeed, DeteriorationSpeed);
if (ForceDeteriorationTimer > 0.0f) { deteriorationSpeed = Math.Max(deteriorationSpeed, 1.0f); }
item.Condition -= deteriorationSpeed * deltaTime;
}
}
@@ -592,7 +596,7 @@ namespace Barotrauma.Items.Components
if (!character.HasAbilityFlag(AbilityFlags.CanTinker)) { return false; }
if (item.GetComponent<Engine>() != null) { return true; }
if (item.GetComponent<Pump>() != null) { return true; }
if (item.HasTag("turretammosource")) { return true; }
if (item.HasTag(Tags.TurretAmmoSource)) { return true; }
if (!character.HasAbilityFlag(AbilityFlags.CanTinkerFabricatorsAndDeconstructors)) { return false; }
if (item.GetComponent<Fabricator>() != null) { return true; }
if (item.GetComponent<Deconstructor>() != null) { return true; }
@@ -623,6 +627,8 @@ namespace Barotrauma.Items.Components
private bool ShouldDeteriorate()
{
if (ForceDeteriorationTimer > 0.0f) { return true; }
if (Level.IsLoadedFriendlyOutpost) { return false; }
#if CLIENT
if (GameMain.GameSession?.GameMode is TutorialMode) { return false; }
@@ -666,13 +672,13 @@ namespace Barotrauma.Items.Components
//oxygen generators don't deteriorate if they're not running
if (oxyGenerator.CurrFlow > 0.1f) { return true; }
}
else if (ic is Powered powered && !(powered is LightComponent))
else if (ic is Powered powered && powered is not LightComponent)
{
if (powered.Voltage >= powered.MinVoltage) { return true; }
}
}
return DeteriorateAlways;
return false;
}
private float GetDeteriorationDelayMultiplier()
@@ -181,7 +181,7 @@ namespace Barotrauma.Items.Components
return;
}
Vector2 diff = target.WorldPosition - source.WorldPosition;
Vector2 diff = target.WorldPosition - GetSourcePos(useDrawPosition: false);
float lengthSqr = diff.LengthSquared();
if (lengthSqr > MaxLength * MaxLength)
{
@@ -372,7 +372,40 @@ namespace Barotrauma.Items.Components
}
}
private PhysicsBody GetBodyToPull(ISpatialEntity target)
/// <summary>
/// Get the position the rope starts from (taking into account barrel positions if needed)
/// </summary>
/// <param name="useDrawPosition">Should the interpolated draw position be used? If not, the WorldPosition is used.</param>
private Vector2 GetSourcePos(bool useDrawPosition = false)
{
Vector2 sourcePos = source.WorldPosition;
if (source is Item sourceItem)
{
if (useDrawPosition)
{
sourcePos = sourceItem.DrawPosition;
}
if (!sourceItem.Removed)
{
if (sourceItem.GetComponent<Turret>() is { } turret)
{
sourcePos = new Vector2(sourceItem.WorldRect.X + turret.TransformedBarrelPos.X, sourceItem.WorldRect.Y - turret.TransformedBarrelPos.Y);
}
else if (sourceItem.GetComponent<RangedWeapon>() is { } weapon)
{
sourcePos += ConvertUnits.ToDisplayUnits(weapon.TransformedBarrelPos);
}
}
}
else if (useDrawPosition && source is Limb sourceLimb && sourceLimb.body != null)
{
sourcePos = sourceLimb.body.DrawPosition;
}
return sourcePos;
}
private static PhysicsBody GetBodyToPull(ISpatialEntity target)
{
if (target is Item targetItem)
{
@@ -57,7 +57,7 @@ namespace Barotrauma.Items.Components
}
}
[InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
[InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
public string FalseOutput
{
get { return falseOutput; }
@@ -0,0 +1,684 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
namespace Barotrauma.Items.Components
{
internal sealed partial class CircuitBox : ItemComponent, IClientSerializable, IServerSerializable
{
public static readonly ImmutableHashSet<CircuitBoxOpcode> UnrealiableOpcodes
= ImmutableHashSet.Create(CircuitBoxOpcode.Cursor);
public ImmutableArray<CircuitBoxInputConnection> Inputs;
public ImmutableArray<CircuitBoxOutputConnection> Outputs;
public readonly List<CircuitBoxComponent> Components = new List<CircuitBoxComponent>();
public readonly List<CircuitBoxInputOutputNode> InputOutputNodes = new();
public readonly List<CircuitBoxWire> Wires = new List<CircuitBoxWire>();
public override bool IsActive => true;
public Option<CircuitBoxConnection> FindInputOutputConnection(Identifier connectionName)
{
foreach (CircuitBoxInputConnection input in Inputs)
{
if (input.Name != connectionName) { continue; }
return Option.Some<CircuitBoxConnection>(input);
}
foreach (CircuitBoxOutputConnection output in Outputs)
{
if (output.Name != connectionName) { continue; }
return Option.Some<CircuitBoxConnection>(output);
}
return Option.None;
}
public readonly ItemContainer[] containers;
private const int ComponentContainerIndex = 0,
WireContainerIndex = 1;
public ItemContainer? ComponentContainer
=> GetContainerOrNull(ComponentContainerIndex);
// wire container falls back to the main container if one isn't specified
public ItemContainer? WireContainer
=> GetContainerOrNull(WireContainerIndex) ?? GetContainerOrNull(ComponentContainerIndex);
public bool IsFull => ComponentContainer?.Inventory is { } inventory && inventory.IsFull(true);
public CircuitBox(Item item, ContentXElement element) : base(item, element)
{
containers = item.GetComponents<ItemContainer>().ToArray();
if (containers.Length < 1)
{
DebugConsole.ThrowError("Circuit box must have at least one item container to function.");
}
InitProjSpecific(element);
var inputBuilder = ImmutableArray.CreateBuilder<CircuitBoxInputConnection>();
var outputBuilder = ImmutableArray.CreateBuilder<CircuitBoxOutputConnection>();
foreach (Connection conn in Item.Connections)
{
if (conn.IsOutput)
{
outputBuilder.Add(new CircuitBoxOutputConnection(Vector2.Zero, conn, this));
}
else
{
inputBuilder.Add(new CircuitBoxInputConnection(Vector2.Zero, conn, this));
}
}
Inputs = inputBuilder.ToImmutable();
Outputs = outputBuilder.ToImmutable();
InputOutputNodes.Add(new CircuitBoxInputOutputNode(Inputs, new Vector2(-512, 0f), CircuitBoxInputOutputNode.Type.Input, this));
InputOutputNodes.Add(new CircuitBoxInputOutputNode(Outputs, new Vector2(512, 0f), CircuitBoxInputOutputNode.Type.Output, this));
item.OnDeselect += OnDeselected;
}
/// <summary>
/// We want to load the components after the map has loaded since we need to link up the components to their items
/// and pretty much all items have higher ID than the circuit box.
/// </summary>
private Option<ContentXElement> delayedElementToLoad;
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap)
{
base.Load(componentElement, usePrefabValues, idRemap);
if (delayedElementToLoad.IsSome()) { return; }
delayedElementToLoad = Option.Some(componentElement);
}
public override void OnInventoryChanged()
=> OnViewUpdateProjSpecific();
public override void Update(float deltaTime, Camera cam)
{
#if CLIENT
// When loading from the server the wires cannot be properly loaded and connected up because we might not be loaded in properly yet.
// So we need to wait until the circuit box starts updating and then we can ensure the wires are connected.
if (wasInitializedByServer)
{
foreach (var w in Wires)
{
w.EnsureWireConnected();
}
wasInitializedByServer = false;
}
#endif
TryInitializeNodes();
}
public override void OnMapLoaded()
=> TryInitializeNodes();
private void TryInitializeNodes()
{
if (!delayedElementToLoad.TryUnwrap(out var loadElement)) { return; }
LoadFromXML(loadElement);
delayedElementToLoad = Option.None;
}
private void LoadFromXML(ContentXElement loadElement)
{
foreach (var subElement in loadElement.Elements())
{
string elementName = subElement.Name.ToString().ToLowerInvariant();
switch (elementName)
{
case "component" when CircuitBoxComponent.TryLoadFromXML(subElement, this).TryUnwrap(out var comp):
Components.Add(comp);
break;
case "wire" when CircuitBoxWire.TryLoadFromXML(subElement, this).TryUnwrap(out var wire):
Wires.Add(wire);
break;
case "inputnode":
LoadFor(CircuitBoxInputOutputNode.Type.Input, subElement);
break;
case "outputnode":
LoadFor(CircuitBoxInputOutputNode.Type.Output, subElement);
break;
}
}
#if SERVER
// We need to let the clients know of the loaded data
if (needsServerInitialization)
{
CreateInitializationEvent();
needsServerInitialization = false;
}
#endif
void LoadFor(CircuitBoxInputOutputNode.Type type, ContentXElement subElement)
{
foreach (var node in InputOutputNodes)
{
if (node.NodeType != type) { continue; }
node.Load(subElement);
break;
}
}
}
public void CloneFrom(CircuitBox original, Dictionary<ushort, Item> clonedContainedItems)
{
Components.Clear();
Wires.Clear();
foreach (var origComp in original.Components)
{
var newComponent = new CircuitBoxComponent(origComp.ID, clonedContainedItems[origComp.Item.ID], origComp.Position, this, origComp.UsedResource);
Components.Add(newComponent);
}
for (int ioIndex = 0; ioIndex < original.InputOutputNodes.Count; ioIndex++)
{
var origNode = original.InputOutputNodes[ioIndex];
var cloneNode = InputOutputNodes[ioIndex];
cloneNode.Position = origNode.Position;
}
foreach (var origWire in original.Wires)
{
Option<CircuitBoxConnection> to = CircuitBoxConnectorIdentifier.FromConnection(origWire.To).FindConnection(this),
from = CircuitBoxConnectorIdentifier.FromConnection(origWire.From).FindConnection(this);
if (!to.TryUnwrap(out var toConn) || !from.TryUnwrap(out var fromConn))
{
DebugConsole.ThrowError($"Error while cloning item \"{Name}\" - failed to find a connection for a wire. ");
continue;
}
var newWire = new CircuitBoxWire(this, origWire.ID, origWire.BackingWire.Select(w => clonedContainedItems[w.ID]), fromConn, toConn, origWire.UsedItemPrefab);
Wires.Add(newWire);
}
}
public override XElement Save(XElement parentElement)
{
XElement componentElement = base.Save(parentElement);
foreach (CircuitBoxInputOutputNode node in InputOutputNodes)
{
componentElement.Add(node.Save());
}
foreach (CircuitBoxComponent node in Components)
{
componentElement.Add(node.Save());
}
foreach (CircuitBoxWire wire in Wires)
{
componentElement.Add(wire.Save());
}
return componentElement;
}
public partial void OnDeselected(Character c);
public record struct CreatedWire(CircuitBoxConnectorIdentifier Start, CircuitBoxConnectorIdentifier End, Option<Item> Item, ushort ID);
public bool Connect(CircuitBoxConnection one, CircuitBoxConnection two, Action<CreatedWire> onCreated, ItemPrefab selectedWirePrefab)
{
if (!VerifyConnection(one, two)) { return false; }
ushort id = ICircuitBoxIdentifiable.FindFreeID(Wires);
switch (one.IsOutput)
{
case true when !two.IsOutput:
{
CircuitBoxConnectorIdentifier start = CircuitBoxConnectorIdentifier.FromConnection(one),
end = CircuitBoxConnectorIdentifier.FromConnection(two);
if (IsExternalConnection(one) || IsExternalConnection(two))
{
CreateWireWithoutItem(one, two, id, selectedWirePrefab);
onCreated(new CreatedWire(start, end, Option.None, id));
return true;
}
CreateWireWithItem(one, two, selectedWirePrefab, id, i => onCreated(new CreatedWire(start, end, Option.Some(i), id)));
return true;
}
case false when two.IsOutput:
{
CircuitBoxConnectorIdentifier start = CircuitBoxConnectorIdentifier.FromConnection(two),
end = CircuitBoxConnectorIdentifier.FromConnection(one);
if (IsExternalConnection(one) || IsExternalConnection(two))
{
CreateWireWithoutItem(two, one, id, selectedWirePrefab);
onCreated(new CreatedWire(start, end, Option.None, id));
return true;
}
CreateWireWithItem(two, one, selectedWirePrefab, id, i => onCreated(new CreatedWire(start, end, Option.Some(i), id)));
return true;
}
}
return false;
}
private static bool VerifyConnection(CircuitBoxConnection one, CircuitBoxConnection two)
{
if (one.IsOutput == two.IsOutput || one == two) { return false; }
if (one is CircuitBoxNodeConnection oneNodeConnection &&
two is CircuitBoxNodeConnection twoNodeConnection)
{
if (oneNodeConnection.Component == twoNodeConnection.Component)
{
return false;
}
}
if (one is CircuitBoxNodeConnection { HasAvailableSlots: false } ||
two is CircuitBoxNodeConnection { HasAvailableSlots: false })
{
return one is not CircuitBoxNodeConnection || two is not CircuitBoxNodeConnection;
}
return true;
}
private static bool IsExternalConnection(CircuitBoxConnection conn) => conn is (CircuitBoxInputConnection or CircuitBoxOutputConnection);
private void CreateWireWithoutItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort id, ItemPrefab prefab)
{
bool hasExternalConnection = false;
if (one is CircuitBoxInputConnection input)
{
hasExternalConnection = true;
input.ExternallyConnectedTo.Add(two);
}
if (two is CircuitBoxOutputConnection output)
{
hasExternalConnection = true;
one.Connection.CircuitBoxConnections.Add(output);
}
if (hasExternalConnection)
{
two.ExternallyConnectedFrom.Add(one);
}
AddWireDirect(id, prefab, Option.None, one, two);
}
private void CreateWireWithItem(CircuitBoxConnection one, CircuitBoxConnection two, ItemPrefab prefab, ushort wireId, Action<Item> onItemSpawned)
{
if (WireContainer is null) { return; }
if (IsExternalConnection(one) || IsExternalConnection(two))
{
DebugConsole.ThrowError("Cannot add a wire between an external connection and a component connection.");
return;
}
SpawnItem(this, prefab, WireContainer, wire =>
{
AddWireDirect(wireId, prefab, Option.Some(wire), one, two);
onItemSpawned(wire);
});
}
private void CreateWireWithItem(CircuitBoxConnection one, CircuitBoxConnection two, ushort wireId, Item it)
{
if (IsExternalConnection(one) || IsExternalConnection(two))
{
DebugConsole.ThrowError("Cannot add a wire between an external connection and a component connection.");
return;
}
AddWireDirect(wireId, it.Prefab, Option.Some(it), one, two);
}
private void AddWireDirect(ushort id, ItemPrefab prefab, Option<Item> backingItem, CircuitBoxConnection one, CircuitBoxConnection two)
=> Wires.Add(new CircuitBoxWire(this, id, backingItem, one, two, prefab));
private bool AddComponentInternal(ushort id, ItemPrefab prefab, ItemPrefab usedResource, Vector2 pos, Action<Item> onItemSpawned)
{
if (id is ICircuitBoxIdentifiable.NullComponentID)
{
DebugConsole.ThrowError("Unable to add component because there are no free IDs.");
return false;
}
if (ComponentContainer?.Inventory is { } inventory && inventory.HowManyCanBePut(prefab) <= 0)
{
DebugConsole.ThrowError("Unable to add component because there is no space in the inventory.");
return false;
}
SpawnItem(this, prefab, ComponentContainer, spawnedItem =>
{
Components.Add(new CircuitBoxComponent(id, spawnedItem, pos, this, usedResource));
onItemSpawned(spawnedItem);
});
OnViewUpdateProjSpecific();
return true;
}
// Unsafe because it doesn't perform error checking since it's data we get from the server
private void AddComponentInternalUnsafe(ushort id, Item backingItem, ItemPrefab usedResource, Vector2 pos)
{
Components.Add(new CircuitBoxComponent(id, backingItem, pos, this, usedResource));
OnViewUpdateProjSpecific();
}
private static void ClearSelectionFor(ushort characterId, IReadOnlyCollection<CircuitBoxSelectable> nodes)
{
foreach (var node in nodes)
{
if (node.SelectedBy != characterId) { continue; }
node.SetSelected(Option.None);
}
}
private void ClearAllSelectionsInternal(ushort characterId)
{
ClearSelectionFor(characterId, Components);
ClearSelectionFor(characterId, InputOutputNodes);
ClearSelectionFor(characterId, Wires);
}
private void SelectComponentsInternal(IReadOnlyCollection<ushort> ids, ushort characterId, bool overwrite)
{
if (overwrite) { ClearSelectionFor(characterId, Components); }
if (!ids.Any()) { return; }
foreach (CircuitBoxComponent node in Components)
{
if (!ids.Contains(node.ID)) { continue; }
node.SetSelected(Option.Some(characterId));
}
}
private void UpdateSelections(ImmutableDictionary<ushort, Option<ushort>> nodeIds,
ImmutableDictionary<ushort, Option<ushort>> wireIds,
ImmutableDictionary<CircuitBoxInputOutputNode.Type, Option<ushort>> inputOutputs)
{
foreach (var wire in Wires)
{
if (!wireIds.TryGetValue(wire.ID, out var selectedBy)) { continue; }
if (selectedBy.TryUnwrap(out var id))
{
wire.IsSelected = true;
wire.SelectedBy = id;
continue;
}
wire.IsSelected = false;
wire.SelectedBy = 0;
}
foreach (var node in Components)
{
if (!nodeIds.TryGetValue(node.ID, out var selectedBy)) { continue; }
node.SetSelected(selectedBy);
}
foreach (var node in InputOutputNodes)
{
if (!inputOutputs.TryGetValue(node.NodeType, out var selectedBy)) { continue; }
node.SetSelected(selectedBy);
}
}
private void SelectWiresInternal(IReadOnlyCollection<ushort> ids, ushort characterId, bool overwrite)
{
if (overwrite) { ClearSelectionFor(characterId, Wires); }
foreach (CircuitBoxWire wire in Wires)
{
if (!ids.Contains(wire.ID)) { continue; }
wire.SetSelected(Option.Some(characterId));
}
}
private void SelectInputOutputInternal(IReadOnlyCollection<CircuitBoxInputOutputNode.Type> io, ushort characterId, bool overwrite)
{
if (overwrite) { ClearSelectionFor(characterId, InputOutputNodes); }
foreach (var node in InputOutputNodes)
{
if (!io.Contains(node.NodeType)) { continue; }
node.SetSelected(Option.Some(characterId));
}
}
private void RemoveComponentInternal(IReadOnlyCollection<ushort> ids)
{
foreach (CircuitBoxComponent node in Components.ToImmutableArray())
{
if (!ids.Contains(node.ID)) { continue; }
Components.Remove(node);
node.Remove();
foreach (CircuitBoxWire wire in Wires.ToImmutableArray())
{
if (node.Connectors.Contains(wire.From) || node.Connectors.Contains(wire.To))
{
RemoveWireCollectionUnsafe(wire);
}
}
}
OnViewUpdateProjSpecific();
}
private void RemoveWireInternal(IReadOnlyCollection<ushort> ids)
{
foreach (CircuitBoxWire wire in Wires.ToImmutableArray())
{
if (!ids.Contains(wire.ID)) { continue; }
RemoveWireCollectionUnsafe(wire);
}
OnViewUpdateProjSpecific();
}
private void RemoveWireCollectionUnsafe(CircuitBoxWire wire)
{
foreach (CircuitBoxOutputConnection output in Outputs)
{
output.Connection.CircuitBoxConnections.Remove(wire.From);
}
wire.From.Connection.CircuitBoxConnections.Remove(wire.To);
if (wire.From is CircuitBoxInputConnection input)
{
input.ExternallyConnectedTo.Remove(wire.To);
}
wire.To.ExternallyConnectedFrom.Remove(wire.From);
wire.From.ExternallyConnectedFrom.Remove(wire.To);
wire.Remove();
Wires.Remove(wire);
}
private void MoveNodesInternal(IReadOnlyCollection<ushort> ids,
IReadOnlyCollection<CircuitBoxInputOutputNode.Type> ios,
Vector2 moveAmount)
{
IEnumerable<CircuitBoxComponent> nodes = Components.Where(node => ids.Contains(node.ID));
foreach (CircuitBoxComponent node in nodes)
{
node.Position += moveAmount;
}
foreach (var io in InputOutputNodes)
{
if (!ios.Contains(io.NodeType)) { continue; }
io.Position += moveAmount;
}
OnViewUpdateProjSpecific();
}
public override bool Select(Character character)
=> item.GetComponent<Holdable>() is not { Attached: false } && base.Select(character);
public partial void OnViewUpdateProjSpecific();
partial void InitProjSpecific(ContentXElement element);
public override void ReceiveSignal(Signal signal, Connection connection)
{
foreach (var input in Inputs)
{
if (input.Connection != connection) { continue; }
input.ReceiveSignal(signal);
break;
}
}
public static bool IsRoundRunning()
=> !Submarine.Unloading && GameMain.GameSession is { IsRunning: true };
public static Option<CircuitBox> FindCircuitBox(ushort itemId, byte componentIndex)
{
if (!IsRoundRunning() || Entity.FindEntityByID(itemId) is not Item item) { return Option.None; }
if (componentIndex >= item.Components.Count)
{
return Option.None;
}
ItemComponent targetComponent = item.Components[componentIndex];
if (targetComponent is CircuitBox circuitBox)
{
return Option.Some(circuitBox);
}
return Option.None;
}
private ItemContainer? GetContainerOrNull(int index) => index >= 0 && index < containers.Length ? containers[index] : null;
public void CreateRefundItemsForUsedResources(IReadOnlyCollection<ushort> ids, Character? character)
{
if (!IsInGame()) { return; }
var prefabsToCreate = Components.Where(comp => ids.Contains(comp.ID))
.Select(static comp => comp.UsedResource)
.ToImmutableArray();
foreach (ItemPrefab prefab in prefabsToCreate)
{
if (character?.Inventory is null)
{
Entity.Spawner?.AddItemToSpawnQueue(prefab, item.Position, item.Submarine);
}
else
{
Entity.Spawner?.AddItemToSpawnQueue(prefab, character.Inventory);
}
}
}
public static ImmutableArray<Item> GetSortedCircuitBoxSortedItemsFromPlayer(Character? character)
=> character?.Inventory?.FindAllItems(predicate: CanItemBeAccessed, recursive: true)
.OrderBy(static i => i.Prefab.Identifier == Tags.FPGACircuit)
.ToImmutableArray() ?? ImmutableArray<Item>.Empty;
public static bool CanItemBeAccessed(Item item) =>
item.ParentInventory switch
{
ItemInventory ii => ii.Container.DrawInventory,
_ => true
};
public static Option<Item> GetApplicableResourcePlayerHas(ItemPrefab prefab, Character? character)
{
if (character is null) { return Option.None; }
return GetApplicableResourcePlayerHas(prefab, GetSortedCircuitBoxSortedItemsFromPlayer(character));
}
public static Option<Item> GetApplicableResourcePlayerHas(ItemPrefab prefab, ImmutableArray<Item> playerItems)
{
foreach (var invItem in playerItems)
{
if (invItem.Prefab == prefab || invItem.Prefab.Identifier == Tags.FPGACircuit)
{
return Option.Some(invItem);
}
}
return Option.None;
}
public static void SpawnItem(CircuitBox circuitBox, ItemPrefab prefab, ItemContainer? container, Action<Item> onSpawned)
{
if (container is null)
{
throw new Exception("Circuit box has no inventory");
}
if (IsInGame())
{
Entity.Spawner?.AddItemToSpawnQueue(prefab, container.Inventory, onSpawned: onSpawned);
return;
}
Item forceSpawnedItem = new Item(prefab, Vector2.Zero, null);
container.Inventory.TryPutItem(forceSpawnedItem, null);
onSpawned(forceSpawnedItem);
}
public static void RemoveItem(Item item)
{
if (IsInGame())
{
Entity.Spawner?.AddItemToRemoveQueue(item);
return;
}
item.Remove();
}
public static bool IsInGame()
=> Screen.Selected is not { IsEditor: true };
public static bool IsCircuitBoxSelected(Character character)
=> character.SelectedItem?.GetComponent<CircuitBox>() is not null;
}
}
@@ -23,6 +23,15 @@ namespace Barotrauma.Items.Components
private readonly HashSet<Wire> wires;
public IReadOnlyCollection<Wire> Wires => wires;
/// <summary>
/// Circuit box input and output connections that are linked to this connection.
/// </summary>
/// <remarks>
/// We don't want to create a wire between the circuit boxes connection panel and the
/// connection panel of the item inside the circuit box so we use this to bridge the gap.
/// </remarks>
public List<CircuitBoxConnection> CircuitBoxConnections = new();
private bool enumeratingWires;
private readonly HashSet<Wire> removedWires = new HashSet<Wire>();
@@ -177,6 +186,12 @@ namespace Barotrauma.Items.Components
}
}
/// <summary>
/// Checks if the the connection is connected to a wire or a circuit box connection
/// </summary>
public bool IsConnectedToSomething()
=> wires.Count > 0 || CircuitBoxConnections.Count > 0;
public void SetRecipientsDirty()
{
recipientsDirty = true;
@@ -304,25 +319,15 @@ namespace Barotrauma.Items.Components
if (recipient.item == this.item || signal.source?.LastSentSignalRecipients.LastOrDefault() == recipient) { continue; }
signal.source?.LastSentSignalRecipients.Add(recipient);
Connection connection = recipient;
connection.LastReceivedSignal = signal;
#if CLIENT
wire.RegisterSignal(signal, source: this);
#endif
SendSignalIntoConnection(signal, recipient);
}
foreach (ItemComponent ic in recipient.item.Components)
{
ic.ReceiveSignal(signal, connection);
}
if (recipient.Effects != null && signal.value != "0")
{
foreach (StatusEffect effect in recipient.Effects)
{
recipient.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
}
}
foreach (CircuitBoxConnection connection in CircuitBoxConnections)
{
connection.ReceiveSignal(signal);
}
enumeratingWires = false;
foreach (var removedWire in removedWires)
@@ -331,7 +336,24 @@ namespace Barotrauma.Items.Components
}
removedWires.Clear();
}
public static void SendSignalIntoConnection(Signal signal, Connection conn)
{
conn.LastReceivedSignal = signal;
foreach (ItemComponent ic in conn.item.Components)
{
ic.ReceiveSignal(signal, conn);
}
if (conn.Effects == null || signal.value == "0") { return; }
foreach (StatusEffect effect in conn.Effects)
{
conn.Item.ApplyStatusEffect(effect, ActionType.OnUse, (float)Timing.Step);
}
}
public void ClearConnections()
{
if (IsPower && Grid != null)
@@ -9,7 +9,8 @@ namespace Barotrauma.Items.Components
{
partial class ConnectionPanel : ItemComponent, IServerSerializable, IClientSerializable
{
public List<Connection> Connections;
const int MaxConnectionCount = 256;
public readonly List<Connection> Connections = new List<Connection>();
private Character user;
@@ -67,10 +68,13 @@ namespace Barotrauma.Items.Components
public ConnectionPanel(Item item, ContentXElement element)
: base(item, element)
{
Connections = new List<Connection>();
foreach (var subElement in element.Elements())
{
if (Connections.Count == MaxConnectionCount)
{
DebugConsole.ThrowError($"Too many connections in the item {item.Prefab.Identifier} (> {MaxConnectionCount}).");
break;
}
switch (subElement.Name.ToString())
{
case "input":
@@ -179,7 +183,7 @@ namespace Barotrauma.Items.Components
{
UpdateProjSpecific(deltaTime);
if (user == null || user.SelectedItem != item)
if (user == null || (user.SelectedItem != item && user.SelectedSecondaryItem != item))
{
#if SERVER
if (user != null) { item.CreateServerEvent(this); }
@@ -208,6 +212,10 @@ namespace Barotrauma.Items.Components
public bool CanRewire()
{
if (item.Container?.GetComponent<CircuitBox>() != null)
{
return true;
}
//attaching wires to items with a body is not allowed
//(signal items remove their bodies when attached to a wall)
if (item.body != null && item.body.BodyType == FarseerPhysics.BodyType.Dynamic)
@@ -395,7 +403,7 @@ namespace Barotrauma.Items.Components
#if CLIENT
TriggerRewiringSound();
#endif
msg.WriteByte((byte)Connections.Count);
foreach (Connection connection in Connections)
{
msg.WriteVariableUInt32((uint)connection.Wires.Count);
@@ -299,9 +299,12 @@ namespace Barotrauma.Items.Components
//make sure the clients know about the states of the checkboxes and text fields
if (customInterfaceElementList.Any())
{
if (item.Submarine == null || !item.Submarine.Loading)
if (item.FullyInitialized)
{
item.CreateServerEvent(this);
CoroutineManager.Invoke(() =>
{
if (!item.Removed) { item.CreateServerEvent(this); }
}, delay: 0.1f);
}
}
#endif
@@ -43,7 +43,7 @@ namespace Barotrauma.Items.Components
}
}
[InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
[InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal sent when the condition is met (if empty, no signal is sent).", alwaysUseInstanceValues: true)]
public string FalseOutput
{
get { return falseOutput; }
@@ -198,6 +198,22 @@ namespace Barotrauma.Items.Components
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>
public bool IsRed => ColorExtensions.IsRedDominant(LightColor);
/// <summary>
/// Returns true if the green component of the light is twice as bright as the red and blue. Can be used by StatusEffects.
/// </summary>
public bool IsGreen => ColorExtensions.IsGreenDominant(LightColor);
/// <summary>
/// Returns true if the blue component of the light is twice as bright as the red and green. Can be used by StatusEffects.
/// </summary>
public bool IsBlue => ColorExtensions.IsBlueDominant(LightColor);
public float TemporaryFlickerTimer;
public override void Move(Vector2 amount, bool ignoreContacts = false)
@@ -121,7 +121,7 @@ namespace Barotrauma.Items.Components
}
private string falseOutput;
[InGameEditable, Serialize("", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)]
[InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal the item outputs when it has not detected movement.", alwaysUseInstanceValues: true)]
public string FalseOutput
{
get { return falseOutput; }
@@ -136,7 +136,7 @@ namespace Barotrauma.Items.Components
}
}
[Editable(DecimalCount = 3), Serialize(0.1f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)]
[InGameEditable(DecimalCount = 3), Serialize(0.0f, IsPropertySaveable.Yes, description: "How fast the objects within the detector's range have to be moving (in m/s).", alwaysUseInstanceValues: true)]
public float MinimumVelocity
{
get;
@@ -254,45 +254,52 @@ namespace Barotrauma.Items.Components
}
}
if (Target.HasFlag(TargetType.Human) || Target.HasFlag(TargetType.Pet) || Target.HasFlag(TargetType.Monster))
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 c 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 (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 (!Target.HasFlag(TargetType.Human)) { continue; }
}
else if (c.IsPet)
{
if (!Target.HasFlag(TargetType.Pet)) { continue; }
}
else
{
if (!Target.HasFlag(TargetType.Monster)) { 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 (!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;
}
}
foreach (Limb limb in c.AnimController.Limbs)
//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)
{
continue;
}
foreach (Limb limb in c.AnimController.Limbs)
{
if (limb.IsSevered) { continue; }
if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; }
if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect))
{
if (limb.IsSevered) { continue; }
if (limb.LinearVelocity.LengthSquared() < MinimumVelocity * MinimumVelocity) { continue; }
if (MathUtils.CircleIntersectsRectangle(limb.WorldPosition, ConvertUnits.ToDisplayUnits(limb.body.GetMaxExtent()), detectRect))
{
MotionDetected = true;
return;
}
MotionDetected = true;
return;
}
}
}
@@ -50,6 +50,9 @@ namespace Barotrauma.Items.Components
[InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component output a value of a capture group instead of a constant signal.", alwaysUseInstanceValues: true)]
public bool UseCaptureGroup { get; set; }
[InGameEditable, Serialize(false, IsPropertySaveable.Yes, description: "Should the component output the value of a capture group even if it's empty?", alwaysUseInstanceValues: true)]
public bool OutputEmptyCaptureGroup { get; set; }
[InGameEditable, Serialize("0", IsPropertySaveable.Yes, description: "The signal this item outputs when the received signal does not match the regular expression.", alwaysUseInstanceValues: true)]
public string FalseOutput { get; set; }
@@ -120,6 +123,7 @@ namespace Barotrauma.Items.Components
}
string signalOut;
bool allowEmptyStringOutput = false;
if (previousResult)
{
if (UseCaptureGroup)
@@ -127,6 +131,7 @@ namespace Barotrauma.Items.Components
if (previousGroups != null && previousGroups.TryGetValue(Output, out Group group))
{
signalOut = group.Value;
allowEmptyStringOutput = OutputEmptyCaptureGroup;
}
else
{
@@ -143,13 +148,9 @@ namespace Barotrauma.Items.Components
signalOut = FalseOutput;
}
if (ContinuousOutput)
if (!string.IsNullOrEmpty(signalOut) || (allowEmptyStringOutput && signalOut == string.Empty)) { item.SendSignal(signalOut, "signal_out"); }
if (!ContinuousOutput)
{
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); }
}
else
{
if (!string.IsNullOrEmpty(signalOut)) { item.SendSignal(signalOut, "signal_out"); }
nonContinuousOutputSent = true;
}
}
@@ -70,7 +70,7 @@ namespace Barotrauma.Items.Components
}
}
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)]
[InGameEditable, Serialize(true, IsPropertySaveable.Yes, description: "Can the relay currently pass power and signals through it.", alwaysUseInstanceValues: true)]
public bool IsOn
{
get
@@ -139,7 +139,7 @@ namespace Barotrauma.Items.Components
isBroken = false;
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (Voltage > OverloadVoltage && CanBeOverloaded && item.Repairables.Any())
{
@@ -10,11 +10,13 @@ namespace Barotrauma.Items.Components
{
public readonly string Text;
public readonly Color Color;
public readonly bool IsWelcomeMessage;
public TerminalMessage(string text, Color color)
public TerminalMessage(string text, Color color, bool isWelcomeMessage)
{
Text = text;
Color = color;
IsWelcomeMessage = isWelcomeMessage;
}
public void Deconstruct(out string text, out Color color)
@@ -60,7 +62,7 @@ namespace Barotrauma.Items.Components
set
{
if (string.IsNullOrEmpty(value)) { return; }
ShowOnDisplay(value, addToHistory: true, TextColor);
ShowOnDisplay(value, addToHistory: true, TextColor, isWelcomeMessage: false);
}
}
@@ -113,7 +115,7 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(XElement element);
partial void ShowOnDisplay(string input, bool addToHistory, Color color);
partial void ShowOnDisplay(string input, bool addToHistory, Color color, bool isWelcomeMessage);
public override void ReceiveSignal(Signal signal, Connection connection)
{
@@ -127,7 +129,7 @@ namespace Barotrauma.Items.Components
signal.value = signal.value.Substring(0, MaxMessageLength);
}
string inputSignal = signal.value.Replace("\\n", "\n");
ShowOnDisplay(inputSignal, addToHistory: true, TextColor);
ShowOnDisplay(inputSignal, addToHistory: true, TextColor, isWelcomeMessage: false);
break;
case "set_text_color":
if (signal.value != prevColorSignal)
@@ -160,7 +162,7 @@ namespace Barotrauma.Items.Components
base.OnItemLoaded();
if (!DisplayedWelcomeMessage.IsNullOrEmpty() && !WelcomeMessageDisplayed)
{
ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor);
ShowOnDisplay(DisplayedWelcomeMessage.Value, addToHistory: !isSubEditor, TextColor, isWelcomeMessage: true);
DisplayedWelcomeMessage = "";
//disable welcome message if a game session is running so it doesn't reappear on successive rounds
if (GameMain.GameSession != null && !isSubEditor)
@@ -175,8 +177,13 @@ namespace Barotrauma.Items.Components
var componentElement = base.Save(parentElement);
for (int i = 0; i < messageHistory.Count; i++)
{
componentElement.Add(new XAttribute("msg" + i, messageHistory[i].Text));
componentElement.Add(new XAttribute("color" + i, messageHistory[i].Color.ToStringHex()));
var msg = messageHistory[i];
componentElement.Add(new XAttribute("msg" + i, msg.Text));
componentElement.Add(new XAttribute("color" + i, msg.Color.ToStringHex()));
if (msg.IsWelcomeMessage)
{
componentElement.Add(new XAttribute("welcomemessage" + i, true));
}
}
return componentElement;
}
@@ -189,7 +196,8 @@ namespace Barotrauma.Items.Components
string msg = componentElement.GetAttributeString("msg" + i, null);
if (msg is null) { break; }
Color color = componentElement.GetAttributeColor("color" + i, TextColor);
ShowOnDisplay(msg, addToHistory: true, color);
bool isWelcomeMessage = componentElement.GetAttributeBool("welcomemessage" + i, false);
ShowOnDisplay(msg, addToHistory: true, color, isWelcomeMessage);
}
}
}
@@ -69,7 +69,10 @@ namespace Barotrauma.Items.Components
public static int GetWaterPercentage(Hull hull)
{
return hull.WaterVolume > 1.0f ? MathHelper.Clamp((int)Math.Ceiling(hull.WaterPercentage), 0, 100) : 0;
//treat less than one pixel of water as "no water"
return hull.WaterVolume / hull.Rect.Width > 1.0f ?
MathHelper.Clamp((int)Math.Ceiling(hull.WaterPercentage), 0, 100) :
0;
}
public override void Update(float deltaTime, Camera cam)
@@ -88,7 +91,7 @@ namespace Barotrauma.Items.Components
//item in water -> we definitely want to send the True output
isInWater = true;
}
else if (item.CurrentHull != null && item.CurrentHull.WaterPercentage > 0.0f && item.CurrentHull.WaterVolume > 1.0f)
else if (item.CurrentHull != null && GetWaterPercentage(item.CurrentHull) > 0)
{
//(center of the) item in not water -> check if the water surface is below the bottom of the item's rect
if (item.CurrentHull.Surface > item.Rect.Y - item.Rect.Height)
@@ -89,6 +89,26 @@ namespace Barotrauma.Items.Components
set;
}
private float jamTimer;
public float JamTimer
{
get { return jamTimer; }
set
{
if (value > 0)
{
#if CLIENT
if (jamTimer <= 0)
{
HintManager.OnRadioJammed(Item);
}
#endif
IsActive = true;
}
jamTimer = Math.Max(0, value);
}
}
public WifiComponent(Item item, ContentXElement element)
: base (item, element)
{
@@ -123,8 +143,12 @@ namespace Barotrauma.Items.Components
}
}
public bool CanTransmit()
public bool CanTransmit(bool ignoreJamming = false)
{
if (!ignoreJamming)
{
if (jamTimer > 0) { return false; }
}
return HasRequiredContainedItems(user: null, addMessage: false);
}
@@ -140,12 +164,13 @@ namespace Barotrauma.Items.Components
{
if (sender == null || sender.channel != channel) { return false; }
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; }
if (jamTimer > 0) { return false; }
//if the component is not linked to chat and has nothing connected to the output, sending a signal to it does nothing
// = no point in receiving
if (!LinkToChat)
{
if (signalOutConnection == null || signalOutConnection.Wires.Count <= 0)
if (signalOutConnection == null || !signalOutConnection.IsConnectedToSomething())
{
return false;
}
@@ -169,12 +194,16 @@ namespace Barotrauma.Items.Components
if (sender == null || sender.channel != channel) { return false; }
if (sender.TeamID != TeamID && !AllowCrossTeamCommunication) { return false; }
if (Vector2.DistanceSquared(item.WorldPosition, sender.item.WorldPosition) > sender.range * sender.range) { return false; }
if (jamTimer > 0) { return false; }
return HasRequiredContainedItems(user: null, addMessage: false);
}
public override void Update(float deltaTime, Camera cam)
{
chatMsgCooldown -= deltaTime;
if (chatMsgCooldown <= 0.0f)
JamTimer -= deltaTime;
ApplyStatusEffects(ActionType.OnActive, deltaTime);
if (chatMsgCooldown <= 0.0f && JamTimer <= 0.0f)
{
IsActive = false;
}
@@ -110,7 +110,14 @@ namespace Barotrauma.Items.Components
get;
set;
}
[Serialize(true, IsPropertySaveable.Yes, "If disabled, the wire will not be dropped when connecting. Used in circuit box to store the wires inside the box.")]
public bool DropOnConnect
{
get;
set;
}
public Wire(Item item, ContentXElement element)
: base(item, element)
{
@@ -224,26 +231,31 @@ namespace Barotrauma.Items.Components
connections[connectionIndex] = newConnection;
FixNodeEnds();
if (addNode)
if (addNode)
{
AddNode(newConnection, connectionIndex);
}
SetConnectedDirty();
if (connections[0] != null && connections[1] != null)
if (DropOnConnect)
{
foreach (ItemComponent ic in item.Components)
if (connections[0] != null && connections[1] != null)
{
if (ic == this) { continue; }
ic.Drop(null);
foreach (ItemComponent ic in item.Components)
{
if (ic == this) { continue; }
ic.Drop(null);
}
item.Container?.RemoveContained(item);
if (item.body != null) { item.body.Enabled = false; }
IsActive = false;
CleanNodes();
}
item.Container?.RemoveContained(item);
if (item.body != null) { item.body.Enabled = false; }
IsActive = false;
CleanNodes();
}
if (item.body != null) { item.Submarine = newConnection.Item.Submarine; }
@@ -304,6 +304,21 @@ namespace Barotrauma.Items.Components
set;
}
private float _maxAngleOffset;
[Serialize(10.0f, IsPropertySaveable.No, description: "How much off the turret can be from the target for the AI to shoot. In degrees.")]
public float MaxAngleOffset
{
get => _maxAngleOffset;
private set => _maxAngleOffset = MathHelper.Clamp(value, 0f, 180f);
}
[Serialize(1.1f, IsPropertySaveable.No, description: "How much does the AI prefer currently selected targets over new targets closer to the turret.")]
public float AICurrentTargetPriorityMultiplier
{
get;
private set;
}
[Serialize(-1, IsPropertySaveable.Yes, description: "The turret won't fire additional projectiles if the number of previously fired, still active projectiles reaches this limit. If set to -1, there is no limit to the number of projectiles.")]
public int MaxActiveProjectiles
{
@@ -383,7 +398,7 @@ namespace Barotrauma.Items.Components
}
item.IsShootable = true;
item.RequireAimToUse = false;
isSlowTurret = item.HasTag("slowturret");
isSlowTurret = item.HasTag("slowturret".ToIdentifier());
InitProjSpecific(element);
}
@@ -474,7 +489,7 @@ namespace Barotrauma.Items.Components
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, null);
ApplyStatusEffects(ActionType.OnActive, deltaTime);
float previousChargeTime = currentChargeTime;
@@ -594,9 +609,9 @@ namespace Barotrauma.Items.Components
UpdateLightComponents();
if (AutoOperate)
if (AutoOperate && ActiveUser == null)
{
UpdateAutoOperate(deltaTime);
UpdateAutoOperate(deltaTime, ignorePower: false);
}
}
@@ -687,7 +702,7 @@ namespace Barotrauma.Items.Components
ItemContainer projectileContainer = projectiles.First().Item.Container?.GetComponent<ItemContainer>();
if (projectileContainer != null && projectileContainer.Item != item)
{
projectileContainer?.Item.Use(deltaTime, null);
projectileContainer?.Item.Use(deltaTime);
}
}
else
@@ -713,7 +728,7 @@ namespace Barotrauma.Items.Components
ItemContainer projectileContainer = containerItem.GetComponent<ItemContainer>();
if (projectileContainer != null)
{
containerItem.Use(deltaTime, null);
containerItem.Use(deltaTime);
projectiles = GetLoadedProjectiles();
if (projectiles.Any()) { return true; }
}
@@ -749,7 +764,7 @@ namespace Barotrauma.Items.Components
{
if (e is not Item linkedItem) { continue; }
if (!((MapEntity)item).Prefab.IsLinkAllowed(e.Prefab)) { continue; }
if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag("turretammosource"))
if (linkedItem.GetComponent<Repairable>() is Repairable repairable && repairable.IsTinkering && linkedItem.HasTag(Tags.TurretAmmoSource))
{
tinkeringStrength = repairable.TinkeringStrength;
}
@@ -757,28 +772,29 @@ namespace Barotrauma.Items.Components
if (!ignorePower)
{
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
float neededPower = GetPowerRequiredToShoot();
// tinkering is currently not factored into the common method as it is checked only when shooting
// but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later
neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction);
while (neededPower > 0.0001f && batteries.Count > 0)
var batteries = GetDirectlyConnectedBatteries().Where(static b => !b.OutputDisabled && b.Charge > 0.0001f && b.MaxOutPut > 0.0001f);
int batteryCount = batteries.Count();
if (batteryCount > 0)
{
batteries.RemoveAll(b => b.Charge <= 0.0001f || b.MaxOutPut <= 0.0001f);
if (!batteries.Any()) { break; }
float takePower = neededPower / batteries.Count;
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
foreach (PowerContainer battery in batteries)
float neededPower = GetPowerRequiredToShoot();
// tinkering is currently not factored into the common method as it is checked only when shooting
// but this is a minor issue that causes mostly cosmetic woes. might still be worth refactoring later
neededPower /= 1f + (tinkeringStrength * TinkeringPowerCostReduction);
while (neededPower > 0.0001f)
{
neededPower -= takePower;
battery.Charge -= takePower / 3600.0f;
float takePower = neededPower / batteryCount;
takePower = Math.Min(takePower, batteries.Min(b => Math.Min(b.Charge * 3600.0f, b.MaxOutPut)));
foreach (PowerContainer battery in batteries)
{
neededPower -= takePower;
battery.Charge -= takePower / 3600.0f;
#if SERVER
battery.Item.CreateServerEvent(battery);
battery.Item.CreateServerEvent(battery);
#endif
}
}
}
}
launchedProjectile = projectiles.FirstOrDefault();
@@ -891,9 +907,21 @@ namespace Barotrauma.Items.Components
}
float spread = MathHelper.ToRadians(Spread) * Rand.Range(-0.5f, 0.5f);
projectile.SetTransform(
ConvertUnits.ToSimUnits(GetRelativeFiringPosition()),
-(launchRotation ?? rotation) + spread);
Vector2 launchPos = ConvertUnits.ToSimUnits(GetRelativeFiringPosition());
//check if there's some other sub between the turret's origin and the launch pos,
//and if so, launch at the intersection of the turret and the sub to prevent the projectile from spawning inside the other sub
Body pickedBody = Submarine.PickBody(ConvertUnits.ToSimUnits(item.WorldPosition), launchPos, null, Physics.CollisionWall, allowInsideFixture: true,
customPredicate: (Fixture f) =>
{
return f.Body.UserData is not Submarine sub || sub != item.Submarine;
});
if (pickedBody != null)
{
launchPos = Submarine.LastPickedPosition;
}
projectile.SetTransform(launchPos, -(launchRotation ?? rotation) + spread);
projectile.UpdateTransform();
projectile.Submarine = projectile.body?.Submarine;
@@ -959,8 +987,15 @@ namespace Barotrauma.Items.Components
private float updateTimer;
private bool updatePending;
public void UpdateAutoOperate(float deltaTime, Identifier friendlyTag = default)
private float GetTargetPriorityModifier() => currentChargingState == ChargingState.WindingUp ? 10f : AICurrentTargetPriorityMultiplier;
public void UpdateAutoOperate(float deltaTime, bool ignorePower, Identifier friendlyTag = default)
{
if (!ignorePower && !HasPowerToShoot())
{
return;
}
IsActive = true;
if (friendlyTag.IsEmpty)
@@ -1006,8 +1041,12 @@ namespace Barotrauma.Items.Components
if (!IsValidTargetForAutoOperate(character, friendlyTag)) { continue; }
float dist = Vector2.DistanceSquared(character.WorldPosition, item.WorldPosition);
if (dist > closestDist) { continue; }
if (!CheckTurretAngle(character.WorldPosition)) { continue; }
if (!IsWithinAimingRadius(character.WorldPosition)) { continue; }
target = character;
if (currentTarget != null && target == currentTarget)
{
priority *= GetTargetPriorityModifier();
}
closestDist = dist / priority;
}
}
@@ -1022,8 +1061,12 @@ namespace Barotrauma.Items.Components
if (dist > closestDist) { continue; }
if (dist > shootDistance * shootDistance) { continue; }
if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
if (!IsWithinAimingRadius(targetItem.WorldPosition)) { continue; }
target = targetItem;
if (currentTarget != null && target == currentTarget)
{
priority *= GetTargetPriorityModifier();
}
closestDist = dist / priority;
}
}
@@ -1090,10 +1133,10 @@ namespace Barotrauma.Items.Components
}
}
if (target == null) { return; }
currentTarget = target;
float angle = -MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition);
targetRotation = MathUtils.WrapAngleTwoPi(angle);
if (Math.Abs(targetRotation - prevTargetRotation) > 0.1f) { updatePending = true; }
if (target is Hull targetHull)
@@ -1106,10 +1149,8 @@ namespace Barotrauma.Items.Components
}
else
{
if (!CheckTurretAngle(angle)) { return; }
float enemyAngle = MathUtils.VectorToAngle(target.WorldPosition - item.WorldPosition);
float turretAngle = -rotation;
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > 0.15f) { return; }
if (!IsWithinAimingRadius(angle)) { return; }
if (!IsPointingTowards(target.WorldPosition)) { return; }
}
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition);
@@ -1129,7 +1170,7 @@ namespace Barotrauma.Items.Components
}
if (shoot)
{
TryLaunch(deltaTime, ignorePower: true);
TryLaunch(deltaTime, ignorePower: ignorePower);
}
}
@@ -1149,12 +1190,12 @@ namespace Barotrauma.Items.Components
bool canShoot = HasPowerToShoot();
if (!canShoot)
{
List<PowerContainer> batteries = GetDirectlyConnectedBatteries();
float lowestCharge = 0.0f;
PowerContainer batteryToLoad = null;
foreach (PowerContainer battery in batteries)
foreach (PowerContainer battery in GetDirectlyConnectedBatteries())
{
if (!battery.Item.IsInteractable(character)) { continue; }
if (battery.OutputDisabled) { continue; }
if (batteryToLoad == null || battery.Charge < lowestCharge)
{
batteryToLoad = battery;
@@ -1328,7 +1369,11 @@ namespace Barotrauma.Items.Components
{
// Only check the angle to targets that are close enough to be shot at
// We shouldn't check the angle when a long creature is traveling outside of the shooting range, because doing so would not allow us to shoot the limbs that might be close enough to shoot at.
if (!CheckTurretAngle(enemy.WorldPosition)) { continue; }
if (!IsWithinAimingRadius(enemy.WorldPosition)) { continue; }
}
if (currentTarget != null && enemy == currentTarget)
{
priority *= GetTargetPriorityModifier();
}
targetPos = enemy.WorldPosition;
closestEnemy = enemy;
@@ -1344,7 +1389,11 @@ namespace Barotrauma.Items.Components
if (dist > closestDistance) { continue; }
if (dist > shootDistance * shootDistance) { continue; }
if (!IsTargetItemCloseEnough(targetItem, dist)) { continue; }
if (!CheckTurretAngle(targetItem.WorldPosition)) { continue; }
if (!IsWithinAimingRadius(targetItem.WorldPosition)) { continue; }
if (currentTarget != null && targetItem == currentTarget)
{
priority *= GetTargetPriorityModifier();
}
targetPos = targetItem.WorldPosition;
closestDistance = dist / priority;
// Override the target character so that we can target the item instead.
@@ -1378,7 +1427,7 @@ namespace Barotrauma.Items.Components
{
if (limb.IsSevered) { continue; }
if (limb.Hidden) { continue; }
if (!CheckTurretAngle(limb.WorldPosition)) { continue; }
if (!IsWithinAimingRadius(limb.WorldPosition)) { continue; }
float dist = Vector2.DistanceSquared(limb.WorldPosition, item.WorldPosition);
if (dist < closestDist)
{
@@ -1416,14 +1465,14 @@ namespace Barotrauma.Items.Components
Vector2 p1 = edge.Point1 + cell.Translation;
Vector2 p2 = edge.Point2 + cell.Translation;
Vector2 closestPoint = MathUtils.GetClosestPointOnLineSegment(p1, p2, item.WorldPosition);
if (!CheckTurretAngle(closestPoint))
if (!IsWithinAimingRadius(closestPoint))
{
// The closest point can't be targeted -> get a point directly in front of the turret
Vector2 barrelDir = new Vector2((float)Math.Cos(rotation), -(float)Math.Sin(rotation));
if (MathUtils.GetLineSegmentIntersection(p1, p2, item.WorldPosition, item.WorldPosition + barrelDir * shootDistance, out Vector2 intersection))
{
closestPoint = intersection;
if (!CheckTurretAngle(closestPoint)) { continue; }
if (!IsWithinAimingRadius(closestPoint)) { continue; }
}
else
{
@@ -1510,19 +1559,7 @@ namespace Barotrauma.Items.Components
character.CursorPosition -= character.Submarine.Position;
}
float enemyAngle = MathUtils.VectorToAngle(targetPos.Value - item.WorldPosition);
float turretAngle = -rotation;
float maxAngleError = 0.15f;
if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f)
{
//larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target)
maxAngleError *= 2.0f;
}
if (Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) > maxAngleError) { return false; }
if (canShoot)
if (IsPointingTowards(targetPos.Value))
{
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(targetPos.Value);
@@ -1551,6 +1588,19 @@ namespace Barotrauma.Items.Components
return false;
}
private bool IsPointingTowards(Vector2 targetPos)
{
float enemyAngle = MathUtils.VectorToAngle(targetPos - item.WorldPosition);
float turretAngle = -rotation;
float maxAngleError = MathHelper.ToRadians(MaxAngleOffset);
if (MaxChargeTime > 0.0f && currentChargingState == ChargingState.WindingUp && FiringRotationSpeedModifier > 0.0f)
{
//larger margin of error if the weapon needs to be charged (-> the bot can start charging when the turret is still rotating towards the target)
maxAngleError *= 2.0f;
}
return Math.Abs(MathUtils.GetShortestAngle(enemyAngle, turretAngle)) <= maxAngleError;
}
private bool IsTargetItemCloseEnough(Item target, float sqrDist) => float.IsPositiveInfinity(target.Prefab.AITurretTargetingMaxDistance) || sqrDist < MathUtils.Pow2(target.Prefab.AITurretTargetingMaxDistance);
/// <summary>
@@ -1669,6 +1719,7 @@ namespace Barotrauma.Items.Components
customPredicate: (Fixture f) =>
{
if (f.UserData is Item i && i.GetComponent<Turret>() != null) { return false; }
if (f.UserData is Hull) { return false; }
return !item.StaticFixtures.Contains(f);
});
return pickedBody;
@@ -1686,7 +1737,7 @@ namespace Barotrauma.Items.Components
return new Vector2(item.WorldRect.X + transformedBarrelPos.X + transformedFiringOffset.X, item.WorldRect.Y - transformedBarrelPos.Y + transformedFiringOffset.Y);
}
private bool CheckTurretAngle(float angle)
private bool IsWithinAimingRadius(float angle)
{
float midRotation = (minRotation + maxRotation) / 2.0f;
while (midRotation - angle < -MathHelper.Pi) { angle -= MathHelper.TwoPi; }
@@ -1694,7 +1745,7 @@ namespace Barotrauma.Items.Components
return angle >= minRotation && angle <= maxRotation;
}
public bool CheckTurretAngle(Vector2 target) => CheckTurretAngle(-MathUtils.VectorToAngle(target - item.WorldPosition));
public bool IsWithinAimingRadius(Vector2 target) => IsWithinAimingRadius(-MathUtils.VectorToAngle(target - item.WorldPosition));
protected override void RemoveComponentSpecific()
{
@@ -1835,7 +1886,7 @@ namespace Barotrauma.Items.Components
break;
case "trigger_in":
if (signal.value == "0") { return; }
item.Use((float)Timing.Step, sender);
item.Use((float)Timing.Step, user: sender);
user = sender;
ActiveUser = sender;
resetActiveUserTimer = 1f;
@@ -7,6 +7,7 @@ using System.Xml.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Barotrauma.Abilities;
using System.Collections.Immutable;
namespace Barotrauma
{
@@ -55,6 +56,11 @@ namespace Barotrauma
public bool HideOtherWearables => ObscureOtherWearables == ObscuringMode.Hide;
public bool AlphaClipOtherWearables => ObscureOtherWearables == ObscuringMode.AlphaClip;
public bool CanBeHiddenByOtherWearables { get; private set; }
/// <summary>
/// Tags or identifiers of items that can hide this one.
/// </summary>
public ImmutableHashSet<Identifier> CanBeHiddenByItem { get; private set; }
public List<WearableType> HideWearablesOfType { get; private set; }
public bool InheritLimbDepth { get; private set; }
/// <summary>
@@ -139,11 +145,6 @@ namespace Barotrauma
case WearableType.Husk:
case WearableType.Herpes:
Limb = LimbType.Head;
ObscureOtherWearables = ObscuringMode.None;
InheritLimbDepth = true;
InheritScale = true;
InheritOrigin = true;
InheritSourceRect = true;
break;
}
}
@@ -177,6 +178,10 @@ namespace Barotrauma
public void ParsePath(bool parseSpritePath)
{
#if SERVER
// Server doesn't care about texture paths at all
return;
#endif
SpritePath = UnassignedSpritePath.Value;
if (_picker?.Info != null)
{
@@ -196,7 +201,7 @@ namespace Barotrauma
}
if (parseSpritePath)
{
Sprite.ParseTexturePath(file: SpritePath);
Sprite?.ParseTexturePath(file: SpritePath);
}
}
@@ -222,22 +227,23 @@ namespace Barotrauma
}
CanBeHiddenByOtherWearables = SourceElement.GetAttributeBool("canbehiddenbyotherwearables", true);
CanBeHiddenByItem = SourceElement.GetAttributeIdentifierArray(nameof(CanBeHiddenByItem), Array.Empty<Identifier>()).ToImmutableHashSet();
InheritLimbDepth = SourceElement.GetAttributeBool("inheritlimbdepth", true);
var scale = SourceElement.GetAttribute("inheritscale");
if (scale != null)
{
InheritScale = scale.GetAttributeBool(false);
InheritScale = scale.GetAttributeBool(Type != WearableType.Item);
}
else
{
InheritScale = SourceElement.GetAttributeBool("inherittexturescale", false);
InheritScale = SourceElement.GetAttributeBool("inherittexturescale", Type != WearableType.Item);
}
IgnoreLimbScale = SourceElement.GetAttributeBool("ignorelimbscale", false);
IgnoreTextureScale = SourceElement.GetAttributeBool("ignoretexturescale", false);
IgnoreRagdollScale = SourceElement.GetAttributeBool("ignoreragdollscale", false);
SourceElement.GetAttributeBool("inherittexturescale", false);
InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", false);
InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", false);
InheritOrigin = SourceElement.GetAttributeBool("inheritorigin", Type != WearableType.Item);
InheritSourceRect = SourceElement.GetAttributeBool("inheritsourcerect", Type != WearableType.Item);
DepthLimb = (LimbType)Enum.Parse(typeof(LimbType), SourceElement.GetAttributeString("depthlimb", "None"), true);
Sound = SourceElement.GetAttributeString("sound", "");
Scale = SourceElement.GetAttributeFloat("scale", 1.0f);
@@ -457,22 +463,20 @@ namespace Barotrauma.Items.Components
if (!equipLimb.WearingItems.Contains(wearableSprite))
{
equipLimb.WearingItems.Add(wearableSprite);
equipLimb.WearingItems.Sort((i1, i2) => { return i2.Sprite.Depth.CompareTo(i1.Sprite.Depth); });
equipLimb.WearingItems.Sort((i1, i2) =>
equipLimb.WearingItems.Sort((wearable, nextWearable) =>
{
if (i1?.WearableComponent == null && i2?.WearableComponent == null)
{
return 0;
}
else if (i1?.WearableComponent == null)
{
return -1;
}
else if (i2?.WearableComponent == null)
{
return 1;
}
return i1.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(i2.WearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes));
float depth = wearable?.Sprite?.Depth ?? 0;
float nextDepth = nextWearable?.Sprite?.Depth ?? 0;
return nextDepth.CompareTo(depth);
});
equipLimb.WearingItems.Sort((wearable, nextWearable) =>
{
var wearableComponent = wearable?.WearableComponent;
var nextWearableComponent = nextWearable?.WearableComponent;
if (wearableComponent == null && nextWearableComponent == null) { return 0; }
if (wearableComponent == null) { return -1; }
if (nextWearableComponent == null) { return 1; }
return wearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes).CompareTo(nextWearableComponent.AllowedSlots.Contains(InvSlotType.OuterClothes));
});
}
#if CLIENT
@@ -553,10 +557,7 @@ namespace Barotrauma.Items.Components
foreach (WearableSprite wearableSprite in wearableSprites)
{
if (wearableSprite != null && wearableSprite.Sprite != null)
{
wearableSprite.Sprite.Remove();
}
wearableSprite?.Sprite?.Remove();
}
}
@@ -8,18 +8,27 @@ using System.Linq;
namespace Barotrauma
{
partial class Inventory : IServerSerializable, IClientSerializable
partial class Inventory
{
public const int MaxStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63
public const int MaxPossibleStackSize = (1 << 6) - 1; //the max value that will fit in 6 bits, i.e 63
public const int MaxItemsPerNetworkEvent = 128;
public class ItemSlot
{
private readonly List<Item> items = new List<Item>(MaxStackSize);
private readonly List<Item> items = new List<Item>(MaxPossibleStackSize);
public bool HideIfEmpty;
public IReadOnlyList<Item> Items => items;
private readonly Inventory inventory;
public ItemSlot(Inventory inventory)
{
this.inventory = inventory;
}
public bool CanBePut(Item item, bool ignoreCondition = false)
{
if (item == null) { return false; }
@@ -41,7 +50,7 @@ namespace Barotrauma
}
}
if (items[0].Quality != item.Quality) { return false; }
if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.MaxStackSize)
if (items[0].Prefab.Identifier != item.Prefab.Identifier || items.Count + 1 > item.Prefab.GetMaxStackSize(inventory))
{
return false;
}
@@ -80,7 +89,7 @@ namespace Barotrauma
}
if (items[0].Prefab.Identifier != itemPrefab.Identifier ||
items.Count + 1 > itemPrefab.MaxStackSize)
items.Count + 1 > itemPrefab.GetMaxStackSize(inventory))
{
return false;
}
@@ -92,7 +101,7 @@ namespace Barotrauma
public int HowManyCanBePut(ItemPrefab itemPrefab, int? maxStackSize = null, float? condition = null)
{
if (itemPrefab == null) { return 0; }
maxStackSize ??= itemPrefab.MaxStackSize;
maxStackSize ??= itemPrefab.GetMaxStackSize(inventory);
if (items.Count > 0)
{
if (condition.HasValue)
@@ -135,9 +144,9 @@ namespace Barotrauma
{
throw new InvalidOperationException("Tried to stack different types of items.");
}
else if (items.Count + 1 > item.Prefab.MaxStackSize)
else if (items.Count + 1 > item.Prefab.GetMaxStackSize(inventory))
{
throw new InvalidOperationException("Tried to add an item to a full inventory slot (stack already full).");
throw new InvalidOperationException($"Tried to add an item to a full inventory slot (stack already full, x{items.Count} {items.First().Prefab.Identifier}).");
}
}
if (items.Contains(item)) { return; }
@@ -229,34 +238,11 @@ namespace Barotrauma
/// <summary>
/// All items contained in the inventory. Stacked items are returned as individual instances. DO NOT modify the contents of the inventory while enumerating this list.
/// </summary>
public IEnumerable<Item> AllItems
public virtual IEnumerable<Item> AllItems
{
get
{
for (int i = 0; i < capacity; i++)
{
foreach (var item in slots[i].Items)
{
if (item == null)
{
#if DEBUG
DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!");
#endif
continue;
}
bool duplicateFound = false;
for (int j = 0; j < i; j++)
{
if (slots[j].Items.Contains(item))
{
duplicateFound = true;
break;
}
}
if (!duplicateFound) { yield return item; }
}
}
return GetAllItems(checkForDuplicates: this is CharacterInventory);
}
}
@@ -292,7 +278,7 @@ namespace Barotrauma
slots = new ItemSlot[capacity];
for (int i = 0; i < capacity; i++)
{
slots[i] = new ItemSlot();
slots[i] = new ItemSlot(this);
}
#if CLIENT
@@ -315,6 +301,60 @@ namespace Barotrauma
#endif
}
protected IEnumerable<Item> GetAllItems(bool checkForDuplicates)
{
for (int i = 0; i < capacity; i++)
{
foreach (var item in slots[i].Items)
{
if (item == null)
{
#if DEBUG
DebugConsole.ThrowError($"Null item in inventory {Owner.ToString() ?? "null"}, slot {i}!");
#endif
continue;
}
if (checkForDuplicates)
{
bool duplicateFound = false;
for (int j = 0; j < i; j++)
{
if (slots[j].Items.Contains(item))
{
duplicateFound = true;
break;
}
}
if (!duplicateFound) { yield return item; }
}
else
{
yield return item;
}
}
}
}
private void NotifyItemComponentsOfChange()
{
#if CLIENT
if (Owner is Character character && character == Character.Controlled)
{
character.SelectedItem?.GetComponent<CircuitBox>()?.OnViewUpdateProjSpecific();
}
#endif
if (Owner is not Item it) { return; }
foreach (var c in it.Components)
{
c.OnInventoryChanged();
}
it.ParentInventory?.NotifyItemComponentsOfChange();
}
/// <summary>
/// Is the item contained in this inventory. Does not recursively check items inside items.
/// </summary>
@@ -367,6 +407,13 @@ namespace Barotrauma
return slots[index].Items;
}
public int GetItemStackSlotIndex(Item item, int index)
{
if (index < 0 || index >= slots.Length) { return -1; }
return slots[index].Items.IndexOf(item);
}
/// <summary>
/// Find the index of the first slot the item is contained in.
/// </summary>
@@ -534,7 +581,7 @@ namespace Barotrauma
var itemInSlot = slots[i].First();
if (itemInSlot.OwnInventory != null &&
!itemInSlot.OwnInventory.Contains(item) &&
(itemInSlot.GetComponent<ItemContainer>()?.GetMaxStackSize(0) ?? 0) == 1 &&
itemInSlot.GetComponent<ItemContainer>()?.GetMaxStackSize(0) == 1 &&
itemInSlot.OwnInventory.TrySwapping(0, item, user, createNetworkEvent, swapWholeStack: false))
{
return true;
@@ -575,7 +622,7 @@ namespace Barotrauma
if (removeItem)
{
item.Drop(user, setTransform: false);
item.Drop(user, setTransform: false, createNetworkEvent: createNetworkEvent && GameMain.NetworkMember is { IsServer: true });
item.ParentInventory?.RemoveItem(item);
}
@@ -597,7 +644,7 @@ namespace Barotrauma
item.body.BodyType = FarseerPhysics.BodyType.Dynamic;
item.SetTransform(item.SimPosition, rotation: 0.0f, findNewHull: false);
//update to refresh the interpolated draw rotation and position (update doesn't run on disabled bodies)
item.body.Update();
item.body.UpdateDrawPosition(interpolate: false);
}
#if SERVER
@@ -624,6 +671,8 @@ namespace Barotrauma
}
}
}
NotifyItemComponentsOfChange();
}
public bool IsEmpty()
@@ -648,7 +697,7 @@ namespace Barotrauma
{
if (!slots[i].Any()) { return false; }
var item = slots[i].FirstOrDefault();
if (slots[i].Items.Count < item.Prefab.MaxStackSize) { return false; }
if (slots[i].Items.Count < item.Prefab.GetMaxStackSize(this)) { return false; }
}
}
else
@@ -878,21 +927,35 @@ namespace Barotrauma
}
}
public virtual void CreateNetworkEvent()
public void CreateNetworkEvent()
{
if (GameMain.NetworkMember == null) { return; }
if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; }
if (Owner is Character character)
//split into multiple events because one might not be enough to fit all the items
List<Range> slotRanges = new List<Range>();
int startIndex = 0;
int itemCount = 0;
for (int i = 0; i < capacity; i++)
{
GameMain.NetworkMember.CreateEntityEvent(character, new Character.InventoryStateEventData());
int count = slots[i].Items.Count;
if (itemCount + count > MaxItemsPerNetworkEvent || i == capacity - 1)
{
slotRanges.Add(new Range(startIndex, i + 1));
startIndex = i + 1;
itemCount = 0;
}
itemCount += count;
}
else if (Owner is Item item)
foreach (var slotRange in slotRanges)
{
GameMain.NetworkMember.CreateEntityEvent(item, new Item.InventoryStateEventData());
CreateNetworkEvent(slotRange);
}
}
protected virtual void CreateNetworkEvent(Range slotRange) { }
public Item FindItem(Func<Item, bool> predicate, bool recursive)
{
Item match = AllItems.FirstOrDefault(predicate);
@@ -955,9 +1018,12 @@ namespace Barotrauma
visualSlots[n].ShowBorderHighlight(Color.White, 0.1f, 0.4f);
if (selectedSlot?.Inventory == this) { selectedSlot.ForceTooltipRefresh = true; }
}
syncItemsDelay = 1.0f;
#endif
CharacterHUD.RecreateHudTextsIfFocused(item);
}
NotifyItemComponentsOfChange();
}
/// <summary>
@@ -989,28 +1055,48 @@ namespace Barotrauma
return slots[index].Contains(item);
}
public void SharedRead(IReadMessage msg, out List<ushort>[] newItemIds)
public void SharedRead(IReadMessage msg, List<ushort>[] receivedItemIds, out bool readyToApply)
{
byte slotCount = msg.ReadByte();
newItemIds = new List<ushort>[slotCount];
for (int i = 0; i < slotCount; i++)
byte start = msg.ReadByte();
byte end = msg.ReadByte();
//if we received the first chunk of item IDs, clear the rest
//to ensure we don't have anything outdated in the list - we're about to receive the rest of the IDs next
if (start == 0)
{
newItemIds[i] = new List<ushort>();
int itemCount = msg.ReadRangedInteger(0, MaxStackSize);
for (int j = 0; j < itemCount; j++)
for (int i = 0; i < capacity; i++)
{
newItemIds[i].Add(msg.ReadUInt16());
receivedItemIds[i] = null;
}
}
for (int i = start; i < end; i++)
{
var newItemIds = new List<ushort>();
int itemCount = msg.ReadRangedInteger(0, MaxPossibleStackSize);
for (int j = 0; j < itemCount; j++)
{
newItemIds.Add(msg.ReadUInt16());
}
receivedItemIds[i] = newItemIds;
}
//if all IDs haven't been received yet (chunked into multiple events?)
//don't apply the state yet
readyToApply = !receivedItemIds.Contains(null);
}
public void SharedWrite(IWriteMessage msg, NetEntityEvent.IData extraData = null)
public void SharedWrite(IWriteMessage msg, Range slotRange)
{
msg.WriteByte((byte)capacity);
for (int i = 0; i < capacity; i++)
int start = slotRange.Start.Value;
int end = slotRange.End.Value;
msg.WriteByte((byte)start);
msg.WriteByte((byte)end);
for (int i = start; i < end; i++)
{
msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxStackSize);
for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxStackSize); j++)
msg.WriteRangedInteger(slots[i].Items.Count, 0, MaxPossibleStackSize);
for (int j = 0; j < Math.Min(slots[i].Items.Count, MaxPossibleStackSize); j++)
{
var item = slots[i].Items[j];
msg.WriteUInt16(item?.ID ?? (ushort)0);
@@ -23,7 +23,7 @@ namespace Barotrauma
{
partial class Item : MapEntity, IDamageable, IIgnorable, ISerializableEntity, IServerPositionSync, IClientSerializable
{
public static List<Item> ItemList = new List<Item>();
public static readonly List<Item> ItemList = new List<Item>();
private static readonly HashSet<Item> dangerousItems = new HashSet<Item>();
@@ -77,17 +77,17 @@ namespace Barotrauma
public CampaignMode.InteractionType CampaignInteractionType
{
get { return campaignInteractionType; }
set
{
if (campaignInteractionType != value)
{
campaignInteractionType = value;
AssignCampaignInteractionTypeProjSpecific(campaignInteractionType);
}
}
}
partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType);
public void AssignCampaignInteractionType(CampaignMode.InteractionType interactionType, IEnumerable<Client> targetClients = null)
{
if (campaignInteractionType == interactionType) { return; }
campaignInteractionType = interactionType;
AssignCampaignInteractionTypeProjSpecific(campaignInteractionType, targetClients);
}
partial void AssignCampaignInteractionTypeProjSpecific(CampaignMode.InteractionType interactionType, IEnumerable<Client> targetClients);
public bool Visible = true;
@@ -105,6 +105,12 @@ namespace Barotrauma
private readonly List<IDrawableComponent> drawableComponents;
private bool hasComponentsToDraw;
/// <summary>
/// Has everything in the item been loaded/instantiated/initialized (basically, can be used to check if the whole constructor/Load method has run).
/// Most commonly used to avoid creating network events when some value changes if the item is being initialized.
/// </summary>
public bool FullyInitialized { get; private set; }
public PhysicsBody body;
private readonly float originalWaterDragCoefficient;
private float? overrideWaterDragCoefficient;
@@ -114,6 +120,21 @@ namespace Barotrauma
set => overrideWaterDragCoefficient = value;
}
/// <summary>
/// Can be used by StatusEffects to set the type of the body (if the item has one)
/// </summary>
public BodyType BodyType
{
get { return body?.BodyType ?? BodyType.Dynamic; }
set
{
if (body != null)
{
body.BodyType = value;
}
}
}
/// <summary>
/// Removes the override value -> falls back to using the original value defined in the xml.
/// </summary>
@@ -125,9 +146,10 @@ namespace Barotrauma
private bool transformDirty = true;
private static readonly List<Item> itemsWithPendingConditionUpdates = new List<Item>();
private float lastSentCondition;
private float sendConditionUpdateTimer;
private bool conditionUpdatePending;
private float prevCondition;
private float condition;
@@ -207,7 +229,11 @@ namespace Barotrauma
set
{
parentInventory = value;
if (parentInventory != null) { Container = parentInventory.Owner as Item; }
if (parentInventory != null)
{
Container = parentInventory.Owner as Item;
RemoveFromDroppedStack(allowClientExecute: false);
}
#if SERVER
PreviousParentInventory = value;
#endif
@@ -229,7 +255,6 @@ namespace Barotrauma
container = value;
CheckCleanable();
SetActiveSprite();
RefreshRootContainer();
}
}
@@ -247,6 +272,37 @@ namespace Barotrauma
set { description = value; }
}
private string descriptionTag;
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true), ConditionallyEditable(ConditionallyEditable.ConditionType.OnlyByStatusEffectsAndNetwork)]
/// <summary>
/// Can be used to set a localized description via StatusEffects
/// </summary>
public string DescriptionTag
{
get { return descriptionTag; }
set
{
if (value == descriptionTag) { return; }
if (value.IsNullOrEmpty())
{
descriptionTag = null;
description = null;
}
else
{
description = TextManager.Get(value).Value;
descriptionTag = value;
}
if (FullyInitialized &&
SerializableProperties != null &&
SerializableProperties.TryGetValue(nameof(DescriptionTag).ToIdentifier(), out SerializableProperty property))
{
GameMain.NetworkMember?.CreateEntityEvent(this, new ChangePropertyEventData(property, this));
}
}
}
[Editable, Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public bool NonInteractable
{
@@ -675,6 +731,8 @@ namespace Barotrauma
set { allowStealing = value; }
}
public bool IsSalvageMissionItem;
private string originalOutpost;
[Serialize("", IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
public string OriginalOutpost
@@ -875,8 +933,8 @@ namespace Barotrauma
{
get { return allPropertyObjects; }
}
public bool IgnoreByAI(Character character) => HasTag("ignorebyai") || OrderedToBeIgnored && character.IsOnPlayerTeam;
public bool IgnoreByAI(Character character) => HasTag(Barotrauma.Tags.ItemIgnoredByAI) || OrderedToBeIgnored && character.IsOnPlayerTeam;
public bool OrderedToBeIgnored { get; set; }
public bool HasBallastFloraInHull
@@ -896,6 +954,9 @@ namespace Barotrauma
}
}
public bool InPlayerSubmarine => Submarine?.Info is { IsPlayer: true };
public bool InBeaconStation => Submarine?.Info is { Type: SubmarineType.BeaconStation };
public bool IsLadder { get; }
public bool IsSecondaryItem { get; }
@@ -910,6 +971,11 @@ namespace Barotrauma
}
}
/// <summary>
/// Timing.TotalTimeUnpaused when some character was last eating the item
/// </summary>
public float LastEatenTime { get; set; }
public Action<Character> OnDeselect;
public Item(ItemPrefab itemPrefab, Vector2 position, Submarine submarine, ushort id = Entity.NullEntityID, bool callOnItemLoaded = true)
@@ -1157,10 +1223,15 @@ namespace Barotrauma
DebugConsole.Log("Created " + Name + " (" + ID + ")");
if (Components.Any(ic => ic is Wire) && Components.All(ic => ic is Wire || ic is Holdable)) { isWire = true; }
if (HasTag("logic")) { isLogic = true; }
if (HasTag(Barotrauma.Tags.LogicItem)) { isLogic = true; }
ApplyStatusEffects(ActionType.OnSpawn, 1.0f);
RecalculateConditionValues();
if (callOnItemLoaded)
{
FullyInitialized = true;
}
#if CLIENT
Submarine.ForceVisibilityRecheck();
#endif
@@ -1179,7 +1250,7 @@ namespace Barotrauma
};
foreach (KeyValuePair<Identifier, SerializableProperty> property in SerializableProperties)
{
if (!property.Value.Attributes.OfType<Editable>().Any()) continue;
if (property.Value.Attributes.OfType<Serialize>().None()) { continue; }
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
}
@@ -1194,9 +1265,10 @@ namespace Barotrauma
for (int i = 0; i < components.Count && i < clone.components.Count; i++)
{
foreach (KeyValuePair<Identifier, SerializableProperty> property in components[i].SerializableProperties)
//order the properties to get them to be applied in a consistent order (may matter for properties that are interconnected somehow, like IsOn/IsActive)
foreach (KeyValuePair<Identifier, SerializableProperty> property in components[i].SerializableProperties.OrderBy(s => s.Key))
{
if (!property.Value.Attributes.OfType<Editable>().Any()) continue;
if (property.Value.Attributes.OfType<Serialize>().None()) { continue; }
clone.components[i].SerializableProperties[property.Key].TrySetValue(clone.components[i], property.Value.GetValue(components[i]));
}
@@ -1219,18 +1291,48 @@ namespace Barotrauma
if (FlippedX) clone.FlipX(false);
if (FlippedY) clone.FlipY(false);
foreach (ItemComponent component in clone.components)
{
component.OnItemLoaded();
}
foreach (Item containedItem in ContainedItems)
Dictionary<ushort, Item> clonedContainedItems = new();
for (int i = 0; i < components.Count && i < clone.components.Count; i++)
{
var containedClone = containedItem.Clone();
clone.ownInventory.TryPutItem(containedClone as Item, null);
ItemComponent component = components[i],
cloneComp = clone.components[i];
if (component is not ItemContainer origInv ||
cloneComp is not ItemContainer cloneInv)
{
continue;
}
foreach (var containedItem in origInv.Inventory.AllItems)
{
var containedClone = (Item)containedItem.Clone();
cloneInv.Inventory.TryPutItem(containedClone, null);
clonedContainedItems.Add(containedItem.ID, containedClone);
}
}
for (int i = 0; i < components.Count && i < clone.components.Count; i++)
{
ItemComponent component = components[i],
cloneComp = clone.components[i];
if (component is not CircuitBox origBox || cloneComp is not CircuitBox cloneBox)
{
continue;
}
cloneBox.CloneFrom(origBox, clonedContainedItems);
}
clone.FullyInitialized = true;
return clone;
}
@@ -1432,7 +1534,7 @@ namespace Barotrauma
var pickable = GetComponent<Pickable>();
if (pickable != null && !pickable.IsAttached &&
Prefab.PreferredContainers.Any() &&
(container == null || container.HasTag("allowcleanup")))
(container == null || container.HasTag(Barotrauma.Tags.AllowCleanup)))
{
if (!cleanableItems.Contains(this))
{
@@ -1638,12 +1740,6 @@ namespace Barotrauma
tags.Add(tag);
}
public bool HasTag(string tag)
{
return HasTag(tag.ToIdentifier());
}
public bool HasTag(Identifier tag)
{
if (tag == null) { return true; }
@@ -1690,7 +1786,7 @@ namespace Barotrauma
public bool ConditionalMatches(PropertyConditional conditional)
{
if (string.IsNullOrEmpty(conditional.TargetItemComponentName))
if (string.IsNullOrEmpty(conditional.TargetItemComponent))
{
if (!conditional.Matches(this)) { return false; }
}
@@ -1698,7 +1794,7 @@ namespace Barotrauma
{
foreach (ItemComponent component in components)
{
if (component.Name != conditional.TargetItemComponentName) { continue; }
if (component.Name != conditional.TargetItemComponent) { continue; }
if (!conditional.Matches(component)) { return false; }
}
}
@@ -1796,7 +1892,7 @@ namespace Barotrauma
}
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
{
targets.AddRange(character.AnimController.Limbs.ToList());
targets.AddRange(character.AnimController.Limbs);
}
if (effect.HasTargetType(StatusEffect.TargetType.Limb) && limb == null && effect.targetLimbs != null)
{
@@ -1811,7 +1907,7 @@ namespace Barotrauma
targets.Add(limb);
}
if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.Add(Container); }
if (Container != null && effect.HasTargetType(StatusEffect.TargetType.Parent)) { targets.AddRange(Container.AllPropertyObjects); }
effect.Apply(type, deltaTime, this, targets, worldPosition);
}
@@ -1897,21 +1993,20 @@ namespace Barotrauma
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
{
if (Math.Abs(lastSentCondition - condition) > 1.0f)
{
conditionUpdatePending = true;
isActive = true;
}
else if (wasInFullCondition != IsFullCondition)
{
conditionUpdatePending = true;
isActive = true;
}
else if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= MaxCondition))
bool needsConditionUpdate = false;
if (!MathUtils.NearlyEqual(lastSentCondition, condition) && (condition <= 0.0f || condition >= MaxCondition))
{
//send the update immediately if the condition changed to max or min
sendConditionUpdateTimer = 0.0f;
conditionUpdatePending = true;
isActive = true;
needsConditionUpdate = true;
}
else if (Math.Abs(lastSentCondition - condition) > 1.0f || wasInFullCondition != IsFullCondition)
{
needsConditionUpdate = true;
}
if (needsConditionUpdate && !itemsWithPendingConditionUpdates.Contains(this))
{
itemsWithPendingConditionUpdates.Add(this);
}
}
@@ -1967,12 +2062,16 @@ namespace Barotrauma
public void SendPendingNetworkUpdates()
{
if (!(GameMain.NetworkMember is { IsServer: true })) { return; }
if (!conditionUpdatePending) { return; }
if (!itemsWithPendingConditionUpdates.Contains(this)) { return; }
SendPendingNetworkUpdatesInternal();
itemsWithPendingConditionUpdates.Remove(this);
}
private void SendPendingNetworkUpdatesInternal()
{
CreateStatusEvent(loadingRound: false);
lastSentCondition = condition;
sendConditionUpdateTimer = NetConfig.ItemConditionUpdateInterval;
conditionUpdatePending = false;
}
public void CreateStatusEvent(bool loadingRound)
@@ -1980,21 +2079,32 @@ namespace Barotrauma
GameMain.NetworkMember.CreateEntityEvent(this, new ItemStatusEventData(loadingRound));
}
public static void UpdatePendingConditionUpdates(float deltaTime)
{
if (GameMain.NetworkMember is not { IsServer: true }) { return; }
for (int i = 0; i < itemsWithPendingConditionUpdates.Count; i++)
{
var item = itemsWithPendingConditionUpdates[i];
if (item == null || item.Removed)
{
itemsWithPendingConditionUpdates.RemoveAt(i--);
continue;
}
if (item.Submarine is { Loading: true }) { continue; }
item.sendConditionUpdateTimer -= deltaTime;
if (item.sendConditionUpdateTimer <= 0.0f)
{
item.SendPendingNetworkUpdatesInternal();
itemsWithPendingConditionUpdates.RemoveAt(i--);
}
}
}
private bool isActive = true;
public override void Update(float deltaTime, Camera cam)
{
#if SERVER
if (!(Submarine is { Loading: true }))
{
sendConditionUpdateTimer -= deltaTime;
if (conditionUpdatePending && sendConditionUpdateTimer <= 0.0f)
{
SendPendingNetworkUpdates();
}
}
#endif
if (!isActive) { return; }
if (impactQueue != null)
@@ -2004,14 +2114,27 @@ namespace Barotrauma
HandleCollision(impact);
}
}
if (isDroppedStackOwner && body != null)
{
foreach (var item in droppedStack)
{
if (item != this)
{
item.body.Enabled = false;
item.body.SetTransformIgnoreContacts(this.SimPosition, body.Rotation);
}
}
}
if (aiTarget != null && aiTarget.NeedsUpdate)
{
aiTarget.Update(deltaTime);
}
var containedEffectType = parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained;
ApplyStatusEffects(ActionType.Always, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
ApplyStatusEffects(parentInventory == null ? ActionType.OnNotContained : ActionType.OnContained, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
ApplyStatusEffects(containedEffectType, deltaTime, character: (parentInventory as CharacterInventory)?.Owner as Character);
for (int i = 0; i < updateableComponents.Count; i++)
{
@@ -2019,7 +2142,7 @@ namespace Barotrauma
if (ic.IsActiveConditionals != null)
{
if (ic.IsActiveConditionalComparison == PropertyConditional.Comparison.And)
if (ic.IsActiveConditionalComparison == PropertyConditional.LogicalOperatorType.And)
{
bool shouldBeActive = true;
foreach (var conditional in ic.IsActiveConditionals)
@@ -2140,6 +2263,7 @@ namespace Barotrauma
updateableComponents.Count == 0 &&
(aiTarget == null || !aiTarget.NeedsUpdate) &&
!hasStatusEffectsOfType[(int)ActionType.Always] &&
!hasStatusEffectsOfType[(int)containedEffectType] &&
(body == null || !body.Enabled))
{
#if CLIENT
@@ -2651,9 +2775,20 @@ namespace Barotrauma
public bool TryInteract(Character user, bool ignoreRequiredItems = false, bool forceSelectKey = false, bool forceUseKey = false)
{
if (CampaignMode.BlocksInteraction(CampaignInteractionType))
var campaignInteractionType = CampaignInteractionType;
#if SERVER
var ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == user);
if (ownerClient != null)
{
return false;
if (!campaignInteractionTypePerClient.TryGetValue(ownerClient, out campaignInteractionType))
{
campaignInteractionType = CampaignMode.InteractionType.None;
}
}
#endif
if (CampaignMode.BlocksInteraction(campaignInteractionType))
{
return false;
}
bool picked = false, selected = false;
@@ -2821,9 +2956,9 @@ namespace Barotrauma
return -1;
}
public void Use(float deltaTime, Character character = null, Limb targetLimb = null)
public void Use(float deltaTime, Character user = null, Limb targetLimb = null, Entity useTarget = null)
{
if (RequireAimToUse && (character == null || !character.IsKeyDown(InputType.Aim)))
if (RequireAimToUse && (user == null || !user.IsKeyDown(InputType.Aim)))
{
return;
}
@@ -2836,17 +2971,17 @@ namespace Barotrauma
{
bool isControlled = false;
#if CLIENT
isControlled = character == Character.Controlled;
isControlled = user == Character.Controlled;
#endif
if (!ic.HasRequiredContainedItems(character, isControlled)) { continue; }
if (ic.Use(deltaTime, character))
if (!ic.HasRequiredContainedItems(user, isControlled)) { continue; }
if (ic.Use(deltaTime, user))
{
ic.WasUsed = true;
#if CLIENT
ic.PlaySound(ActionType.OnUse, character);
ic.PlaySound(ActionType.OnUse, user);
#endif
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, character, targetLimb, useTarget: character, user: character);
ic.ApplyStatusEffects(ActionType.OnUse, deltaTime, user, targetLimb, useTarget: useTarget, user: user);
ic.OnUsed.Invoke(new ItemComponent.ItemUseInfo(this, user));
if (ic.DeleteOnUse) { remove = true; }
}
}
@@ -2876,7 +3011,7 @@ namespace Barotrauma
#if CLIENT
ic.PlaySound(ActionType.OnSecondaryUse, character);
#endif
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character);
ic.ApplyStatusEffects(ActionType.OnSecondaryUse, deltaTime, character: character, user: character, useTarget: character);
if (ic.DeleteOnUse) { remove = true; }
}
@@ -3009,10 +3144,10 @@ namespace Barotrauma
Container = null;
}
if (parentInventory != null)
if (ParentInventory != null)
{
parentInventory.RemoveItem(this);
parentInventory = null;
ParentInventory.RemoveItem(this);
ParentInventory = null;
}
SetContainedItemPositions();
@@ -3021,6 +3156,108 @@ namespace Barotrauma
#endif
}
private List<Item> droppedStack;
public IEnumerable<Item> DroppedStack => droppedStack ?? Enumerable.Empty<Item>();
private bool isDroppedStackOwner;
/// <summary>
/// "Merges" the set of items so they behave as one physical object and can be picked up by clicking once.
/// The items need to be instances of the same prefab and have a physics body.
/// </summary>
public void CreateDroppedStack(IEnumerable<Item> items, bool allowClientExecute)
{
if (!allowClientExecute && GameMain.NetworkMember is { IsClient: true }) { return; }
int itemCount = items.Count();
if (itemCount == 1) { return; }
if (items.DistinctBy(it => it.Prefab).Count() > 1)
{
DebugConsole.ThrowError($"Attempted to create a dropped stack of multiple different items ({string.Join(", ", items.DistinctBy(it => it.Prefab))})\n{Environment.StackTrace}");
return;
}
if (items.Any(it => it.body == null))
{
DebugConsole.ThrowError($"Attempted to create a dropped stack for an item with no body ({items.First().Prefab.Identifier})\n{Environment.StackTrace}");
return;
}
if (items.None())
{
DebugConsole.ThrowError($"Attempted to create a dropped stack of an empty list of items.\n{Environment.StackTrace}");
return;
}
int maxStackSize = items.First().Prefab.MaxStackSize;
if (itemCount > maxStackSize)
{
for (int i = 0; i < MathF.Ceiling(itemCount / maxStackSize); i++)
{
int startIndex = i * maxStackSize;
items.ElementAt(startIndex).CreateDroppedStack(items.Skip(startIndex).Take(maxStackSize), allowClientExecute);
}
}
else
{
droppedStack ??= new List<Item>();
foreach (Item item in items)
{
if (!droppedStack.Contains(item))
{
droppedStack.Add(item);
}
}
SetDroppedStackItemStates();
#if SERVER
if (GameMain.NetworkMember is { IsServer: true } server)
{
server.CreateEntityEvent(this, new DroppedStackEventData(droppedStack));
}
#endif
}
}
private void RemoveFromDroppedStack(bool allowClientExecute)
{
if (!allowClientExecute && GameMain.NetworkMember is { IsClient: true }) { return; }
if (droppedStack == null) { return; }
body.Enabled = ParentInventory == null;
isDroppedStackOwner = false;
droppedStack.Remove(this);
SetDroppedStackItemStates();
droppedStack = null;
#if SERVER
if (GameMain.NetworkMember is { IsServer: true } server)
{
server.CreateEntityEvent(this, new DroppedStackEventData(Enumerable.Empty<Item>()));
}
#endif
}
private void SetDroppedStackItemStates()
{
if (droppedStack == null) { return; }
bool isFirst = true;
foreach (Item item in droppedStack)
{
item.droppedStack = droppedStack;
item.isDroppedStackOwner = isFirst;
if (item.body != null)
{
item.body.Enabled = item.body.PhysEnabled = isFirst;
if (isFirst)
{
item.isActive = true;
item.body.ResetDynamics();
}
}
isFirst = false;
}
}
public void Equip(Character character)
{
if (Removed)
@@ -3072,7 +3309,7 @@ namespace Barotrauma
if (allProperties.Count > 1)
{
int propertyIndex = allProperties.FindIndex(p => p.property == property && p.obj == entity);
if (propertyIndex < -1)
if (propertyIndex < 0)
{
throw new Exception($"Could not find the property \"{property.Name}\" in \"{entity.Name ?? "null"}\"");
}
@@ -3187,6 +3424,11 @@ namespace Barotrauma
propertyIndex = (int)msg.ReadVariableUInt32();
}
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})");
}
bool allowEditing = true;
object parentObject = allProperties[propertyIndex].obj;
SerializableProperty property = allProperties[propertyIndex].property;
@@ -3598,7 +3840,9 @@ namespace Barotrauma
if (component.Parent != null) { component.IsActive = component.Parent.IsActive; }
component.OnItemLoaded();
}
item.FullyInitialized = true;
return item;
}
@@ -3811,6 +4055,7 @@ namespace Barotrauma
repairableItems.Remove(this);
sonarVisibleItems.Remove(this);
cleanableItems.Remove(this);
RemoveFromDroppedStack(allowClientExecute: true);
}
partial void RemoveProjSpecific();
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Barotrauma.Items.Components;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
@@ -20,9 +22,10 @@ namespace Barotrauma
ApplyStatusEffect = 7,
Upgrade = 8,
ItemStat = 9,
DroppedStack = 10,
MinValue = 0,
MaxValue = 9
MaxValue = 10
}
public interface IEventData : NetEntityEvent.IData
@@ -47,10 +50,12 @@ namespace Barotrauma
{
public EventType EventType => EventType.InventoryState;
public readonly ItemContainer Component;
public readonly Range SlotRange;
public InventoryStateEventData(ItemContainer component)
public InventoryStateEventData(ItemContainer component, Range slotRange)
{
Component = component;
SlotRange = slotRange;
}
}
@@ -62,6 +67,10 @@ namespace Barotrauma
public ChangePropertyEventData(SerializableProperty serializableProperty, ISerializableEntity entity)
{
if (serializableProperty.GetAttribute<Editable>() == null)
{
DebugConsole.ThrowError($"Attempted to create {nameof(ChangePropertyEventData)} for the non-editable property {serializableProperty.Name}.");
}
SerializableProperty = serializableProperty;
Entity = entity;
}
@@ -94,6 +103,13 @@ namespace Barotrauma
private readonly struct AssignCampaignInteractionEventData : IEventData
{
public EventType EventType => EventType.AssignCampaignInteraction;
public readonly ImmutableArray<Client> TargetClients;
public AssignCampaignInteractionEventData(IEnumerable<Client> targetClients)
{
TargetClients = (targetClients ?? Enumerable.Empty<Client>()).ToImmutableArray();
}
}
public readonly struct ApplyStatusEffectEventData : IEventData
@@ -63,7 +63,7 @@ namespace Barotrauma
if (itemPrefab == null) { return 0; }
if (i < 0 || i >= slots.Length) { return 0; }
if (!container.CanBeContained(itemPrefab, i)) { return 0; }
return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.MaxStackSize, container.GetMaxStackSize(i)), condition);
return slots[i].HowManyCanBePut(itemPrefab, maxStackSize: Math.Min(itemPrefab.GetMaxStackSize(this), container.GetMaxStackSize(i)), condition);
}
public override bool IsFull(bool takeStacksIntoAccount = false)
@@ -74,7 +74,7 @@ namespace Barotrauma
{
if (!slots[i].Any()) { return false; }
var item = slots[i].FirstOrDefault();
if (slots[i].Items.Count < Math.Min(item.Prefab.MaxStackSize, container.GetMaxStackSize(i))) { return false; }
if (slots[i].Items.Count < Math.Min(item.Prefab.GetMaxStackSize(this), container.GetMaxStackSize(i))) { return false; }
}
}
else
@@ -133,7 +133,7 @@ namespace Barotrauma
return wasPut;
}
public override void CreateNetworkEvent()
protected override void CreateNetworkEvent(Range slotRange)
{
if (!Item.ItemList.Contains(container.Item))
{
@@ -150,11 +150,17 @@ namespace Barotrauma
DebugConsole.Log("Creating a network event for the item \"" + container.Item + "\" failed, ItemContainer not found in components");
return;
}
if (slotRange.Start.Value < 0 || slotRange.End.Value > capacity)
{
DebugConsole.ThrowError($"Error when creating an inventory event: invalid slot range ({slotRange})\n" + Environment.StackTrace);
return;
}
if (GameMain.NetworkMember != null)
{
if (GameMain.NetworkMember.IsClient) { syncItemsDelay = 1.0f; }
GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container));
GameMain.NetworkMember.CreateEntityEvent(Owner as INetSerializable, new Item.InventoryStateEventData(container, slotRange));
}
}
@@ -43,9 +43,9 @@ namespace Barotrauma
public readonly bool CopyCondition;
//tag/identifier of the deconstructor(s) that can be used to deconstruct the item into this
public readonly string[] RequiredDeconstructor;
public readonly Identifier[] RequiredDeconstructor;
//tag/identifier of other item(s) that that need to be present in the deconstructor to deconstruct the item into this
public readonly string[] RequiredOtherItem;
public readonly Identifier[] RequiredOtherItem;
//text to display on the deconstructor's activate button when this output is available
public readonly string ActivateButtonText;
public readonly string InfoText;
@@ -63,9 +63,9 @@ namespace Barotrauma
OutConditionMax = element.GetAttributeFloat("outconditionmax", element.GetAttributeFloat("outcondition", 1.0f));
CopyCondition = element.GetAttributeBool("copycondition", false);
Commonness = element.GetAttributeFloat("commonness", 1.0f);
RequiredDeconstructor = element.GetAttributeStringArray("requireddeconstructor",
element.Parent?.GetAttributeStringArray("requireddeconstructor", Array.Empty<string>()) ?? Array.Empty<string>());
RequiredOtherItem = element.GetAttributeStringArray("requiredotheritem", Array.Empty<string>());
RequiredDeconstructor = element.GetAttributeIdentifierArray("requireddeconstructor",
element.Parent?.GetAttributeIdentifierArray("requireddeconstructor", Array.Empty<Identifier>()) ?? Array.Empty<Identifier>());
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);
@@ -88,20 +88,27 @@ namespace Barotrauma
public abstract ItemPrefab FirstMatchingPrefab { get; }
public LocalizedString OverrideHeader { get; }
public LocalizedString OverrideDescription { get; }
public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription)
public RequiredItem(int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem)
{
Amount = amount;
MinCondition = minCondition;
MaxCondition = maxCondition;
UseCondition = useCondition;
OverrideHeader = overrideHeader;
OverrideDescription = overrideDescription;
DefaultItem = defaultItem;
}
public readonly int Amount;
public readonly float MinCondition;
public readonly float MaxCondition;
public readonly bool UseCondition;
/// <summary>
/// Used only when there's multiple optional items.
/// </summary>
public readonly Identifier DefaultItem;
public bool IsConditionSuitable(float conditionPercentage)
{
@@ -138,8 +145,8 @@ namespace Barotrauma
return item?.Prefab.Identifier == ItemPrefabIdentifier;
}
public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription) :
base(amount, minCondition, maxCondition, useCondition, overrideDescription)
public RequiredItemByIdentifier(Identifier itemPrefab, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader) :
base(amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem: Identifier.Empty)
{
ItemPrefabIdentifier = itemPrefab;
using MD5 md5 = MD5.Create();
@@ -158,9 +165,26 @@ namespace Barotrauma
public override UInt32 UintIdentifier { get; }
public override IEnumerable<ItemPrefab> ItemPrefabs => ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag));
private readonly List<ItemPrefab> cachedPrefabs = new List<ItemPrefab>();
public override ItemPrefab FirstMatchingPrefab => ItemPrefab.Prefabs.FirstOrDefault(p => p.Tags.Contains(Tag));
private Md5Hash prevContentPackagesHash;
public override IEnumerable<ItemPrefab> ItemPrefabs
{
get
{
if (prevContentPackagesHash == null ||
!prevContentPackagesHash.Equals(ContentPackageManager.EnabledPackages.MergedHash))
{
cachedPrefabs.Clear();
cachedPrefabs.AddRange(ItemPrefab.Prefabs.Where(p => p.Tags.Contains(Tag)));
prevContentPackagesHash = ContentPackageManager.EnabledPackages.MergedHash;
}
return cachedPrefabs;
}
}
public override ItemPrefab FirstMatchingPrefab => ItemPrefabs.FirstOrDefault();
public override bool MatchesItem(Item item)
{
@@ -168,8 +192,8 @@ namespace Barotrauma
return item.HasTag(Tag);
}
public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription)
: base(amount, minCondition, maxCondition, useCondition, overrideDescription)
public RequiredItemByTag(Identifier tag, int amount, float minCondition, float maxCondition, bool useCondition, LocalizedString overrideDescription, LocalizedString overrideHeader, Identifier defaultItem)
: base(amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem)
{
Tag = tag;
using MD5 md5 = MD5.Create();
@@ -186,7 +210,7 @@ namespace Barotrauma
public ItemPrefab TargetItem => ItemPrefab.Prefabs[TargetItemPrefabIdentifier];
private readonly Lazy<LocalizedString> displayName;
public LocalizedString DisplayName
public LocalizedString DisplayName
=> ItemPrefab.Prefabs.ContainsKey(TargetItemPrefabIdentifier) ? displayName.Value : "";
public readonly ImmutableArray<RequiredItem> RequiredItems;
public readonly ImmutableArray<Identifier> SuitableFabricatorIdentifiers;
@@ -198,6 +222,7 @@ namespace Barotrauma
public readonly uint RecipeHash;
public readonly int Amount;
public readonly int? Quality;
public readonly bool HideForNonTraitors;
/// <summary>
/// How many of this item the fabricator can create (< 0 = unlimited)
@@ -230,6 +255,8 @@ namespace Barotrauma
FabricationLimitMin = element.GetAttributeInt(nameof(FabricationLimitMin), limitDefault);
FabricationLimitMax = element.GetAttributeInt(nameof(FabricationLimitMax), limitDefault);
HideForNonTraitors = element.GetAttributeBool(nameof(HideForNonTraitors), false);
if (element.GetAttribute(nameof(Quality)) != null)
{
Quality = element.GetAttributeInt(nameof(Quality), 0);
@@ -266,10 +293,15 @@ namespace Barotrauma
bool useCondition = subElement.GetAttributeBool("usecondition", true);
int amount = subElement.GetAttributeInt("count", subElement.GetAttributeInt("amount", 1));
LocalizedString description = string.Empty;
if (subElement.GetAttributeString("description", string.Empty) is string texTag && !texTag.IsNullOrEmpty())
LocalizedString overrideDescription = string.Empty;
if (subElement.GetAttributeString("description", string.Empty) is string descriptionTag && !descriptionTag.IsNullOrEmpty())
{
description = TextManager.Get(texTag);
overrideDescription = TextManager.Get(descriptionTag);
}
LocalizedString overrideHeader = string.Empty;
if (subElement.GetAttributeString("header", string.Empty) is string headerTag && !headerTag.IsNullOrEmpty())
{
overrideHeader = TextManager.Get(headerTag);
}
if (requiredItemIdentifier != Identifier.Empty)
@@ -284,7 +316,7 @@ namespace Barotrauma
amount += requiredItems[existing].Amount;
requiredItems.RemoveAt(existing);
}
requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, description));
requiredItems.Add(new RequiredItemByIdentifier(requiredItemIdentifier, amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader));
}
else
{
@@ -298,7 +330,8 @@ namespace Barotrauma
amount += requiredItems[existing].Amount;
requiredItems.RemoveAt(existing);
}
requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, description));
Identifier defaultItem = subElement.GetAttributeIdentifier("defaultitem", Identifier.Empty);
requiredItems.Add(new RequiredItemByTag(requiredItemTag, amount, minCondition, maxCondition, useCondition, overrideDescription, overrideHeader, defaultItem));
}
break;
}
@@ -316,12 +349,13 @@ namespace Barotrauma
uint outputId = ToolBox.IdentifierToUint32Hash(TargetItemPrefabIdentifier, md5);
var requiredItems = string.Join(':', RequiredItems
.Select(i => i.UintIdentifier)
.Select(i => string.Join(',', i)));
.Select(static i => $"{i.UintIdentifier}:{i.Amount}")
.Select(static i => string.Join(',', i)));
// above but include the item amount
var requiredSkills = string.Join(':', RequiredSkills.Select(s => $"{s.Identifier}:{s.Level}"));
uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{requiredItems}|{requiredSkills}", md5);
uint retVal = ToolBox.StringToUInt32Hash($"{Amount}|{outputId}|{RequiredTime}|{RequiresRecipe}|{requiredItems}|{requiredSkills}", md5);
if (retVal == 0) { retVal = 1; }
return retVal;
}
@@ -681,6 +715,9 @@ namespace Barotrauma
[Serialize(0.0f, IsPropertySaveable.No)]
public float OffsetOnSelected { get; private set; }
[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; }
private float health;
[Serialize(100.0f, IsPropertySaveable.No)]
@@ -749,7 +786,7 @@ namespace Barotrauma
[Serialize(false, IsPropertySaveable.No)]
public bool DisableItemUsageWhenSelected { get; private set; }
[Serialize("", IsPropertySaveable.No)]
[Serialize("metalcrate", IsPropertySaveable.No)]
public string CargoContainerIdentifier { get; private set; }
[Serialize(false, IsPropertySaveable.No)]
@@ -802,7 +839,40 @@ namespace Barotrauma
public int MaxStackSize
{
get { return maxStackSize; }
private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxStackSize); }
private set { maxStackSize = MathHelper.Clamp(value, 1, Inventory.MaxPossibleStackSize); }
}
private int maxStackSizeCharacterInventory;
[Serialize(-1, IsPropertySaveable.No)]
public int MaxStackSizeCharacterInventory
{
get { return maxStackSizeCharacterInventory; }
private set { maxStackSizeCharacterInventory = Math.Min(value, Inventory.MaxPossibleStackSize); }
}
private int maxStackSizeHoldableOrWearableInventory;
[Serialize(-1, IsPropertySaveable.No)]
public int MaxStackSizeHoldableOrWearableInventory
{
get { return maxStackSizeHoldableOrWearableInventory; }
private set { maxStackSizeHoldableOrWearableInventory = Math.Min(value, Inventory.MaxPossibleStackSize); }
}
public int GetMaxStackSize(Inventory inventory)
{
if (inventory is CharacterInventory && maxStackSizeCharacterInventory > 0)
{
return maxStackSizeCharacterInventory;
}
else if (maxStackSizeHoldableOrWearableInventory > 0 &&
inventory?.Owner is Item item && (item.GetComponent<Holdable>() != null || item.GetComponent<Wearable>() != null))
{
return maxStackSizeHoldableOrWearableInventory;
}
else
{
return maxStackSize;
}
}
[Serialize(false, IsPropertySaveable.No)]
@@ -830,13 +900,19 @@ namespace Barotrauma
[Serialize(1.0f, IsPropertySaveable.No, description: "How much the bots prioritize shooting this item with slow turrets, like railguns? Defaults to 1. Not used if AITurretPriority is 0. Distance to the target affects the decision making.")]
public float AISlowTurretPriority { get; private set; }
[Serialize(float.PositiveInfinity, IsPropertySaveable.No, description: "The max distance at which the bots are allowed to target the items. Defaults to infinity.")]
public float AITurretTargetingMaxDistance { get; private set; }
[Serialize(false, IsPropertySaveable.Yes, description: "If enabled, taking items from this container is never considered stealing.")]
public bool AllowStealingContainedItems { get; private set; }
[Serialize("255,255,255,255", IsPropertySaveable.No, description: "Used in circuit box to set the color of the nodes.")]
public Color SignalComponentColor { get; private set; }
[Serialize(false, IsPropertySaveable.No, description: "If enabled, the player is unable to open the middle click menu when this item is selected.")]
public bool DisableCommandMenuWhenSelected { get; set; }
protected override Identifier DetermineIdentifier(XElement element)
{
Identifier identifier = base.DetermineIdentifier(element);
@@ -1040,7 +1116,7 @@ namespace Barotrauma
{
DebugConsole.ThrowError(
$"Error in item prefab \"{ToString()}\": " +
$"{prevRecipe.DisplayName} has the same hash as {newRecipe.DisplayName}. " +
$"{prevRecipe.TargetItemPrefabIdentifier} has the same hash as {newRecipe.TargetItemPrefabIdentifier}. " +
$"This will cause issues with fabrication."
);
}
@@ -1172,6 +1248,11 @@ namespace Barotrauma
#endif
this.allowedLinks = ConfigElement.GetAttributeIdentifierArray("allowedlinks", Array.Empty<Identifier>()).ToImmutableHashSet();
GrabWhenSelected = ConfigElement.GetAttributeBool(
nameof(GrabWhenSelected),
ConfigElement.GetChildElement(nameof(ItemContainer)) != null &&
ConfigElement.GetChildElement("Body") == null);
}
public CommonnessInfo? GetCommonnessInfo(Level level)
@@ -43,21 +43,24 @@ namespace Barotrauma
}
/// <summary>
/// Should an empty inventory be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it.
/// Should an empty inventory (or an empty inventory slot if <see cref="TargetSlot"/> is set) be considered valid? Can be used to, for example, make an item do something if there's a specific item, or nothing, inside it.
/// </summary>
public bool MatchOnEmpty { get; set; }
/// <summary>
/// Should only an empty inventory be considered valid? Can be used to, for example, make an item do something when there's nothing inside it.
/// Should only an empty inventory (or an empty inventory slot if <see cref="TargetSlot"/> is set) be considered valid? Can be used to, for example, make an item do something when there's nothing inside it.
/// </summary>
public bool RequireEmpty { get; set; }
private bool RequireOrMatchOnEmpty => MatchOnEmpty || RequireEmpty;
/// <summary>
/// Only valid for the RequiredItems of an ItemComponent. Can be used to ignore the requirement in the submarine editor,
/// making it easier to for example make rewire things that require some special tool to rewire.
/// </summary>
public bool IgnoreInEditor { get; set; }
/// <summary>
/// Identifier(s) or tag(s) of the items that are NOT considered valid.
/// Can be used to, for example, exclude some specific items when using tags that apply to multiple items.
@@ -103,6 +106,11 @@ namespace Barotrauma
/// </summary>
public int TargetSlot = -1;
/// <summary>
/// The slot type the target must be in when targeting an item contained inside a character's inventory
/// </summary>
public InvSlotType CharacterInventorySlotType;
/// <summary>
/// Overrides the position defined in ItemContainer. Only valid when used in the Containable definitions of an ItemContainer.
/// </summary>
@@ -167,6 +175,10 @@ namespace Barotrauma
{
if (item.HasTag(excludedIdentifier)) { return false; }
}
if (item.ParentInventory?.Owner is Character character && CharacterInventorySlotType != InvSlotType.None)
{
if (!character.HasEquippedItem(item, CharacterInventorySlotType)) { return false; }
}
if (Identifiers.Contains(item.Prefab.Identifier)) { return true; }
foreach (var identifier in Identifiers)
{
@@ -261,6 +273,8 @@ namespace Barotrauma
Rotation = element.GetAttributeFloat("rotation", 0f);
SetActive = element.GetAttributeBool("setactive", false);
CharacterInventorySlotType = element.GetAttributeEnum(nameof(CharacterInventorySlotType), InvSlotType.None);
if (element.GetAttribute(nameof(Hide)) != null)
{
Hide = element.GetAttributeBool(nameof(Hide), false);
@@ -332,7 +346,7 @@ namespace Barotrauma
case RelationType.Equipped:
if (character == null) { return false; }
var heldItems = character.HeldItems;
if ((RequireEmpty || MatchOnEmpty) && heldItems.None()) { return true; }
if (RequireOrMatchOnEmpty && heldItems.None()) { return true; }
foreach (Item equippedItem in heldItems)
{
if (equippedItem == null) { continue; }
@@ -347,7 +361,7 @@ namespace Barotrauma
if (character == null) { return false; }
if (character.Inventory == null) { return MatchOnEmpty || RequireEmpty; }
var allItems = character.Inventory.AllItems;
if ((RequireEmpty || MatchOnEmpty) && allItems.None()) { return true; }
if (RequireOrMatchOnEmpty && allItems.None()) { return true; }
foreach (Item pickedItem in allItems)
{
if (pickedItem == null) { continue; }
@@ -370,11 +384,21 @@ namespace Barotrauma
private bool CheckContained(Item parentItem)
{
if (parentItem.OwnInventory == null) { return false; }
bool isEmpty = parentItem.OwnInventory.IsEmpty();
if (RequireEmpty && !isEmpty) { return false; }
if (MatchOnEmpty && isEmpty) { return true; }
if (TargetSlot == -1 && RequireOrMatchOnEmpty)
{
bool isEmpty = parentItem.OwnInventory.IsEmpty();
if (RequireEmpty) { return isEmpty; }
if (MatchOnEmpty && isEmpty) { return true; }
}
foreach (var container in parentItem.GetComponents<Items.Components.ItemContainer>())
{
if (TargetSlot > -1 && RequireOrMatchOnEmpty)
{
var itemInSlot = container.Inventory.GetItemAt(TargetSlot);
if (RequireEmpty) { return itemInSlot == null; }
if (MatchOnEmpty && itemInSlot == null) { return true; }
}
foreach (Item contained in container.Inventory.AllItems)
{
if (TargetSlot > -1 && parentItem.OwnInventory.FindIndex(contained) != TargetSlot) { continue; }
@@ -389,7 +413,8 @@ namespace Barotrauma
{
element.Add(
new XAttribute("items", JoinedIdentifiers),
new XAttribute("type", type.ToString()),
new XAttribute("type", type.ToString()),
new XAttribute("characterinventoryslottype", CharacterInventorySlotType.ToString()),
new XAttribute("optional", IsOptional),
new XAttribute("ignoreineditor", IgnoreInEditor),
new XAttribute("excludebroken", ExcludeBroken),
@@ -9,11 +9,13 @@ namespace Barotrauma
{
public Identifier Item;
public int Amount;
public bool MultiPlayerOnly;
public StartItem(XElement element)
{
Item = element.GetAttributeIdentifier("identifier", Identifier.Empty);
Amount = element.GetAttributeInt("amount", 1);
Amount = element.GetAttributeInt(nameof(Amount), 1);
MultiPlayerOnly = element.GetAttributeBool(nameof(MultiPlayerOnly), false);
}
}