OBT/1.2.0(Spring Update)

Sync with Upstream
This commit is contained in:
NotAlwaysTrue
2026-04-25 13:25:41 +08:00
committed by GitHub
parent 5207b381b7
commit 59bc21973a
421 changed files with 24090 additions and 11391 deletions
@@ -739,6 +739,12 @@ namespace Barotrauma.Items.Components
{
picker.Inventory.FlashAllowedSlots(item, Color.Red);
}
else
{
//normally this would be done in the base.OnPicked method, but clients don't call it,
//but instead rely on the server telling them to put the item in the inventory
SoundPlayer.PlayUISound(GUISoundType.PickItem);
}
return false;
}
#endif
@@ -1,4 +1,5 @@
using FarseerPhysics;
using Barotrauma.LuaCs.Events;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using Microsoft.Xna.Framework;
@@ -347,18 +348,9 @@ namespace Barotrauma.Items.Components
}
else if (f2.Body.UserData is Character targetCharacter)
{
if (targetCharacter == picker || targetCharacter == User) { return false; }
if (targetCharacter.IgnoreMeleeWeapons) { return false; }
if (HitFriendlyTarget(targetCharacter)) { return false; }
if (AllowHitMultiple)
{
if (hitTargets.Contains(targetCharacter)) { return false; }
}
else
{
if (hitTargets.Any(t => t is Character)) { return false; }
}
hitTargets.Add(targetCharacter);
//only allow hitting limbs, not the main collider
//otherwise it's difficult to make certain parts of the ragdoll not take hits by making them ignore collisions or melee weapons
return false;
}
else if (!HitOnlyCharacters)
{
@@ -435,7 +427,7 @@ namespace Barotrauma.Items.Components
Structure targetStructure = target.UserData as Structure ?? targetFixture.UserData as Structure;
Item targetItem = target.UserData is Holdable h ? h.Item : target.UserData as Item ?? targetFixture.UserData as Item;
Entity targetEntity = targetCharacter ?? targetStructure ?? targetItem ?? target.UserData as Entity;
GameMain.LuaCs.Hook.Call("meleeWeapon.handleImpact", this, target);
LuaCsSetup.Instance.EventService.PublishEvent<IEventMeleeWeaponHandleImpact>(x => x.OnMeleeWeaponHandleImpact(this, target));
if (Attack != null)
{
@@ -380,7 +380,7 @@ namespace Barotrauma.Items.Components
break;
case "requireditem":
case "requireditems":
SetRequiredItems(subElement);
SetRequiredItems(subElement, allowEmpty: true);
break;
case "requiredskill":
case "requiredskills":
@@ -1102,6 +1102,9 @@ namespace Barotrauma.Items.Components
foreach (RelatedItem ri in DisabledRequiredItems)
{
XElement newElement = new XElement("requireditem");
//if we have some actual requirements, no need to keep the empty requirement
//as a "placeholder" for the user to add requirements in the sub editor
if (ri.Identifiers.IsEmpty && RequiredItems.Any()) { continue; }
ri.Save(newElement);
componentElement.Add(newElement);
}
@@ -425,7 +425,7 @@ namespace Barotrauma.Items.Components
partial void InitProjSpecific(ContentXElement element);
public void OnItemContained(Item containedItem)
public void OnItemContained(Item containedItem, bool triggerOnInsertedEffects = true)
{
int index = Inventory.FindIndex(containedItem);
RelatedItem relatedItem = null;
@@ -444,14 +444,20 @@ namespace Barotrauma.Items.Components
ActiveContainedItem activeContainedItem = new(containedItem, effect, containableItem.ExcludeBroken, containableItem.ExcludeFullCondition, containableItem.BlameEquipperForDeath);
activeContainedItems.Add(activeContainedItem);
if (!ShouldApplyEffects(activeContainedItem) || item.Submarine is { Loading: true} || initializingLoadedItems ||
containedItem.OnInsertedEffectsApplied)
{
continue;
if (triggerOnInsertedEffects)
{
if (!ShouldApplyEffects(activeContainedItem) || item.Submarine is { Loading: true} || initializingLoadedItems ||
containedItem.OnInsertedEffectsApplied)
{
continue;
}
activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets);
}
activeContainedItem.StatusEffect.Apply(ActionType.OnInserted, deltaTime: 1, item, targets);
}
containedItem.OnInsertedEffectsApplied = true;
if (triggerOnInsertedEffects)
{
containedItem.OnInsertedEffectsApplied = true;
}
}
}
}
@@ -1119,6 +1125,16 @@ namespace Barotrauma.Items.Components
}
else
{
if (item.GetComponent<Holdable>() is { Attachable: true })
{
//if the item is attachable to walls, we need a bit of special logic because the item can either
//have or not have a body depending on whether it's attached.
//since it seems previously the contained item positions have always been configured as if the item had no body (using the top-left corner as the origin),
//let's modify the position here to position the items correctly even when the body is active (moving the origin from the center of the body to the top-left corner)
transformedItemPos -= item.Rect.Size.FlipY().ToVector2() / 2;
}
Matrix transform = Matrix.CreateRotationZ(drawPosition ? item.body.DrawRotation : item.body.Rotation);
if (bodyFlipped)
{
@@ -0,0 +1,121 @@
#nullable enable
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
{
/// <summary>
/// Item component used by <see cref="Controller.SpawnItemOnSelected"/> for keeping a reference to the character that is currently
/// selecting the controller. Also provides functionality for changing the inventory sprite of the item based on the linked character.
/// </summary>
partial class LinkedControllerCharacterComponent : ItemComponent, IServerSerializable
{
#if CLIENT
private class SpriteOverride
{
public readonly Sprite? Sprite;
public readonly Identifier SpeciesName;
public readonly Identifier SpeciesGroup;
public SpriteOverride(ContentXElement element)
{
if (element.GetChildElement("Sprite") is ContentXElement spriteElement)
{
Sprite = new Sprite(spriteElement);
}
SpeciesName = element.GetAttributeIdentifier("speciesname", Identifier.Empty);
SpeciesGroup = element.GetAttributeIdentifier("speciesgroup", Identifier.Empty);
}
}
private readonly ImmutableArray<SpriteOverride> spriteOverrides;
#endif
[Serialize(0.5f, IsPropertySaveable.No, description: $"Maximum value which {nameof(DeconstructTimeMultiplier)} can be.")]
public float MaxDeconstructTimeMultiplier
{
get;
set;
}
public Character? Character { get; private set; }
public bool DoesBleed => Character?.DoesBleed == true;
public float DeconstructTimeMultiplier { get; private set; } = 1f;
public LinkedControllerCharacterComponent(Item item, ContentXElement element) : base(item, element)
{
#if CLIENT
spriteOverrides = element.Elements()
.Where(static e => e.Name.LocalName.ToLowerInvariant() == "spriteoverride")
.Select(static e => new SpriteOverride(e))
.ToImmutableArray();
#endif
}
public void UpdateLinkedCharacter(Character? character)
{
Character = character;
if (character != null)
{
var animController = character.AnimController;
float totalLimbs = animController.Limbs.Length;
float nonSeveredLimbs = animController.Limbs.Count(static l => !l.IsSevered);
// Decrease deconstruction time if the character is missing some limbs
DeconstructTimeMultiplier *= MathF.Max(MaxDeconstructTimeMultiplier, nonSeveredLimbs / totalLimbs);
}
#if CLIENT
if (character != null)
{
SpriteOverride? spriteOverride =
spriteOverrides.Where(s => s.SpeciesName == character.SpeciesName).FirstOrDefault()
?? spriteOverrides.Where(s => s.SpeciesGroup == character.Group).FirstOrDefault();
if (spriteOverride != null)
{
item.OverrideInventorySprite = spriteOverride.Sprite;
}
}
else
{
item.OverrideInventorySprite = null;
}
#elif SERVER
Item.CreateServerEvent(this);
#endif
}
public void ClientEventRead(IReadMessage msg, float sendingTime)
{
UInt16 characterId = msg.ReadUInt16();
if (characterId == Entity.NullEntityID)
{
UpdateLinkedCharacter(null);
}
else if (Entity.FindEntityByID(characterId) is Character character)
{
UpdateLinkedCharacter(character);
}
}
public void ServerEventWrite(IWriteMessage msg, Client c, NetEntityEvent.IData? extraData = null)
{
if (Character != null)
{
msg.WriteUInt16(Character.ID);
}
else
{
msg.WriteUInt16(Entity.NullEntityID);
}
}
}
}
@@ -1,9 +1,12 @@
using FarseerPhysics;
using Barotrauma.Networking;
using Barotrauma.Networking;
using FarseerPhysics;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma.Items.Components
@@ -45,6 +48,39 @@ namespace Barotrauma.Items.Components
private Camera cam;
private Character user;
public Character User
{
get { return user; }
private set
{
if (user == value)
{
return;
}
user = value;
if (user != null)
{
teleportTransition = 0f;
teleportStartPosition = user.WorldPosition;
}
#if SERVER
item.CreateServerEvent(this);
#endif
#if CLIENT
UpdateMsg();
if (HideAllItemComponentHUDs && Character.Controlled == user)
{
// Prevents any UIs in this item from briefly showing up when you select this controller, since
// activeHUDs would take a single frame to be updated to not contain any other item component HUD
Item.ClearActiveHUDs();
}
#endif
}
}
private Item focusTarget;
private float targetRotation;
@@ -55,11 +91,6 @@ namespace Barotrauma.Items.Components
set { userPos = value; }
}
public Character User
{
get { return user; }
}
public IEnumerable<LimbPos> LimbPositions { get { return limbPositions; } }
[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)]
@@ -123,6 +154,13 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Should the HUDs of all item components in this item be hidden when a character is using this controller.")]
public bool HideAllItemComponentHUDs
{
get;
set;
}
public enum UseEnvironment
{
Air, Water, Both
@@ -152,6 +190,49 @@ namespace Barotrauma.Items.Components
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Can a character put another character into this controller by dragging them and selecting this controller?")]
public bool AllowPuttingInOtherCharacters
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can a character select this controller by themselves?")]
public bool CanBeSelectedByCharacters
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "If a character selects this controller, but another character already has it selected, should it be kicked out?")]
public bool SelectingKicksCharacterOut
{
get;
set;
}
[Serialize("", IsPropertySaveable.No, description: "Message displayed when there's a character inside this controller.")]
public string KickOutCharacterMsg
{
get;
set;
}
[Serialize("", IsPropertySaveable.No, description: "Message displayed when you are putting a character into the controller.")]
public string PutOtherCharacterMsg
{
get;
set;
}
[Serialize("", IsPropertySaveable.No, description: "Spawns this item in the first available item container slot when a character selects this controller, if the item container is full, the character will not be able to select the controller.")]
public Identifier SpawnItemOnSelected
{
get;
private set;
}
public bool ControlCharacterPose
{
get { return limbPositions.Count > 0; }
@@ -204,6 +285,23 @@ namespace Barotrauma.Items.Components
set;
}
/// <summary>
/// Used to determine how fast the character is teleported
/// to the item when they first select the controller.
/// Only relevant for <see cref="ForceUserToStayAttached" />
/// </summary>
private const float TeleportTransitionSpeed = 8f;
private float teleportTransition = 0f;
private Vector2 teleportStartPosition;
private readonly ItemPrefab spawnItemOnSelectedPrefab;
private readonly ItemContainer containerToSpawnOnSelectedItem;
/// <summary>
/// Item spawned by <see cref="spawnItemOnSelectedPrefab"/>
/// </summary>
private Item spawnedItemOnSelected = null;
public Controller(Item item, ContentXElement element)
: base(item, element)
{
@@ -211,6 +309,18 @@ namespace Barotrauma.Items.Components
Enum.TryParse(element.GetAttributeString("direction", "None"), out dir);
LoadLimbPositions(element);
IsActive = true;
containerToSpawnOnSelectedItem = item.GetComponent<ItemContainer>();
if (!SpawnItemOnSelected.IsEmpty && !ItemPrefab.Prefabs.TryGet(SpawnItemOnSelected, out spawnItemOnSelectedPrefab))
{
DebugConsole.ThrowError($"Failed to find item prefab \"{SpawnItemOnSelected}\"");
}
if (containerToSpawnOnSelectedItem == null && !SpawnItemOnSelected.IsEmpty)
{
DebugConsole.ThrowError($"Error - Controller has a {nameof(SpawnItemOnSelected)} but no ItemContainer defined");
}
}
/// <summary>
@@ -236,58 +346,77 @@ namespace Barotrauma.Items.Components
item.SendSignal(signal, "trigger_out");
}
if (forceSelectNextFrame && user != null)
if (forceSelectNextFrame && User != null)
{
user.SelectedItem = item;
User.SelectedItem = item;
}
forceSelectNextFrame = false;
userCanInteractCheckTimer -= deltaTime;
if (user == null
|| user.Removed
|| !user.IsAnySelectedItem(item)
|| (item.ParentInventory != null && !IsAttachedUser(user))
|| (UsableIn == UseEnvironment.Water && !user.AnimController.InWater)
|| (UsableIn == UseEnvironment.Air && user.AnimController.InWater)
|| !CheckUserCanInteract())
if (User == null
|| User.Removed
|| (((User.Stun <= 0f && !User.IsKnockedDownOrRagdolled && !User.LockHands) || !ForceUserToStayAttached) && (!User.IsAnySelectedItem(item) || !CheckUserCanInteract()))
|| (item.ParentInventory != null && !IsAttachedUser(User))
|| (UsableIn == UseEnvironment.Water && !User.AnimController.InWater)
|| (UsableIn == UseEnvironment.Air && User.AnimController.InWater)
|| !CheckSpawnItem()
)
{
if (user != null)
if (User != null)
{
CancelUsing(user);
user = null;
CancelUsing(User);
User = null;
}
if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; }
return;
}
if (ForceUserToStayAttached && Vector2.DistanceSquared(item.WorldPosition, user.WorldPosition) > 0.1f)
if (ForceUserToStayAttached)
{
user.TeleportTo(item.WorldPosition);
user.AnimController.Collider.ResetDynamics();
foreach (var limb in user.AnimController.Limbs)
teleportTransition = MathF.Min(teleportTransition + deltaTime * TeleportTransitionSpeed, 1f);
if (teleportTransition >= 1f)
{
if (limb.Removed || limb.IsSevered) { continue; }
limb.body?.ResetDynamics();
// Transition is complete, if someone was holding this character, force them to deselect
// so they aren't holding the character that is now attached to the controller
if (User.SelectedBy != null)
{
User.SelectedBy.SelectedCharacter = null;
}
}
if (User == Character.Controlled
|| teleportTransition < 1f
|| Vector2.DistanceSquared(item.WorldPosition, User.WorldPosition) > 0.1f)
{
var targetPosition = Vector2.Lerp(teleportStartPosition, item.WorldPosition, teleportTransition);
User.TeleportTo(targetPosition);
User.AnimController.Collider.ResetDynamics();
foreach (var limb in User.AnimController.Limbs)
{
if (limb.Removed || limb.IsSevered) { continue; }
limb.body?.ResetDynamics();
}
}
}
user.AnimController.StartUsingItem();
User.AnimController.StartUsingItem();
if (userPos != Vector2.Zero)
{
Vector2 diff = (item.WorldPosition + userPos) - user.WorldPosition;
Vector2 diff = (item.WorldPosition + userPos) - User.WorldPosition;
if (user.AnimController.InWater)
if (User.AnimController.InWater)
{
if (diff.LengthSquared() > 30.0f * 30.0f)
{
user.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One);
user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
User.AnimController.TargetMovement = Vector2.Clamp(diff * 0.01f, -Vector2.One, Vector2.One);
User.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
}
else
{
user.AnimController.TargetMovement = Vector2.Zero;
User.AnimController.TargetMovement = Vector2.Zero;
UserInCorrectPosition = true;
}
}
@@ -295,10 +424,10 @@ namespace Barotrauma.Items.Components
{
// Secondary items (like ladders or chairs) will control the character position over primary items
// Only control the character position if the character doesn't have another secondary item already controlling it
if (!user.HasSelectedAnotherSecondaryItem(Item))
if (!User.HasSelectedAnotherSecondaryItem(Item))
{
diff.Y = 0.0f;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && user != Character.Controlled)
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && User != Character.Controlled)
{
if (Math.Abs(diff.X) > 20.0f)
{
@@ -308,48 +437,48 @@ namespace Barotrauma.Items.Components
else if (Math.Abs(diff.X) > 0.1f)
{
//aim to keep the collider at the correct position once close enough
user.AnimController.Collider.LinearVelocity = new Vector2(
User.AnimController.Collider.LinearVelocity = new Vector2(
diff.X * 0.1f,
user.AnimController.Collider.LinearVelocity.Y);
User.AnimController.Collider.LinearVelocity.Y);
}
}
else if (Math.Abs(diff.X) > 10.0f)
{
user.AnimController.TargetMovement = Vector2.Normalize(diff);
user.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
User.AnimController.TargetMovement = Vector2.Normalize(diff);
User.AnimController.TargetDir = diff.X > 0.0f ? Direction.Right : Direction.Left;
return;
}
user.AnimController.TargetMovement = Vector2.Zero;
User.AnimController.TargetMovement = Vector2.Zero;
}
UserInCorrectPosition = true;
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, user);
ApplyStatusEffects(ActionType.OnActive, deltaTime, User);
if (limbPositions.Count == 0) { return; }
user.AnimController.StartUsingItem();
User.AnimController.StartUsingItem();
if (user.SelectedItem != null)
if (User.SelectedItem != null)
{
user.AnimController.ResetPullJoints(l => l.IsLowerBody);
User.AnimController.ResetPullJoints(l => l.IsLowerBody);
}
else
{
user.AnimController.ResetPullJoints();
User.AnimController.ResetPullJoints();
}
if (dir != 0) { user.AnimController.TargetDir = dir; }
if (dir != 0) { User.AnimController.TargetDir = dir; }
foreach (LimbPos lb in limbPositions)
{
Limb limb = user.AnimController.GetLimb(lb.LimbType);
Limb limb = User.AnimController.GetLimb(lb.LimbType);
if (limb == null || !limb.body.Enabled) { continue; }
// Don't move lower body limbs if there's another selected secondary item that should control them
if (limb.IsLowerBody && user.HasSelectedAnotherSecondaryItem(Item)) { continue; }
if (limb.IsLowerBody && User.HasSelectedAnotherSecondaryItem(Item)) { continue; }
// Don't move hands if there's a selected primary item that should control them
if (limb.IsArm && Item == user.SelectedSecondaryItem && user.SelectedItem != null) { continue; }
if (limb.IsArm && Item == User.SelectedSecondaryItem && User.SelectedItem != null) { continue; }
if (lb.AllowUsingLimb)
{
switch (lb.LimbType)
@@ -357,12 +486,12 @@ namespace Barotrauma.Items.Components
case LimbType.RightHand:
case LimbType.RightForearm:
case LimbType.RightArm:
if (user.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; }
if (User.Inventory.GetItemInLimbSlot(InvSlotType.RightHand) != null) { continue; }
break;
case LimbType.LeftHand:
case LimbType.LeftForearm:
case LimbType.LeftArm:
if (user.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; }
if (User.Inventory.GetItemInLimbSlot(InvSlotType.LeftHand) != null) { continue; }
break;
}
}
@@ -374,15 +503,77 @@ namespace Barotrauma.Items.Components
}
}
private bool IsSpawnContainerFull()
{
if (spawnItemOnSelectedPrefab == null || containerToSpawnOnSelectedItem == null)
{
return false;
}
if (containerToSpawnOnSelectedItem.Inventory.IsFull())
{
return true;
}
return false;
}
private bool CheckSpawnItem()
{
if (spawnItemOnSelectedPrefab == null || containerToSpawnOnSelectedItem == null)
{
return true;
}
if (containerToSpawnOnSelectedItem.Inventory.AllItems.Any(x => x.Prefab == spawnItemOnSelectedPrefab))
{
return true;
}
if (spawnedItemOnSelected != null && !spawnedItemOnSelected.Removed)
{
if (spawnedItemOnSelected.ParentInventory != containerToSpawnOnSelectedItem.Inventory)
{
// Item was moved or dropped, force the user in this controller out
return false;
}
else
{
return true;
}
}
if (IsSpawnContainerFull())
{
return false;
}
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
Entity.Spawner.AddItemToSpawnQueue(spawnItemOnSelectedPrefab, containerToSpawnOnSelectedItem.Inventory, onSpawned: spawnedItem =>
{
spawnedItemOnSelected = spawnedItem;
var linkedCharacterComponent = spawnedItem.GetComponent<LinkedControllerCharacterComponent>();
if (linkedCharacterComponent != null)
{
linkedCharacterComponent.UpdateLinkedCharacter(User);
}
});
}
return true;
}
private bool CheckUserCanInteract()
{
//optimization: CanInteractWith is relatively heavy (can involve visibility checks for example), let's not do it every frame
if (user != null)
if (User != null)
{
if (userCanInteractCheckTimer <= 0.0f)
{
userCanInteractCheckTimer = UserCanInteractCheckInterval;
return user.CanInteractWith(item);
return User.CanInteractWith(item);
}
}
//we only do the actual check every UserCanInteractCheckInterval seconds
@@ -394,13 +585,13 @@ namespace Barotrauma.Items.Components
public override bool Use(float deltaTime, Character activator = null)
{
if (activator != user)
if (activator != User)
{
return false;
}
if (user == null || user.Removed || !user.IsAnySelectedItem(item) || !user.CanInteractWith(item))
if (User == null || User.Removed || !User.IsAnySelectedItem(item) || !User.CanInteractWith(item))
{
user = null;
User = null;
return false;
}
@@ -419,7 +610,7 @@ namespace Barotrauma.Items.Components
}
else if (!string.IsNullOrEmpty(output))
{
item.SendSignal(new Signal(output, sender: user), "trigger_out");
item.SendSignal(new Signal(output, sender: User), "trigger_out");
}
lastUsed = Timing.TotalTime;
@@ -428,13 +619,13 @@ namespace Barotrauma.Items.Components
public override bool SecondaryUse(float deltaTime, Character character = null)
{
if (user != character)
if (User != character)
{
return false;
}
if (user == null || character.Removed || !user.IsAnySelectedItem(item) || !character.CanInteractWith(item))
if (User == null || character.Removed || !User.IsAnySelectedItem(item) || !character.CanInteractWith(item))
{
user = null;
User = null;
return false;
}
if (character == null)
@@ -495,7 +686,7 @@ namespace Barotrauma.Items.Components
if (IsOutOfPower()) { return null; }
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: user), positionOut);
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: User), positionOut);
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
{
@@ -547,8 +738,20 @@ namespace Barotrauma.Items.Components
private void CancelUsing(Character character)
{
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
{
if (spawnedItemOnSelected != null)
{
Entity.Spawner.AddEntityToRemoveQueue(spawnedItemOnSelected);
spawnedItemOnSelected = null;
}
}
if (character == null || character.Removed) { return; }
// Wake character's colliders so they don't just float in the air when taken out of the controller
character.AnimController.BodyInRest = false;
foreach (LimbPos lb in limbPositions)
{
Limb limb = character.AnimController.GetLimb(lb.LimbType);
@@ -588,31 +791,80 @@ namespace Barotrauma.Items.Components
return false;
}
//someone already using the item
if (user != null && !user.Removed)
// Someone already using the item
if (User != null && !User.Removed)
{
if (user == activator)
// Let the server handle the logic here
if (GameMain.NetworkMember is { IsClient: true })
{
IsActive = false;
CancelUsing(user);
user = null;
return false;
}
else if (user.IsBot && !activator.IsBot)
// Prevent user from kicking character out if they are holding another character
if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter))
{
return false;
}
if (User == activator || SelectingKicksCharacterOut)
{
#if SERVER
if (User != activator)
{
GameServer.Log($"{GameServer.CharacterLogName(activator)} removed {GameServer.CharacterLogName(User)} from {item.Name}",
ServerLog.MessageType.Attack);
}
#endif
IsActive = false;
CancelUsing(User);
User = null;
return false;
}
else if (User.IsBot && !activator.IsBot)
{
if (AllowSelectingWhenSelectedByBot)
{
CancelUsing(user);
user = activator;
CancelUsing(User);
User = activator;
IsActive = true;
return true;
}
}
return AllowSelectingWhenSelectedByOther;
}
else
else if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter))
{
user = activator;
// Stun pets longer so they don't immediately get out of the controller
if (activator.SelectedCharacter.IsPet)
{
activator.SelectedCharacter.SetStun(MathF.Max(activator.SelectedCharacter.Stun, 4f), isNetworkMessage: true);
}
else
{
// Small amount of stun since non-ragdolled characters behave weirdly when syncing the periodic teleportation in multiplayer
activator.SelectedCharacter.SetStun(MathF.Max(activator.SelectedCharacter.Stun, 1f), isNetworkMessage: true);
}
#if SERVER
GameServer.Log($"{GameServer.CharacterLogName(activator)} forced {GameServer.CharacterLogName(activator.SelectedCharacter)} into {item.Name}",
ServerLog.MessageType.Attack);
#endif
User = activator.SelectedCharacter;
User.SelectedItem = this.Item;
IsActive = true;
if (ForceUserToStayAttached && item.Container != null)
{
forceSelectNextFrame = true;
}
return false;
}
else if (CanBeSelectedByCharacters)
{
activator.DeselectCharacter();
User = activator;
IsActive = true;
if (ForceUserToStayAttached && item.Container != null)
{
@@ -621,15 +873,12 @@ namespace Barotrauma.Items.Components
}
}
//allow the selection logic above to run when out of power, but allow sending signals
//allow the selection logic above to run when out of power, but disallow sending signals
if (IsOutOfPower()) { return false; }
#if SERVER
item.CreateServerEvent(this);
#endif
if (!string.IsNullOrEmpty(output))
{
item.SendSignal(new Signal(output, sender: user), "signal_out");
item.SendSignal(new Signal(output, sender: User), "signal_out");
}
return true;
}
@@ -639,7 +888,7 @@ namespace Barotrauma.Items.Components
/// </summary>
public bool IsAttachedUser(Character character)
{
return character != null && character == user && ForceUserToStayAttached;
return character != null && character == User && ForceUserToStayAttached;
}
public override void FlipX(bool relativeToSub)
@@ -668,12 +917,87 @@ namespace Barotrauma.Items.Components
}
}
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
{
#if CLIENT
UpdateMsg();
#endif
bool canPutCharacter = AllowPuttingInOtherCharacters && CanPutSelectedCharacter(character.SelectedCharacter, addMessage);
bool canKickCharacter = SelectingKicksCharacterOut && User != null && !User.Removed;
bool canUseController = CanBeSelectedByCharacters;
// Prevent kicking a character out when the user is holding another character to put into the controller.
// This avoids accidentally taking out a character (e.g. from a deconstructor).
if (canPutCharacter && canKickCharacter)
{
#if CLIENT
if (addMessage)
{
GUI.AddMessage(TextManager.Get("ItemMsgAlreadyHasCharacterFail"), Color.Red, playSound: false);
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
}
if (!canKickCharacter && !canPutCharacter && !canUseController)
{
return false;
}
if (IsSpawnContainerFull())
{
#if CLIENT
if (addMessage)
{
GUI.AddMessage(TextManager.Get("ItemMsgNotEnoughSpaceCharacterFail"), Color.Red, playSound: false);
SoundPlayer.PlayUISound(GUISoundType.PickItemFail);
}
#endif
return false;
}
return base.HasRequiredItems(character, addMessage, msg);
}
public override bool HasAccess(Character character)
{
if (!item.IsInteractable(character)) { return false; }
return base.HasAccess(character);
}
private bool CanPutSelectedCharacter(Character character, bool showMessage = false)
{
if (character == null)
{
return false;
}
if (!character.IsContainable)
{
#if CLIENT
if (showMessage)
{
GUI.AddMessage(TextManager.Get("ItemMsgPutCharacterFail"), Color.Red);
}
#endif
return false;
}
if (character.IsKnockedDownOrRagdolled) { return true; }
if (character.LockHands) { return true; }
if (character.IsPet)
{
return true;
}
return false;
}
partial void HideHUDs(bool value);
public override XElement Save(XElement parentElement)
@@ -1,11 +1,13 @@
using Barotrauma.Abilities;
using Barotrauma.Extensions;
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Xml.Linq;
using static OneOf.Types.TrueFalseOrNull;
namespace Barotrauma.Items.Components
{
@@ -114,7 +116,19 @@ namespace Barotrauma.Items.Components
float deconstructTime = 0.0f;
foreach (Item targetItem in inputContainer.Inventory.AllItems)
{
deconstructTime += targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier);
float itemDeconstructTime = item.Submarine is { Info.Type: SubmarineType.Outpost }
? targetItem.Prefab.DeconstructTimeInOutposts : targetItem.Prefab.DeconstructTime;
float targetDeconstructTime = itemDeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier);
var linkedCharacter = targetItem.GetComponent<LinkedControllerCharacterComponent>();
if (linkedCharacter != null)
{
targetDeconstructTime *= linkedCharacter.DeconstructTimeMultiplier;
}
deconstructTime += targetDeconstructTime;
ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructing, deltaTime);
}
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
@@ -143,8 +157,21 @@ namespace Barotrauma.Items.Components
var targetItem = inputContainer.Inventory.LastOrDefault();
if (targetItem == null) { return; }
ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructing, deltaTime);
var validDeconstructItems = targetItem.Prefab.DeconstructItems.Where(it => it.IsValidDeconstructor(item)).ToList();
float deconstructTime = validDeconstructItems.Any() ? targetItem.Prefab.DeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f;
float itemDeconstructTime = item.Submarine is { Info.Type: SubmarineType.Outpost }
? targetItem.Prefab.DeconstructTimeInOutposts : targetItem.Prefab.DeconstructTime;
float deconstructTime = !targetItem.Prefab.DeconstructItems.Any() || validDeconstructItems.Any()
? itemDeconstructTime / (deconstructionSpeed * deconstructionSpeedModifier) : 1.0f;
var linkedCharacter = targetItem.GetComponent<LinkedControllerCharacterComponent>();
if (linkedCharacter != null)
{
deconstructTime *= linkedCharacter.DeconstructTimeMultiplier;
}
progressState = Math.Min(progressTimer / deconstructTime, 1.0f);
if (progressTimer > deconstructTime)
@@ -183,6 +210,8 @@ namespace Barotrauma.Items.Components
amountMultiplier = (int)itemCreationMultiplier.Value;
}
ApplyDeconstructionStatusEffects(targetItem, ActionType.OnDeconstructed, 1f);
if (targetItem.Prefab.RandomDeconstructionOutput)
{
int amount = targetItem.Prefab.RandomDeconstructionOutputAmount;
@@ -297,30 +326,8 @@ namespace Barotrauma.Items.Components
{
humanAi.HandleRelocation(spawnedItem);
}
for (int i = 0; i < outputContainer.Capacity; i++)
{
var containedItem = outputContainer.Inventory.GetItemAt(i);
bool combined = false;
if (containedItem?.OwnInventory != null)
{
foreach (Item subItem in containedItem.ContainedItems.ToList())
{
if (subItem.Combine(spawnedItem, null))
{
combined = true;
break;
}
}
}
if (!combined)
{
if (containedItem?.Combine(spawnedItem, null) ?? false)
{
break;
}
}
}
PutItemsToLinkedContainer();
TryMoveItemToOutputContainers(spawnedItem);
});
}
}
@@ -332,8 +339,9 @@ namespace Barotrauma.Items.Components
GameAnalyticsManager.AddDesignEvent("ItemDeconstructed:" + (GameMain.GameSession?.GameMode?.Preset.Identifier.Value ?? "none") + ":" + targetItem.Prefab.Identifier);
}
bool? result = GameMain.LuaCs.Hook.Call<bool?>("item.deconstructed", targetItem, this, user, allowRemove);
if (result == true) { return; }
bool? should = null;
LuaCsSetup.Instance.EventService.PublishEvent<IEventItemDeconstructed>(x => should = x.OnItemDeconstructed(targetItem, this, user, allowRemove) ?? should);
if (should == true) { return; }
if (targetItem.AllowDeconstruct && allowRemove)
{
@@ -350,6 +358,7 @@ namespace Barotrauma.Items.Components
}
}
}
inputContainer.Inventory.RemoveItem(targetItem);
Entity.Spawner.AddItemToRemoveQueue(targetItem);
MoveInputQueue();
@@ -384,6 +393,34 @@ namespace Barotrauma.Items.Components
}
}
private void TryMoveItemToOutputContainers(Item spawnedItem)
{
for (int i = 0; i < outputContainer.Capacity; i++)
{
var containedItem = outputContainer.Inventory.GetItemAt(i);
bool combined = false;
if (containedItem?.OwnInventory != null)
{
foreach (Item subItem in containedItem.ContainedItems.ToList())
{
if (subItem.Combine(spawnedItem, null))
{
combined = true;
break;
}
}
}
if (!combined)
{
if (containedItem?.Combine(spawnedItem, null) ?? false)
{
break;
}
}
}
PutItemsToLinkedContainer();
}
private void PutItemsToLinkedContainer()
{
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
@@ -402,6 +439,72 @@ namespace Barotrauma.Items.Components
}
}
private void ApplyDeconstructionStatusEffects(Item targetItem, ActionType type, float deltaTime)
{
var linkedCharacterComponent = targetItem.GetComponent<LinkedControllerCharacterComponent>();
Character character = null;
if (linkedCharacterComponent is { Character.Removed: false })
{
character = linkedCharacterComponent.Character;
}
Limb limb = character?.AnimController.Limbs.GetRandomUnsynced();
if (user != null)
{
item.GetStatusEffectsOfType(type).ForEach(statusEffect => statusEffect.SetUser(user));
targetItem.GetStatusEffectsOfType(type).ForEach(statusEffect => statusEffect.SetUser(user));
}
// Apply OnDeconstruct/OnDeconstructing to the Deconstructor/item being deconstructed
item.ApplyStatusEffects(type, deltaTime, character, limb, useTarget: targetItem);
targetItem.ApplyStatusEffects(type, deltaTime, character, limb);
if (character != null)
{
if (type == ActionType.OnDeconstructed)
{
// Move whatever was on the character inventory to free up space for status effects that spawn items
MoveItemsFromCharacterToOutput();
}
character.ApplyStatusEffects(type, deltaTime);
if (type == ActionType.OnDeconstructed)
{
// This needs to run next frame because the status effect might enqueue items to be spawned next frame
CoroutineManager.Invoke(() =>
{
if (character.Removed) { return; }
// Move items again since the status effect could have spawned additional items in the character inventory
MoveItemsFromCharacterToOutput();
Entity.Spawner?.AddEntityToRemoveQueue(character);
}, 0.1f);
}
void MoveItemsFromCharacterToOutput()
{
if (character.Inventory != null)
{
foreach (var item in character.Inventory.AllItemsMod)
{
if (item.Removed) { continue; }
if (!item.IsPlayerTeamInteractable) { continue; }
if (!outputContainer.Inventory.TryPutItem(item, user: null))
{
item.Drop(dropper: null);
}
TryMoveItemToOutputContainers(item);
}
}
}
}
}
/// <summary>
/// Move items towards the last slot in the inventory if there's free slots
/// </summary>
@@ -388,6 +388,13 @@ namespace Barotrauma.Items.Components
{
Attack.DamageMultiplier = damageMultiplier;
}
foreach (var statusEffect in Item.GetStatusEffectsOfType(ActionType.OnImpact))
{
foreach (var explosion in statusEffect.Explosions)
{
explosion.Attack.DamageMultiplier = damageMultiplier;
}
}
// Set user for hitscan projectiles to work properly.
User = user;
// Need to set null for non-characterusable items.
@@ -460,6 +467,7 @@ namespace Barotrauma.Items.Components
{
initialRotation -= MathHelper.Pi;
}
Submarine initialSubmarine = item.Submarine;
for (int i = 0; i < HitScanCount; i++)
{
float launchAngle;
@@ -476,6 +484,8 @@ namespace Barotrauma.Items.Components
Vector2 launchDir = new Vector2((float)Math.Cos(launchAngle), (float)Math.Sin(launchAngle));
Vector2 prevSimpos = item.SimPosition;
item.body.SetTransformIgnoreContacts(item.body.SimPosition, launchAngle);
//when launching multiple projectiles, ensure each raycast starts from the same sub
item.Submarine = initialSubmarine;
if (Hitscan)
{
DoHitscan(launchDir);
@@ -25,7 +25,7 @@ namespace Barotrauma.Items.Components
FiringRateMultiplier
}
private readonly Dictionary<StatType, float> statValues = new Dictionary<StatType, float>();
public readonly Dictionary<StatType, float> statValues = new Dictionary<StatType, float>();
private int qualityLevel;
@@ -448,9 +448,10 @@ namespace Barotrauma.Items.Components
UpdateProjSpecific(deltaTime);
IsTinkering = false;
if (prevSentConditionValue != (int)item.ConditionPercentage || conditionSignal == null)
int condition = (int)(item.Condition / (item.MaxCondition / item.MaxRepairConditionMultiplier) * 100f);
if (prevSentConditionValue != condition || conditionSignal == null)
{
prevSentConditionValue = (int)item.ConditionPercentage;
prevSentConditionValue = condition;
conditionSignal = prevSentConditionValue.ToString();
}
@@ -27,6 +27,11 @@ namespace Barotrauma.Items.Components
private init => _displayName = value;
}
/// <summary>
/// Display name ignoring <see cref="DisplayNameOverride"/>
/// </summary>
public LocalizedString DefaultDisplayName => _displayName;
public LocalizedString DisplayNameOverride;
private readonly HashSet<Wire> wires;
@@ -350,15 +355,11 @@ namespace Barotrauma.Items.Components
wire.RegisterSignal(signal, source: this);
#endif
SendSignalIntoConnection(signal, recipient);
GameMain.LuaCs.Hook.Call("signalReceived", signal, recipient);
GameMain.LuaCs.Hook.Call("signalReceived." + recipient.item.Prefab.Identifier, signal, recipient);
}
foreach (CircuitBoxConnection connection in CircuitBoxConnections)
{
connection.ReceiveSignal(signal);
GameMain.LuaCs.Hook.Call("signalReceived", signal, connection.Connection);
GameMain.LuaCs.Hook.Call("signalReceived." + connection.Connection.Item.Prefab.Identifier, signal, connection);
}
enumeratingWires = false;
foreach (var removedWire in removedWires)
@@ -296,6 +296,7 @@ namespace Barotrauma.Items.Components
}
#endif
CheckIfNeedsUpdate();
SetLightSourceTransformProjSpecific();
}
public void CheckIfNeedsUpdate()
@@ -1,10 +1,13 @@
using Barotrauma.Networking;
using Barotrauma.LuaCs.Events;
using Barotrauma.Networking;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using static Barotrauma.CharacterHealth;
using static Barotrauma.MedicalClinic;
namespace Barotrauma.Items.Components
{
@@ -228,8 +231,8 @@ namespace Barotrauma.Items.Components
public void TransmitSignal(Signal signal, bool sentFromChat)
{
var should = GameMain.LuaCs.Hook.Call<bool?>("wifiSignalTransmitted", this, signal, sentFromChat);
bool? should = null;
LuaCsSetup.Instance.EventService.PublishEvent<IEventWifiSignalTransmitted>(x => should = x.OnWifiSignalTransmitted(this, signal, sentFromChat) ?? should);
if (should != null && should.Value) { return; }
bool chatMsgSent = false;
@@ -42,6 +42,10 @@ namespace Barotrauma.Items.Components
private float currentChargeTime;
private bool tryingToCharge;
private const float LineOfSightCheckInterval = 0.5f;
private (Body WorldTarget, Body TransformedTarget, double Time) lastLineOfSightCheck;
private (Character Target, bool CanSee, double Time) lastCanSeeTargetCheck;
private enum ChargingState
{
Inactive,
@@ -1088,6 +1092,7 @@ namespace Barotrauma.Items.Components
foreach (Submarine sub in Submarine.Loaded)
{
if (sub == Item.Submarine) { continue; }
if (sub.IsRespawnShuttle) { continue; }
if (item.Submarine != null)
{
if (Character.IsOnFriendlyTeam(item.Submarine.TeamID, sub.TeamID)) { continue; }
@@ -1164,15 +1169,28 @@ namespace Barotrauma.Items.Components
}
Vector2 start = ConvertUnits.ToSimUnits(item.WorldPosition);
Vector2 end = ConvertUnits.ToSimUnits(target.WorldPosition);
bool doLineOfSightCheck = lastLineOfSightCheck.Time < Timing.TotalTimeUnpaused - LineOfSightCheckInterval;
if (doLineOfSightCheck)
{
lastLineOfSightCheck.WorldTarget = CheckLineOfSight(start, end);
lastLineOfSightCheck.Time = Timing.TotalTime;
}
// Check that there's not other entities that shouldn't be targeted (like a friendly sub) between us and the target.
Body worldTarget = CheckLineOfSight(start, end);
Body worldTarget = lastLineOfSightCheck.WorldTarget;
bool shoot;
if (target.Submarine != null)
{
start -= target.Submarine.SimPosition;
end -= target.Submarine.SimPosition;
Body transformedTarget = CheckLineOfSight(start, end);
shoot = CanShoot(transformedTarget, user: null, friendlyTag, TargetSubmarines) && (worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines));
if (doLineOfSightCheck)
{
start -= target.Submarine.SimPosition;
end -= target.Submarine.SimPosition;
lastLineOfSightCheck.TransformedTarget = CheckLineOfSight(start, end);
}
shoot =
(worldTarget == null || CanShoot(worldTarget, user: null, friendlyTag, TargetSubmarines)) &&
CanShoot(lastLineOfSightCheck.TransformedTarget, user: null, friendlyTag, TargetSubmarines);
}
else
{
@@ -1437,8 +1455,20 @@ namespace Barotrauma.Items.Components
// Adjust the target character position (limb or submarine)
if (currentTarget is Character targetCharacter)
{
bool enemyInAnotherSub = targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine;
bool canSeeTarget = true;
if (enemyInAnotherSub)
{
if (lastCanSeeTargetCheck.Time < Timing.TotalTime - LineOfSightCheckInterval ||
targetCharacter != lastCanSeeTargetCheck.Target)
{
canSeeTarget = targetCharacter.CanSeeTarget(Item);
lastCanSeeTargetCheck = (targetCharacter, canSeeTarget, Timing.TotalTime);
}
}
//if the enemy is inside another sub, aim at the room they're in to make it less obvious that the enemy "knows" exactly where the target is
if (targetCharacter.Submarine != null && targetCharacter.CurrentHull != null && targetCharacter.Submarine != item.Submarine && !targetCharacter.CanSeeTarget(Item))
if (enemyInAnotherSub && !canSeeTarget)
{
targetPos = targetCharacter.CurrentHull.WorldPosition;
if (closestDistance > maxDistance * maxDistance)