Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Machines/Controller.cs
2026-04-30 21:59:54 +08:00

1111 lines
41 KiB
C#

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;
using System.Linq;
namespace Barotrauma.Items.Components
{
class LimbPos : ISerializableEntity
{
[Editable]
public LimbType LimbType { get; set; }
[Editable]
public Vector2 Position { get; set; }
public bool AllowUsingLimb;
public string Name => LimbType.ToString();
public Dictionary<Identifier, SerializableProperty> SerializableProperties => null;
public LimbPos(LimbType limbType, Vector2 position, bool allowUsingLimb)
{
LimbType = limbType;
Position = position;
AllowUsingLimb = allowUsingLimb;
}
}
partial class Controller : ItemComponent, IServerSerializable
{
//where the limbs of the user should be positioned when using the controller
private readonly List<LimbPos> limbPositions = new List<LimbPos>();
private Direction dir;
public Direction Direction => dir;
//the position where the user walks to when using the controller
//(relative to the position of the item)
private Vector2 userPos;
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;
public Vector2 UserPos
{
get { return userPos; }
set { userPos = value; }
}
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)]
public bool IsToggle
{
get;
set;
}
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 { 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.")]
public bool HideHUD
{
get;
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
};
[Serialize(UseEnvironment.Both, IsPropertySaveable.No, description: "Can the item be selected in air, underwater or both.")]
public UseEnvironment UsableIn { get; set; }
[Serialize(false, IsPropertySaveable.No, description: "Should the character using the item be drawn behind the item.")]
public bool DrawUserBehind
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when another character has already selected it?")]
public bool AllowSelectingWhenSelectedByOther
{
get;
set;
}
[Serialize(true, IsPropertySaveable.No, description: "Can another character select this controller when a bot has already selected it?")]
public bool AllowSelectingWhenSelectedByBot
{
get;
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; }
}
public bool UserInCorrectPosition
{
get;
private set;
}
public bool AllowAiming
{
get;
private set;
} = true;
[Serialize(false, IsPropertySaveable.No)]
public bool NonInteractableWhenFlippedX
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No)]
public bool NonInteractableWhenFlippedY
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "Does the Controller require power to function (= to send signals and move the camera focus to a connected item)?")]
public bool RequirePower
{
get;
set;
}
[Serialize(false, IsPropertySaveable.No, description: "If true, other items can be used simultaneously.")]
public bool IsSecondaryItem
{
get;
private set;
}
[Serialize(false, IsPropertySaveable.No, description: "If enabled, the user sticks to the position of this item even if the item moves.")]
public bool ForceUserToStayAttached
{
get;
set;
}
/// <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)
{
userPos = element.GetAttributeVector2("UserPos", Vector2.Zero);
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>
/// Hack for allowing characters to interact with a loader to get inside a boarding pod.
/// Doing that simply by autointeracting with the contained pod is difficult, because interacting with the loader selects it
/// _after_ the Select method of the pod is called by the autointeract logic, and the character only goes inside the pod if it's the selected item.
/// </summary>
private bool forceSelectNextFrame;
private float userCanInteractCheckTimer;
private const float UserCanInteractCheckInterval = 1.0f;
public override void Update(float deltaTime, Camera cam)
{
this.cam = cam;
if (!ForceUserToStayAttached) { UserInCorrectPosition = false; }
string signal = IsToggle && State ? output : falseOutput;
if (item.Connections != null && IsToggle && !string.IsNullOrEmpty(signal) && !IsOutOfPower())
{
item.SendSignal(signal, "signal_out");
item.SendSignal(signal, "trigger_out");
}
if (forceSelectNextFrame && User != null)
{
User.SelectedItem = item;
}
forceSelectNextFrame = false;
userCanInteractCheckTimer -= deltaTime;
if (User == null
|| User.Removed
|| (((User.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)
{
CancelUsing(User);
User = null;
}
if (item.Connections == null || !IsToggle || string.IsNullOrEmpty(signal)) { IsActive = false; }
return;
}
if (ForceUserToStayAttached)
{
teleportTransition = MathF.Min(teleportTransition + deltaTime * TeleportTransitionSpeed, 1f);
if (teleportTransition >= 1f)
{
// 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();
if (userPos != Vector2.Zero)
{
Vector2 diff = (item.WorldPosition + userPos) - User.WorldPosition;
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;
}
else
{
User.AnimController.TargetMovement = Vector2.Zero;
UserInCorrectPosition = true;
}
}
else
{
// 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))
{
diff.Y = 0.0f;
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && User != Character.Controlled)
{
if (Math.Abs(diff.X) > 20.0f)
{
//wait for the character to walk to the correct position
return;
}
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(
diff.X * 0.1f,
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;
return;
}
User.AnimController.TargetMovement = Vector2.Zero;
}
UserInCorrectPosition = true;
}
}
ApplyStatusEffects(ActionType.OnActive, deltaTime, User);
if (limbPositions.Count == 0) { return; }
User.AnimController.StartUsingItem();
if (User.SelectedItem != null)
{
User.AnimController.ResetPullJoints(l => l.IsLowerBody);
}
else
{
User.AnimController.ResetPullJoints();
}
if (dir != 0) { User.AnimController.TargetDir = dir; }
foreach (LimbPos lb in limbPositions)
{
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; }
// 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 (lb.AllowUsingLimb)
{
switch (lb.LimbType)
{
case LimbType.RightHand:
case LimbType.RightForearm:
case LimbType.RightArm:
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; }
break;
}
}
limb.Disabled = true;
Vector2 worldPosition = new Vector2(item.WorldRect.X, item.WorldRect.Y) + lb.Position * item.Scale;
Vector2 diff = worldPosition - limb.WorldPosition;
limb.PullJointEnabled = true;
limb.PullJointWorldAnchorB = limb.SimPosition + ConvertUnits.ToSimUnits(diff);
}
}
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 (userCanInteractCheckTimer <= 0.0f)
{
userCanInteractCheckTimer = UserCanInteractCheckInterval;
return User.CanInteractWith(item);
}
}
//we only do the actual check every UserCanInteractCheckInterval seconds
//can mean the component can stay selected for <1s after the user no longer has access to it
return true;
}
private double lastUsed;
public override bool Use(float deltaTime, Character activator = null)
{
if (activator != User)
{
return false;
}
if (User == null || User.Removed || !User.IsAnySelectedItem(item) || !User.CanInteractWith(item))
{
User = null;
return false;
}
if (IsOutOfPower()) { return false; }
ApplyStatusEffects(ActionType.OnUse, 1.0f, activator);
if (IsToggle && (activator == null || lastUsed < Timing.TotalTime - 0.1))
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
State = !State;
#if SERVER
item.CreateServerEvent(this);
#endif
}
}
else if (!string.IsNullOrEmpty(output))
{
item.SendSignal(new Signal(output, sender: User), "trigger_out");
}
lastUsed = Timing.TotalTime;
return true;
}
public override bool SecondaryUse(float deltaTime, Character character = null)
{
if (User != character)
{
return false;
}
if (User == null || character.Removed || !User.IsAnySelectedItem(item) || !character.CanInteractWith(item))
{
User = null;
return false;
}
if (character == null)
{
return false;
}
if (IsOutOfPower()) { return false; }
focusTarget = GetFocusTarget();
if (focusTarget == null)
{
Vector2 centerPos = new Vector2(item.WorldRect.Center.X, item.WorldRect.Center.Y);
Vector2 offset = character.CursorWorldPosition - centerPos;
offset.Y = -offset.Y;
targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset));
return false;
}
character.ViewTarget = focusTarget;
#if CLIENT
if (character == Character.Controlled && cam != null)
{
Lights.LightManager.ViewTarget = focusTarget;
cam.TargetPos = focusTarget.WorldPosition;
cam.OffsetAmount = MathHelper.Lerp(cam.OffsetAmount, (focusTarget as Item).Prefab.OffsetOnSelected * focusTarget.OffsetOnSelectedMultiplier, deltaTime * 10.0f);
HideHUDs(true);
}
#endif
if (!character.IsRemotePlayer || character.ViewTarget == focusTarget)
{
Vector2 centerPos = new Vector2(focusTarget.WorldRect.Center.X, focusTarget.WorldRect.Center.Y);
if (focusTarget.GetComponent<Turret>() is { } turret)
{
centerPos = new Vector2(focusTarget.WorldRect.X + turret.TransformedBarrelPos.X, focusTarget.WorldRect.Y - turret.TransformedBarrelPos.Y);
}
Vector2 offset = character.CursorWorldPosition - centerPos;
offset.Y = -offset.Y;
targetRotation = MathUtils.WrapAngleTwoPi(MathUtils.VectorToAngle(offset));
}
return true;
}
public bool IsOutOfPower()
{
if (!RequirePower) { return false; }
var powered = item.GetComponent<Powered>();
return powered == null || powered.Voltage < powered.MinVoltage;
}
public Item GetFocusTarget()
{
var positionOut = item.Connections?.Find(c => c.Name == "position_out");
if (positionOut == null) { return null; }
if (IsOutOfPower()) { return null; }
item.SendSignal(new Signal(MathHelper.ToDegrees(targetRotation).ToString("G", CultureInfo.InvariantCulture), sender: User), positionOut);
// Use ToList() snapshot for thread-safe iteration
var signalRecipients = item.LastSentSignalRecipients.ToList();
for (int i = signalRecipients.Count - 1; i >= 0; i--)
{
if (signalRecipients[i].Item.Condition <= 0.0f || signalRecipients[i].IsPower) { continue; }
if (signalRecipients[i].Item.Prefab.FocusOnSelected)
{
return signalRecipients[i].Item;
}
}
foreach (var recipientPanel in item.GetConnectedComponentsRecursive<ConnectionPanel>(positionOut, allowTraversingBackwards: false))
{
if (recipientPanel.Item.Condition <= 0.0f) { continue; }
if (recipientPanel.Item.Prefab.FocusOnSelected)
{
return recipientPanel.Item;
}
}
return null;
}
public override bool Pick(Character picker)
{
if (IsOutOfPower()) { return false; }
#if CLIENT
if (Screen.Selected == GameMain.SubEditorScreen) { return false; }
#endif
if (IsToggle)
{
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
{
State = !State;
#if SERVER
item.CreateServerEvent(this);
#endif
}
}
else if (!string.IsNullOrEmpty(output))
{
item.SendSignal(new Signal(output, sender: picker), "signal_out");
}
#if CLIENT
PlaySound(ActionType.OnUse, picker);
#endif
ApplyStatusEffects(ActionType.OnUse, 1f, picker);
return true;
}
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);
if (limb == null) { continue; }
limb.Disabled = false;
limb.PullJointEnabled = false;
}
//disable flipping for 0.5 seconds, because flipping the character when it's in a weird pose (e.g. lying in bed) can mess up the ragdoll
if (character.AnimController is HumanoidAnimController humanoidAnim)
{
humanoidAnim.LockFlipping(0.5f);
}
if (character.SelectedItem == item) { character.SelectedItem = null; }
if (character.SelectedSecondaryItem == item) { character.SelectedSecondaryItem = null; }
character.AnimController.StopUsingItem();
if (character == Character.Controlled)
{
HideHUDs(false);
}
#if SERVER
item.CreateServerEvent(this);
#endif
}
public override bool Select(Character activator)
{
if (activator == null || activator.Removed) { return false; }
if (Item.Condition <= 0.0f && !UpdateWhenInactive) { return false; }
if (UsableIn == UseEnvironment.Water && !activator.AnimController.InWater ||
UsableIn == UseEnvironment.Air && activator.AnimController.InWater)
{
return false;
}
// Someone already using the item
if (User != null && !User.Removed)
{
// Let the server handle the logic here
if (GameMain.NetworkMember is { IsClient: true })
{
return false;
}
// 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;
IsActive = true;
return true;
}
}
return AllowSelectingWhenSelectedByOther;
}
else if (AllowPuttingInOtherCharacters && CanPutSelectedCharacter(activator.SelectedCharacter))
{
// 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)
{
forceSelectNextFrame = true;
return false;
}
}
//allow the selection logic above to run when out of power, but disallow sending signals
if (IsOutOfPower()) { return false; }
if (!string.IsNullOrEmpty(output))
{
item.SendSignal(new Signal(output, sender: User), "signal_out");
}
return true;
}
/// <summary>
/// "Attached user" sticks to this item. Can be used for things such as clown crates and boarding pods.
/// </summary>
public bool IsAttachedUser(Character character)
{
return character != null && character == User && ForceUserToStayAttached;
}
public override void FlipX(bool relativeToSub)
{
if (dir != Direction.None)
{
dir = dir == Direction.Left ? Direction.Right : Direction.Left;
}
userPos.X = -UserPos.X;
FlipLimbPositions();
}
public override void FlipY(bool relativeToSub)
{
userPos.Y = -UserPos.Y;
for (int i = 0; i < limbPositions.Count; i++)
{
float diff = (item.Rect.Y + limbPositions[i].Position.Y) - item.Rect.Center.Y;
Vector2 flippedPos =
new Vector2(
limbPositions[i].Position.X,
item.Rect.Center.Y - diff - item.Rect.Y);
limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb);
}
}
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)
{
return SaveLimbPositions(base.Save(parentElement));
}
public override void Load(ContentXElement componentElement, bool usePrefabValues, IdRemap idRemap, bool isItemSwap)
{
base.Load(componentElement, usePrefabValues, idRemap, isItemSwap);
if (GameMain.GameSession?.GameMode?.Preset == GameModePreset.TestMode)
{
LoadLimbPositions(componentElement);
}
}
private XElement SaveLimbPositions(XElement element)
{
if (Screen.Selected == GameMain.SubEditorScreen)
{
if (item.FlippedX)
{
FlipLimbPositions();
}
// Don't save flipped positions.
foreach (var limbPos in limbPositions)
{
element.Add(new XElement("limbposition",
new XAttribute("limb", limbPos.LimbType),
new XAttribute("position", XMLExtensions.Vector2ToString(limbPos.Position)),
new XAttribute("allowusinglimb", limbPos.AllowUsingLimb)));
}
if (item.FlippedX)
{
FlipLimbPositions();
}
}
return element;
}
private void LoadLimbPositions(ContentXElement element)
{
limbPositions.Clear();
foreach (var subElement in element.Elements())
{
if (subElement.Name != "limbposition") { continue; }
string limbStr = subElement.GetAttributeString("limb", "");
if (!Enum.TryParse(subElement.GetAttribute("limb").Value, out LimbType limbType))
{
DebugConsole.ThrowError($"Error in item \"{item.Name}\" - {limbStr} is not a valid limb type.",
contentPackage: element.ContentPackage);
}
else
{
LimbPos limbPos = new LimbPos(limbType,
subElement.GetAttributeVector2("position", Vector2.Zero),
subElement.GetAttributeBool("allowusinglimb", false));
limbPositions.Add(limbPos);
if (!limbPos.AllowUsingLimb)
{
if (limbType == LimbType.RightHand || limbType == LimbType.RightForearm || limbType == LimbType.RightArm ||
limbType == LimbType.LeftHand || limbType == LimbType.LeftForearm || limbType == LimbType.LeftArm)
{
AllowAiming = false;
}
}
}
}
}
private void FlipLimbPositions()
{
for (int i = 0; i < limbPositions.Count; i++)
{
float diff = (item.Rect.X + limbPositions[i].Position.X * item.Scale) - item.Rect.Center.X;
Vector2 flippedPos =
new Vector2(
(item.Rect.Center.X - diff - item.Rect.X) / item.Scale,
limbPositions[i].Position.Y);
limbPositions[i] = new LimbPos(limbPositions[i].LimbType, flippedPos, limbPositions[i].AllowUsingLimb);
}
}
public override void OnItemLoaded()
{
if (item.FlippedX && NonInteractableWhenFlippedX)
{
item.NonInteractable = true;
}
else if (item.FlippedY && NonInteractableWhenFlippedY)
{
item.NonInteractable = true;
}
}
public override void Reset()
{
base.Reset();
LoadLimbPositions(originalElement);
if (item.FlippedX)
{
FlipLimbPositions();
}
}
}
}