Replaced static lists and dictionaries with thread-safe ConcurrentDictionary or ThreadLocal collections for various item components and systems. Updated all relevant code to use snapshots (ToArray, ToList) for safe iteration, and added helper methods for marking and clearing changed connections. These changes improve thread safety and prevent potential concurrency issues in multi-threaded scenarios.
870 lines
34 KiB
C#
870 lines
34 KiB
C#
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using FarseerPhysics.Dynamics;
|
|
#if CLIENT
|
|
using Barotrauma.Lights;
|
|
#endif
|
|
using Barotrauma.Extensions;
|
|
|
|
namespace Barotrauma.Items.Components
|
|
{
|
|
partial class Door : Pickable, IDrawableComponent, IServerSerializable
|
|
{
|
|
private static readonly ConcurrentDictionary<Door, byte> _doorDict = new ConcurrentDictionary<Door, byte>();
|
|
|
|
public static ICollection<Door> DoorList => _doorDict.Keys;
|
|
|
|
private Gap linkedGap;
|
|
private bool isOpen;
|
|
|
|
private float openState, lastOpenState;
|
|
private readonly Sprite doorSprite, weldedSprite, brokenSprite;
|
|
private readonly bool scaleBrokenSprite, fadeBrokenSprite;
|
|
private readonly bool autoOrientGap;
|
|
|
|
private bool isJammed;
|
|
public bool IsJammed
|
|
{
|
|
get { return isJammed; }
|
|
set
|
|
{
|
|
if (isJammed == value) { return; }
|
|
isJammed = value;
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private bool isStuck;
|
|
|
|
[Serialize(false, IsPropertySaveable.Yes, alwaysUseInstanceValues: true)]
|
|
public bool IsStuck
|
|
{
|
|
get { return isStuck; }
|
|
private set
|
|
{
|
|
if (isStuck == value) { return; }
|
|
isStuck = value;
|
|
#if SERVER
|
|
if (item.FullyInitialized)
|
|
{
|
|
item.CreateServerEvent(this);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public bool IgnoreSignals { get; private set; }
|
|
|
|
//how much "less stuck" partially doors get when opened
|
|
const float StuckReductionOnOpen = 30.0f;
|
|
|
|
private float resetPredictionTimer;
|
|
private float toggleCooldownTimer;
|
|
private Character lastUser;
|
|
|
|
private float damageSoundCooldown;
|
|
|
|
private double lastBrokenTime;
|
|
|
|
private Rectangle doorRect;
|
|
|
|
private bool isBroken;
|
|
|
|
public bool CanBeTraversed => !Impassable && (IsBroken || IsOpen);
|
|
|
|
public bool IsBroken
|
|
{
|
|
get { return isBroken; }
|
|
set
|
|
{
|
|
if (isBroken == value) { return; }
|
|
isBroken = value;
|
|
if (isBroken)
|
|
{
|
|
DisableBody();
|
|
}
|
|
else
|
|
{
|
|
EnableBody();
|
|
}
|
|
#if SERVER
|
|
item.CreateServerEvent(this);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public PhysicsBody Body { get; private set; }
|
|
|
|
//the fixture that's part of the submarine's collider (= fixture that things outside the sub can collide with if the door is outside hulls)
|
|
public Fixture OutsideSubmarineFixture;
|
|
|
|
private float RepairThreshold
|
|
{
|
|
get { return item.GetComponent<Repairable>() == null ? 0.0f : item.MaxCondition; }
|
|
}
|
|
|
|
public bool CanBeWelded = true;
|
|
|
|
private float stuck;
|
|
[Serialize(0.0f, IsPropertySaveable.Yes, description: "How badly stuck the door is (in percentages). If the percentage reaches 100, the door needs to be cut open to make it usable again.")]
|
|
public float Stuck
|
|
{
|
|
get { return stuck; }
|
|
set
|
|
{
|
|
if (isOpen || isBroken || !CanBeWelded) { return; }
|
|
stuck = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
//don't allow clients to make the door stuck unless the server says so (handled in ClientRead)
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (stuck <= 0.0f) { IsStuck = false; }
|
|
if (stuck >= 99.0f) { IsStuck = true; }
|
|
}
|
|
}
|
|
|
|
[Serialize(3.0f, IsPropertySaveable.Yes, description: "How quickly the door opens."), Editable]
|
|
public float OpeningSpeed { get; private set; }
|
|
|
|
[Serialize(3.0f, IsPropertySaveable.Yes, description: "How quickly the door closes."), Editable]
|
|
public float ClosingSpeed { get; private set; }
|
|
|
|
[Serialize(1.0f, IsPropertySaveable.Yes, description: "The door cannot be opened/closed during this time after it has been opened/closed by another character."), Editable]
|
|
public float ToggleCoolDown { get; private set; }
|
|
|
|
public bool? PredictedState { get; private set; }
|
|
|
|
public Gap LinkedGap
|
|
{
|
|
get
|
|
{
|
|
if (linkedGap == null)
|
|
{
|
|
GetLinkedGap();
|
|
}
|
|
return linkedGap;
|
|
}
|
|
}
|
|
|
|
private void GetLinkedGap()
|
|
{
|
|
linkedGap = item.linkedTo.FirstOrDefault(e => e is Gap) as Gap;
|
|
if (linkedGap == null)
|
|
{
|
|
Rectangle rect = item.Rect;
|
|
linkedGap = new Gap(rect, !IsHorizontal, Item.Submarine)
|
|
{
|
|
Submarine = item.Submarine
|
|
};
|
|
item.linkedTo.Add(linkedGap);
|
|
}
|
|
RefreshLinkedGap();
|
|
}
|
|
|
|
public bool IsHorizontal { get; private set; }
|
|
|
|
public bool IsConvexHullHorizontal => autoOrientGap && linkedGap != null ? !linkedGap.IsHorizontal : IsHorizontal;
|
|
|
|
[Serialize("0.0,0.0,0.0,0.0", IsPropertySaveable.No, description: "Position and size of the window on the door. The upper left corner is 0,0. Set the width and height to 0 if you don't want the door to have a window.")]
|
|
public Rectangle Window { get; set; }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.Yes, description: "Is the door currently open.")]
|
|
public bool IsOpen
|
|
{
|
|
get { return isOpen; }
|
|
set
|
|
{
|
|
isOpen = value;
|
|
OpenState = isOpen ? 1.0f : 0.0f;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can be used by status effects to tell the door to open (setting IsOpen directly would make it immediately fully open)
|
|
/// </summary>
|
|
public bool ShouldBeOpen
|
|
{
|
|
get { return isOpen; }
|
|
set
|
|
{
|
|
if (isOpen != value)
|
|
{
|
|
ToggleState(ActionType.OnUse, user: null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsClosed => !IsOpen;
|
|
|
|
public bool IsFullyOpen => IsOpen && OpenState >= 1.0f;
|
|
|
|
public bool IsFullyClosed => IsClosed && OpenState <= 0f;
|
|
|
|
public bool HasWindow => Window != Rectangle.Empty;
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "If the door has integrated buttons, it can be opened by interacting with it directly (instead of using buttons wired to it).")]
|
|
public bool HasIntegratedButtons { get; private set; }
|
|
|
|
[ConditionallyEditable(ConditionallyEditable.ConditionType.HasIntegratedButtons),
|
|
Serialize(true, IsPropertySaveable.No, description: "If the door has integrated buttons, should clicking on it perform the default action of opening the door? Can be used in conjunction with the \"activate_out\" output to pass a signal to a circuit without toggling the door when someone tries to open/close the door.")]
|
|
public bool ToggleWhenClicked { get; private set; }
|
|
|
|
public float OpenState
|
|
{
|
|
get { return openState; }
|
|
set
|
|
{
|
|
lastOpenState = openState;
|
|
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
|
|
#if CLIENT
|
|
float size = IsHorizontal ? item.Rect.Width : item.Rect.Height;
|
|
//refresh convex hulls if the body of the door has moved by 5 pixels,
|
|
//or if it becomes fully closed or fully open
|
|
if (Math.Abs(lastConvexHullState - openState) * size > 5.0f ||
|
|
(openState <= 0.0f && lastConvexHullState > 0.0f) ||
|
|
(openState >= 1.0f && lastConvexHullState < 1.0f))
|
|
{
|
|
UpdateConvexHulls();
|
|
lastConvexHullState = openState;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
[Serialize(false, IsPropertySaveable.No, description: "Characters and items cannot pass through impassable doors. Useful for things such as ducts that should only let water and air through.")]
|
|
public bool Impassable
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
[Editable, Serialize(true, IsPropertySaveable.Yes, description: "", alwaysUseInstanceValues: true)]
|
|
public bool UseBetweenOutpostModules { get; private set; }
|
|
|
|
[Editable, Serialize(false, IsPropertySaveable.No, description: "If true, bots won't try to close this door behind them.", alwaysUseInstanceValues: true)]
|
|
public bool BotsShouldKeepOpen { get; private set; }
|
|
|
|
public Door(Item item, ContentXElement element)
|
|
: base(item, element)
|
|
{
|
|
IsHorizontal = element.GetAttributeBool("horizontal", false);
|
|
canBePicked = element.GetAttributeBool("canbepicked", false);
|
|
autoOrientGap = element.GetAttributeBool("autoorientgap", false);
|
|
|
|
allowedSlots.Clear();
|
|
|
|
foreach (var subElement in element.Elements())
|
|
{
|
|
string textureDir = GetTextureDirectory(subElement);
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "sprite":
|
|
doorSprite = new Sprite(subElement, path: textureDir);
|
|
break;
|
|
case "weldedsprite":
|
|
weldedSprite = new Sprite(subElement, path: textureDir);
|
|
break;
|
|
case "brokensprite":
|
|
brokenSprite = new Sprite(subElement, path: textureDir);
|
|
scaleBrokenSprite = subElement.GetAttributeBool("scale", false);
|
|
fadeBrokenSprite = subElement.GetAttributeBool("fade", false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
IsActive = true;
|
|
_doorDict.TryAdd(this, 0);
|
|
}
|
|
|
|
public override void OnItemLoaded()
|
|
{
|
|
//do this here because the scale of the item might not be set to the final value yet in the constructor
|
|
doorRect = new Rectangle(
|
|
item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale),
|
|
item.Rect.Y - item.Rect.Height / 2 + (int)(doorSprite.size.Y / 2.0f * item.Scale),
|
|
(int)(doorSprite.size.X * item.Scale),
|
|
(int)(doorSprite.size.Y * item.Scale));
|
|
|
|
Body = new PhysicsBody(
|
|
ConvertUnits.ToSimUnits(Math.Max(doorRect.Width, 1)),
|
|
ConvertUnits.ToSimUnits(Math.Max(doorRect.Height, 1)),
|
|
radius: 0.0f,
|
|
density: 1.5f,
|
|
BodyType.Static,
|
|
Physics.CollisionWall,
|
|
Physics.CollisionCharacter | Physics.CollisionItem | Physics.CollisionCharacter | Physics.CollisionItemBlocking | Physics.CollisionProjectile,
|
|
findNewContacts: false)
|
|
{
|
|
UserData = item,
|
|
Friction = 0.5f
|
|
};
|
|
Body.SetTransformIgnoreContacts(
|
|
ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)),
|
|
0.0f);
|
|
if (isBroken)
|
|
{
|
|
DisableBody();
|
|
}
|
|
}
|
|
|
|
public override void Move(Vector2 amount, bool ignoreContacts = false)
|
|
{
|
|
if (ignoreContacts)
|
|
{
|
|
Body?.SetTransformIgnoreContacts(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
|
|
}
|
|
else
|
|
{
|
|
Body?.SetTransform(Body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
|
|
}
|
|
#if CLIENT
|
|
UpdateConvexHulls();
|
|
#endif
|
|
}
|
|
|
|
private readonly LocalizedString accessDeniedTxt = TextManager.Get("AccessDenied");
|
|
private readonly LocalizedString cannotOpenText = TextManager.Get("DoorMsgCannotOpen");
|
|
public override bool HasRequiredItems(Character character, bool addMessage, LocalizedString msg = null)
|
|
{
|
|
if (IsBroken)
|
|
{
|
|
return false;
|
|
}
|
|
if (isOpen)
|
|
{
|
|
Msg = HasAccess(character) ? "ItemMsgClose" : "ItemMsgForceCloseCrowbar";
|
|
}
|
|
else
|
|
{
|
|
Msg = HasAccess(character) ? "ItemMsgOpen" : "ItemMsgForceOpenCrowbar";
|
|
}
|
|
ParseMsg();
|
|
if (addMessage)
|
|
{
|
|
msg ??= (HasIntegratedButtons ? accessDeniedTxt : cannotOpenText).Value;
|
|
}
|
|
return base.HasRequiredItems(character, addMessage, msg);
|
|
}
|
|
|
|
public override bool Pick(Character picker)
|
|
{
|
|
if (item.Condition < RepairThreshold && item.GetComponent<Repairable>().HasRequiredItems(picker, addMessage: false)) { return true; }
|
|
if (RequiredItems.None()) { return false; }
|
|
if (HasAccess(picker) && HasRequiredItems(picker, false)) { return false; }
|
|
return base.Pick(picker);
|
|
}
|
|
|
|
public override bool OnPicked(Character picker)
|
|
{
|
|
if (item.Condition < RepairThreshold && item.GetComponent<Repairable>().HasRequiredItems(picker, addMessage: false)) { return true; }
|
|
if (!HasAccess(picker))
|
|
{
|
|
ToggleState(ActionType.OnPicked, picker);
|
|
ApplyStatusEffects(ActionType.OnPicked, 1.0f, picker);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private void ToggleState(ActionType actionType, Character user)
|
|
{
|
|
if (toggleCooldownTimer > 0.0f && user != lastUser)
|
|
{
|
|
OnFailedToOpen();
|
|
return;
|
|
}
|
|
if (ToggleWhenClicked)
|
|
{
|
|
//do not activate cooldown at this point if the door doesn't get toggled when clicked
|
|
//(i.e. if it just sends out a signal that might get passed back to the door and try to toggle it)
|
|
toggleCooldownTimer = ToggleCoolDown;
|
|
}
|
|
if (IsStuck || IsJammed)
|
|
{
|
|
#if CLIENT
|
|
if (IsStuck) { HintManager.OnTryOpenStuckDoor(user); }
|
|
#endif
|
|
toggleCooldownTimer = 1.0f;
|
|
OnFailedToOpen();
|
|
return;
|
|
}
|
|
item.SendSignal("1", "activate_out");
|
|
lastUser = user;
|
|
if (ToggleWhenClicked)
|
|
{
|
|
SetState(PredictedState == null ? !isOpen : !PredictedState.Value, false, true, forcedOpen: actionType == ActionType.OnPicked);
|
|
}
|
|
}
|
|
|
|
public override bool Select(Character character)
|
|
{
|
|
if (isBroken) { return true; }
|
|
bool hasRequiredItems = HasRequiredItems(character, false);
|
|
if (HasAccess(character))
|
|
{
|
|
float originalPickingTime = PickingTime;
|
|
PickingTime = 0;
|
|
ToggleState(ActionType.OnUse, character);
|
|
PickingTime = originalPickingTime;
|
|
StopPicking(picker);
|
|
return true;
|
|
}
|
|
#if CLIENT
|
|
else if (hasRequiredItems && character != null && character == Character.Controlled)
|
|
{
|
|
GUI.AddMessage(accessDeniedTxt, GUIStyle.Red);
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the given position inside the vertical bounds of the window, and roughly on the door horizontally? Or the other way around if the door opens horizontally.
|
|
/// </summary>
|
|
/// <param name="position">Position in the same coordinate space as the door.</param>
|
|
/// <param name="maxPerpendicularDistance">Maximum horizontal distance from the door (or vertical if the door opens horizontally)</param>
|
|
public bool IsPositionOnWindow(Vector2 position, float maxPerpendicularDistance = 10.0f)
|
|
{
|
|
if (IsHorizontal)
|
|
{
|
|
return
|
|
position.X >= item.Rect.X + Window.X &&
|
|
position.X <= item.Rect.X + Window.X + Window.Width &&
|
|
position.Y >= item.Rect.Y - maxPerpendicularDistance &&
|
|
position.Y <= item.Rect.Y - item.Rect.Height - maxPerpendicularDistance;
|
|
}
|
|
else
|
|
{
|
|
return
|
|
position.Y >= item.Rect.Y + Window.Y &&
|
|
position.Y <= item.Rect.Y + Window.Y + Window.Height &&
|
|
position.X >= item.Rect.X - maxPerpendicularDistance &&
|
|
position.X <= item.Rect.Right + maxPerpendicularDistance;
|
|
}
|
|
}
|
|
|
|
public override void Update(float deltaTime, Camera cam)
|
|
{
|
|
UpdateProjSpecific(deltaTime);
|
|
toggleCooldownTimer -= deltaTime;
|
|
damageSoundCooldown -= deltaTime;
|
|
|
|
if (isBroken)
|
|
{
|
|
lastBrokenTime = Timing.TotalTime;
|
|
//the door has to be restored to 50% health before collision detection on the body is re-enabled
|
|
|
|
//multiply by MaxRepairConditionMultiplier so the item gets repaired at 50% of the _default max condition_
|
|
//otherwise increasing the max condition is arguably harmful, as the door needs to be repaired further to re-enable the collider
|
|
if (item.ConditionPercentage * Math.Max(item.MaxRepairConditionMultiplier, 1.0f) > 50.0f &&
|
|
(GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer))
|
|
{
|
|
IsBroken = false;
|
|
}
|
|
return;
|
|
}
|
|
|
|
bool isClosing = false;
|
|
if ((!IsStuck && !IsJammed) || !isOpen)
|
|
{
|
|
if (PredictedState == null)
|
|
{
|
|
OpenState += deltaTime * (isOpen ? OpeningSpeed : -ClosingSpeed);
|
|
isClosing = openState is > 0.0f and < 1.0f && !isOpen;
|
|
}
|
|
else
|
|
{
|
|
OpenState += deltaTime * (PredictedState.Value ? OpeningSpeed : -ClosingSpeed);
|
|
isClosing = openState is > 0.0f and < 1.0f && !PredictedState.Value;
|
|
|
|
resetPredictionTimer -= deltaTime;
|
|
if (resetPredictionTimer <= 0.0f)
|
|
{
|
|
PredictedState = null;
|
|
}
|
|
}
|
|
LinkedGap.Open = isBroken ? 1.0f : openState;
|
|
}
|
|
|
|
if (isClosing)
|
|
{
|
|
//server gives the clients more leeway on moving through closing doors
|
|
//latency can often otherwise make a client get blocked by a closing door server-side even if it seemed like they made it through client-side
|
|
float pushCharactersAwayThreshold = GameMain.NetworkMember is { IsServer: true } ? 0.1f : 0.9f;
|
|
|
|
if (OpenState < pushCharactersAwayThreshold) { PushCharactersAway(); }
|
|
if (CheckSubmarinesInDoorWay())
|
|
{
|
|
PredictedState = null;
|
|
isOpen = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bool wasEnabled = Body.Enabled;
|
|
Body.Enabled = Impassable || openState < 1.0f;
|
|
if (OutsideSubmarineFixture != null)
|
|
{
|
|
OutsideSubmarineFixture.CollidesWith = Body.Enabled ? SubmarineBody.CollidesWith : Category.None;
|
|
}
|
|
if (wasEnabled && !Body.Enabled && IsHorizontal)
|
|
{
|
|
//when opening a hatch, force characters above it to refresh the floor position
|
|
//(otherwise the character won't fall through the hatch until it moves)
|
|
foreach (Character c in Character.CharacterList)
|
|
{
|
|
if (c.WorldPosition.Y < item.WorldPosition.Y) { continue; }
|
|
if (c.WorldPosition.X < item.WorldRect.X || c.WorldPosition.X > item.WorldRect.Right) { continue; }
|
|
c.AnimController?.ForceRefreshFloorY();
|
|
}
|
|
}
|
|
}
|
|
|
|
//don't use the predicted state here, because it might set
|
|
//other items to an incorrect state if the prediction is wrong
|
|
item.SendSignal(isOpen ? "1" : "0", "state_out");
|
|
}
|
|
|
|
partial void UpdateProjSpecific(float deltaTime);
|
|
|
|
|
|
public override void UpdateBroken(float deltaTime, Camera cam)
|
|
{
|
|
base.UpdateBroken(deltaTime, cam);
|
|
if (GameMain.NetworkMember == null || GameMain.NetworkMember.IsServer)
|
|
{
|
|
IsBroken = true;
|
|
}
|
|
}
|
|
|
|
private void EnableBody()
|
|
{
|
|
if (!Impassable)
|
|
{
|
|
Body.FarseerBody.SetIsSensor(false);
|
|
var ce = Body.FarseerBody.ContactList;
|
|
while (ce != null && ce.Contact != null)
|
|
{
|
|
ce.Contact.Enabled = false;
|
|
ce = ce.Next;
|
|
}
|
|
PushCharactersAway();
|
|
}
|
|
if (OutsideSubmarineFixture != null && Body.Enabled)
|
|
{
|
|
OutsideSubmarineFixture.CollidesWith = SubmarineBody.CollidesWith;
|
|
}
|
|
#if CLIENT
|
|
UpdateConvexHulls();
|
|
#endif
|
|
isBroken = false;
|
|
}
|
|
|
|
private void DisableBody()
|
|
{
|
|
//change the body to a sensor instead of disabling it completely,
|
|
//because otherwise repairtool raycasts won't hit it
|
|
if (!Impassable)
|
|
{
|
|
Body.FarseerBody.SetIsSensor(true);
|
|
var ce = Body.FarseerBody.ContactList;
|
|
while (ce != null && ce.Contact != null)
|
|
{
|
|
ce.Contact.Enabled = false;
|
|
ce = ce.Next;
|
|
}
|
|
}
|
|
|
|
if (OutsideSubmarineFixture != null)
|
|
{
|
|
OutsideSubmarineFixture.CollidesWith = Category.None;
|
|
}
|
|
if (linkedGap != null)
|
|
{
|
|
linkedGap.Open = 1.0f;
|
|
}
|
|
|
|
IsOpen = false;
|
|
#if CLIENT
|
|
if (convexHull != null) { convexHull.Enabled = false; }
|
|
if (convexHull2 != null) { convexHull2.Enabled = false; }
|
|
#endif
|
|
}
|
|
|
|
public void RefreshLinkedGap()
|
|
{
|
|
LinkedGap.Layer = item.Layer;
|
|
LinkedGap.ConnectedDoor = this;
|
|
if (autoOrientGap)
|
|
{
|
|
LinkedGap.AutoOrient();
|
|
}
|
|
LinkedGap.Open = isBroken ? 1.0f : openState;
|
|
LinkedGap.PassAmbientLight = Window != Rectangle.Empty;
|
|
}
|
|
|
|
public override void OnMapLoaded()
|
|
{
|
|
RefreshLinkedGap();
|
|
#if CLIENT
|
|
convexHull = new ConvexHull(doorRect, IsConvexHullHorizontal, item);
|
|
if (Window != Rectangle.Empty)
|
|
{
|
|
convexHull2 = new ConvexHull(doorRect, IsConvexHullHorizontal, item);
|
|
}
|
|
UpdateConvexHulls();
|
|
#endif
|
|
}
|
|
|
|
public override void OnScaleChanged()
|
|
{
|
|
#if CLIENT
|
|
UpdateConvexHulls();
|
|
#endif
|
|
if (linkedGap != null)
|
|
{
|
|
RefreshLinkedGap();
|
|
linkedGap.Rect = item.Rect;
|
|
}
|
|
}
|
|
|
|
protected override void RemoveComponentSpecific()
|
|
{
|
|
base.RemoveComponentSpecific();
|
|
|
|
if (Body != null)
|
|
{
|
|
Body.Remove();
|
|
Body = null;
|
|
}
|
|
|
|
foreach (Gap gap in Gap.GapList)
|
|
{
|
|
if (gap.ConnectedDoor == this)
|
|
{
|
|
gap.ConnectedDoor = null;
|
|
}
|
|
}
|
|
|
|
if (OutsideSubmarineFixture != null)
|
|
{
|
|
OutsideSubmarineFixture.Body.Remove(OutsideSubmarineFixture);
|
|
OutsideSubmarineFixture = null;
|
|
}
|
|
|
|
//no need to remove the gap if we're unloading the whole submarine
|
|
//otherwise the gap will be removed twice and cause console warnings
|
|
if (!Submarine.Unloading)
|
|
{
|
|
linkedGap?.Remove();
|
|
}
|
|
doorSprite?.Remove();
|
|
weldedSprite?.Remove();
|
|
|
|
#if CLIENT
|
|
convexHull?.Remove();
|
|
convexHull2?.Remove();
|
|
#endif
|
|
|
|
_doorDict.TryRemove(this, out _);
|
|
}
|
|
|
|
private bool CheckSubmarinesInDoorWay()
|
|
{
|
|
if (linkedGap != null && linkedGap.IsRoomToRoom) { return false; }
|
|
|
|
Rectangle doorRect = item.WorldRect;
|
|
if (IsHorizontal)
|
|
{
|
|
doorRect.Width = (int)(item.Rect.Width * (1.0f - openState));
|
|
}
|
|
else
|
|
{
|
|
doorRect.Height = (int)(item.Rect.Height * (1.0f - openState));
|
|
}
|
|
|
|
foreach (Submarine sub in Submarine.Loaded)
|
|
{
|
|
if (sub == item.Submarine || sub.DockedTo.Contains(item.Submarine)) { continue; }
|
|
Rectangle worldBorders = sub.Borders;
|
|
worldBorders.Location += sub.WorldPosition.ToPoint();
|
|
if (!Submarine.RectsOverlap(worldBorders, doorRect)) { continue; }
|
|
foreach (Hull hull in sub.GetHulls(alsoFromConnectedSubs: false))
|
|
{
|
|
if (Submarine.RectsOverlap(hull.WorldRect, doorRect)) { return true; }
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool itemPosErrorShown;
|
|
private readonly HashSet<Character> characterPosErrorShown = new HashSet<Character>();
|
|
private void PushCharactersAway()
|
|
{
|
|
if (!MathUtils.IsValid(item.SimPosition))
|
|
{
|
|
if (!itemPosErrorShown)
|
|
{
|
|
DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ")");
|
|
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:DoorPosInvalid", GameAnalyticsManager.ErrorSeverity.Error,
|
|
"Failed to push a character out of a doorway - position of the door is not valid (" + item.SimPosition + ").");
|
|
itemPosErrorShown = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
Vector2 simPos = ConvertUnits.ToSimUnits(new Vector2(item.Rect.X, item.Rect.Y));
|
|
|
|
Vector2 currSize = IsHorizontal ?
|
|
new Vector2(item.Rect.Width * (1.0f - openState), doorSprite.size.Y * item.Scale) :
|
|
new Vector2(doorSprite.size.X * item.Scale, item.Rect.Height * (1.0f - openState));
|
|
Vector2 simSize = ConvertUnits.ToSimUnits(currSize);
|
|
|
|
foreach (Character c in Character.CharacterList)
|
|
{
|
|
if (!c.Enabled) { continue; }
|
|
if (c.SelectedItem?.GetComponent<Controller>() is { } controller && controller.IsAttachedUser(c)) { continue; }
|
|
if (!MathUtils.IsValid(c.SimPosition))
|
|
{
|
|
if (!characterPosErrorShown.Contains(c))
|
|
{
|
|
if (GameSettings.CurrentConfig.VerboseLogging) { DebugConsole.ThrowError("Failed to push a character out of a doorway - position of the character \"" + c.Name + "\" is not valid (" + c.SimPosition + ")"); }
|
|
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:CharacterPosInvalid", GameAnalyticsManager.ErrorSeverity.Error,
|
|
"Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + c.SimPosition + ")." +
|
|
" Removed: " + c.Removed +
|
|
" Remoteplayer: " + c.IsRemotePlayer);
|
|
characterPosErrorShown.Add(c);
|
|
}
|
|
continue;
|
|
}
|
|
int dir = IsHorizontal ? Math.Sign(c.SimPosition.Y - item.SimPosition.Y) : Math.Sign(c.SimPosition.X - item.SimPosition.X);
|
|
|
|
foreach (Limb limb in c.AnimController.Limbs)
|
|
{
|
|
if (limb.IsSevered) { continue; }
|
|
if (PushBodyOutOfDoorway(c, limb.body, dir, simPos, simSize) && damageSoundCooldown <= 0.0f)
|
|
{
|
|
#if CLIENT
|
|
SoundPlayer.PlayDamageSound("LimbBlunt", 1.0f, limb.body);
|
|
#endif
|
|
damageSoundCooldown = 0.5f;
|
|
}
|
|
}
|
|
PushBodyOutOfDoorway(c, c.AnimController.Collider, dir, simPos, simSize);
|
|
}
|
|
}
|
|
|
|
private bool PushBodyOutOfDoorway(Character c, PhysicsBody body, int dir, Vector2 doorRectSimPos, Vector2 doorRectSimSize)
|
|
{
|
|
if (!MathUtils.IsValid(body.SimPosition))
|
|
{
|
|
DebugConsole.ThrowError("Failed to push a limb out of a doorway - position of the body (character \"" + c.Name + "\") is not valid (" + body.SimPosition + ")");
|
|
GameAnalyticsManager.AddErrorEventOnce("PushCharactersAway:LimbPosInvalid", GameAnalyticsManager.ErrorSeverity.Error,
|
|
"Failed to push a character out of a doorway - position of the character \"" + c.SpeciesName + "\" is not valid (" + body.SimPosition + ")." +
|
|
" Removed: " + c.Removed +
|
|
" Remoteplayer: " + c.IsRemotePlayer);
|
|
return false;
|
|
}
|
|
|
|
float diff;
|
|
if (IsHorizontal)
|
|
{
|
|
if (body.SimPosition.X < doorRectSimPos.X || body.SimPosition.X > doorRectSimPos.X + doorRectSimSize.X) { return false; }
|
|
diff = body.SimPosition.Y - item.SimPosition.Y;
|
|
}
|
|
else
|
|
{
|
|
if (body.SimPosition.Y > doorRectSimPos.Y || body.SimPosition.Y < doorRectSimPos.Y - doorRectSimSize.Y) { return false; }
|
|
diff = body.SimPosition.X - item.SimPosition.X;
|
|
}
|
|
|
|
//if the limb is at a different side of the door than the character (collider),
|
|
//immediately teleport it to the correct side
|
|
if (Math.Sign(diff) != dir)
|
|
{
|
|
if (IsHorizontal)
|
|
{
|
|
body.SetTransformIgnoreContacts(new Vector2(body.SimPosition.X, item.SimPosition.Y + dir * doorRectSimSize.Y * 2.0f), body.Rotation);
|
|
}
|
|
else
|
|
{
|
|
body.SetTransformIgnoreContacts(new Vector2(item.SimPosition.X + dir * doorRectSimSize.X * 1.2f, body.SimPosition.Y), body.Rotation);
|
|
}
|
|
}
|
|
|
|
//apply an impulse to push the limb further from the door
|
|
if (IsHorizontal)
|
|
{
|
|
if (Math.Abs(body.SimPosition.Y - item.SimPosition.Y) > doorRectSimSize.Y * 0.5f) { return false; }
|
|
body.ApplyLinearImpulse(new Vector2(isOpen ? 0.0f : 1.0f, dir * 2.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
}
|
|
else
|
|
{
|
|
if (Math.Abs(body.SimPosition.X - item.SimPosition.X) > doorRectSimSize.X * 0.5f) { return false; }
|
|
body.ApplyLinearImpulse(new Vector2(dir * 2.0f, isOpen ? 0.0f : -1.0f), maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
}
|
|
|
|
//don't stun if the door was broken a moment ago
|
|
//otherwise enabling the door's collider and pushing the character away will interrupt repairing
|
|
if (lastBrokenTime < Timing.TotalTime - 1.0f)
|
|
{
|
|
c.SetStun(0.2f);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
partial void OnFailedToOpen();
|
|
|
|
public override bool HasAccess(Character character)
|
|
{
|
|
if (!item.IsInteractable(character)) { return false; }
|
|
if (!base.HasAccess(character)) { return false; }
|
|
if (HasIntegratedButtons) { return true; }
|
|
var buttons = Item.GetConnectedComponents<Controller>(recursive: true);
|
|
// If there are no buttons, and we can access the door, treat it accessible. Might be controlled by some mechanism, such as motion sensor.
|
|
return buttons.None() || buttons.Any(b => b.HasAccess(character));
|
|
}
|
|
|
|
public override void ReceiveSignal(Signal signal, Connection connection)
|
|
{
|
|
if (IsStuck || IsJammed || IgnoreSignals) { return; }
|
|
|
|
bool wasOpen = PredictedState == null ? isOpen : PredictedState.Value;
|
|
|
|
if (connection.Name == "toggle")
|
|
{
|
|
if (signal.value == "0") { return; }
|
|
if (toggleCooldownTimer > 0.0f && signal.sender != lastUser) { OnFailedToOpen(); return; }
|
|
if (IsStuck) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
|
|
toggleCooldownTimer = ToggleCoolDown;
|
|
lastUser = signal.sender;
|
|
SetState(!wasOpen, false, true, forcedOpen: false);
|
|
}
|
|
else if (connection.Name == "set_state")
|
|
{
|
|
bool signalOpen = signal.value != "0";
|
|
if (IsStuck && signalOpen != wasOpen) { toggleCooldownTimer = 1.0f; OnFailedToOpen(); return; }
|
|
SetState(signalOpen, false, true, forcedOpen: false);
|
|
}
|
|
|
|
#if SERVER
|
|
if (signal.sender != null && wasOpen != isOpen)
|
|
{
|
|
GameServer.Log(GameServer.CharacterLogName(signal.sender) + (isOpen ? " opened " : " closed ") + item.Name, ServerLog.MessageType.ItemInteraction);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public void TrySetState(bool open, bool isNetworkMessage, bool sendNetworkMessage = false)
|
|
{
|
|
SetState(open, isNetworkMessage, sendNetworkMessage, forcedOpen: false);
|
|
}
|
|
|
|
partial void SetState(bool open, bool isNetworkMessage, bool sendNetworkMessage, bool forcedOpen);
|
|
}
|
|
}
|