Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/SharedSource/Items/Components/Door.cs
Eero 46595b1399 WIP Make collections thread-safe and add safe iteration
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.
2025-12-28 04:59:56 +08:00

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);
}
}