9b35f6b23f
* Update bug-reports.yml * Fix modifyChatMessage hook * Add LuaCsSetup.Lua back for compatibility * Fix Game.AssignOnExecute having command arguments be passed as varargs instead of a table * Actually use the PackageId const everywhere we need to refer to our content package * Load languages files even if the package is disabled * Fix Hook.Remove not being implemented properly * - Changed event aliases to be case insensitive. * - Fixed assembly logging style. - Fixed double logging during execution. * Fix garbage network data being read by the game when reading LuaCs network messages * PackageId -> PackageName * Added caching toggle to PluginManagementService * Fix LuaCs initializing too late for singleplayer campaigns and rework the C# prompt to only show when enabling mods/joining server * Oops, fix NRE crash * Fix hide username in logs config not doing anything * Fix Cs prompt showing up more than one between rounds * Fix server host being prompted twice with the C# popup * Ignore our workshop packages from the game's dependency thing since it doesn't really make sense * Load console commands after executing and possible fix for the not console command permitted * Added fallback friendly name resolution for ModConfig assembly contents. * Register Voronoi2 stuff * Added configinfo null check to SettingBase.cs * Add safety check so this stops crashing when we look at it the wrong way * Fixed "Folder" attribute files not being found. * Keep the LuaCsConfig class laying around for compatibility, not sure anywhere in our code base (and shouldn't be) * Added fallback compilation for UseInternalsAwareAssembly if the publicized script compilation fails. * Added legacy overload of AddCommand for mod compat. * Added LoggerService to Lua env. Made ILoggerService compliant with LuaCsLogger API. * Changed csharp script compilation algorithm to be best effort. * Added "RunUnrestricted" mode for lua scripts that need to run outside of sandbox. * - Fixed networking sync vars failing to sync initially. - Fixed lua failing to differentiate overloads ISettingBase. * Add alias for human.CPRSuccess and human.CPRFailed * - Fixed up the settings menu. - Made SettingEntry throw an error if "Value" attribute is not found in XML. - Fixed saved values for settings sometimes not reloading after disabling and re-enabling a package. * Fix LuaCs net messages received during connection initialization to be read incorrectly, happened because we would reset the BitPosition in our harmony patch which would cause the message to be read incorrectly later * Allow reloadlua to force the state to running * New icon for settings and make the top left text more user friendly * Fix client.packages hook sending normal packages * Fixed OnUpdate() not passing in deltaTime instead of totalTime. * Missing diffs frombb21a09244* Added networking tests for configs. * Added missing diffs forf61f852a25. * Some tweaks to the text * Remove missing Value error, it should just use the default value if it's not specified * Fix UseInternalAccessName * Always purge cashes for plugin content on unloading. * Fix texture not multiple of 4 * v1.12.7.0 (Spring Update 2026 Hotfix 1) --------- Co-authored-by: Joonas Rikkonen <poe.regalis@gmail.com> Co-authored-by: Evil Factory <36804725+evilfactory@users.noreply.github.com> Co-authored-by: MapleWheels <njainanan@hotmail.com>
1108 lines
41 KiB
C#
1108 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;
|
|
|
|
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);
|
|
|
|
for (int i = item.LastSentSignalRecipients.Count - 1; i >= 0; i--)
|
|
{
|
|
if (item.LastSentSignalRecipients[i].Item.Condition <= 0.0f || item.LastSentSignalRecipients[i].IsPower) { continue; }
|
|
if (item.LastSentSignalRecipients[i].Item.Prefab.FocusOnSelected)
|
|
{
|
|
return item.LastSentSignalRecipients[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();
|
|
}
|
|
}
|
|
}
|
|
}
|