diff --git a/Content.Server/GameObjects/Components/Buckle/BuckleComponent.cs b/Content.Server/GameObjects/Components/Buckle/BuckleComponent.cs index 11cf32432a..46eb6d3b65 100644 --- a/Content.Server/GameObjects/Components/Buckle/BuckleComponent.cs +++ b/Content.Server/GameObjects/Components/Buckle/BuckleComponent.cs @@ -273,6 +273,8 @@ namespace Content.Server.GameObjects.Components.Buckle ReAttach(strap); BuckleStatus(); + SendMessage(new BuckleMessage(Owner, to)); + return true; } @@ -317,19 +319,13 @@ namespace Content.Server.GameObjects.Components.Buckle } } - if (BuckledTo.Owner.TryGetComponent(out StrapComponent strap)) - { - strap.Remove(this); - _entitySystem.GetEntitySystem() - .PlayFromEntity(strap.UnbuckleSound, Owner); - } - if (Owner.Transform.Parent == BuckledTo.Owner.Transform) { ContainerHelpers.AttachParentToContainerOrGrid(Owner.Transform); Owner.Transform.WorldRotation = BuckledTo.Owner.Transform.WorldRotation; } + var oldBuckledTo = BuckledTo; BuckledTo = null; if (Owner.TryGetComponent(out AppearanceComponent appearance)) @@ -353,6 +349,15 @@ namespace Content.Server.GameObjects.Components.Buckle BuckleStatus(); + if (oldBuckledTo.Owner.TryGetComponent(out StrapComponent strap)) + { + strap.Remove(this); + _entitySystem.GetEntitySystem() + .PlayFromEntity(strap.UnbuckleSound, Owner); + } + + SendMessage(new UnbuckleMessage(Owner, oldBuckledTo.Owner)); + return true; } diff --git a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs index 9083ac09af..b066d162ed 100644 --- a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs @@ -1,4 +1,5 @@ -using Content.Server.GameObjects.Components.Observer; +#nullable enable +using Content.Server.GameObjects.Components.Observer; using Content.Server.GameObjects.EntitySystems.Click; using Content.Server.Interfaces.GameTicking; using Content.Server.Mobs; @@ -21,7 +22,7 @@ namespace Content.Server.GameObjects.Components.Mobs [RegisterComponent] public class MindComponent : Component, IExamine { - private bool _showExamineInfo = false; + private bool _showExamineInfo; /// public override string Name => "Mind"; @@ -30,7 +31,7 @@ namespace Content.Server.GameObjects.Components.Mobs /// The mind controlling this mob. Can be null. /// [ViewVariables] - public Mind Mind { get; private set; } + public Mind? Mind { get; private set; } /// /// True if we have a mind, false otherwise. @@ -74,7 +75,7 @@ namespace Content.Server.GameObjects.Components.Mobs if (HasMind) { - var visiting = Mind.VisitingEntity; + var visiting = Mind?.VisitingEntity; if (visiting != null) { if (visiting.TryGetComponent(out GhostComponent ghost)) @@ -82,7 +83,7 @@ namespace Content.Server.GameObjects.Components.Mobs ghost.CanReturnToBody = false; } - Mind.TransferTo(visiting); + Mind!.TransferTo(visiting); } else { @@ -98,11 +99,14 @@ namespace Content.Server.GameObjects.Components.Mobs } var ghost = Owner.EntityManager.SpawnEntity("MobObserver", spawnPosition); - ghost.Name = Mind.CharacterName; - var ghostComponent = ghost.GetComponent(); ghostComponent.CanReturnToBody = false; - Mind.TransferTo(ghost); + + if (Mind != null) + { + ghost.Name = Mind.CharacterName; + Mind.TransferTo(ghost); + } }); } } @@ -117,20 +121,24 @@ namespace Content.Server.GameObjects.Components.Mobs public void Examine(FormattedMessage message, bool inDetailsRange) { if (!ShowExamineInfo || !inDetailsRange) + { return; + } - var dead = false; + var dead = + Owner.TryGetComponent(out var species) && + species.CurrentDamageState is DeadState; - if(Owner.TryGetComponent(out var species)) - if (species.CurrentDamageState is DeadState) - dead = true; - - if(!HasMind) + if (!HasMind) + { message.AddMarkup(!dead ? $"[color=red]" + Loc.GetString("{0:They} are totally catatonic. The stresses of life in deep-space must have been too much for {0:them}. Any recovery is unlikely.", Owner) + "[/color]" : $"[color=purple]" + Loc.GetString("{0:Their} soul has departed.", Owner) + "[/color]"); - else if (Mind.Session == null) + } + else if (Mind?.Session == null) + { message.AddMarkup("[color=yellow]" + Loc.GetString("{0:They} have a blank, absent-minded stare and appears completely unresponsive to anything. {0:They} may snap out of it soon.", Owner) + "[/color]"); + } } } } diff --git a/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs b/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs index 9c5792fa47..208c7227b2 100644 --- a/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/ServerStatusEffectsComponent.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Content.Server.GameObjects.Components.Buckle; +using Content.Server.GameObjects.Components.Movement; using Content.Shared.GameObjects.Components.Mobs; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.Network; @@ -95,6 +96,14 @@ namespace Content.Server.GameObjects.Components.Mobs buckle.TryUnbuckle(player); break; + case StatusEffect.Piloting: + if (!player.TryGetComponent(out ShuttleControllerComponent controller)) + { + break; + } + + controller.RemoveController(); + break; } break; diff --git a/Content.Server/GameObjects/Components/Movement/ShuttleControllerComponent.cs b/Content.Server/GameObjects/Components/Movement/ShuttleControllerComponent.cs index 700b9e7b53..62063854f2 100644 --- a/Content.Server/GameObjects/Components/Movement/ShuttleControllerComponent.cs +++ b/Content.Server/GameObjects/Components/Movement/ShuttleControllerComponent.cs @@ -1,17 +1,19 @@ -using Content.Server.GameObjects.Components.Mobs; +#nullable enable +using Content.Server.GameObjects.Components.Buckle; +using Content.Server.GameObjects.Components.Mobs; +using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Movement; +using Content.Shared.GameObjects.Components.Strap; using Content.Shared.Physics; -using Robust.Server.GameObjects; -using Robust.Server.GameObjects.Components.Container; using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.Map; -using Robust.Shared.Interfaces.Network; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; +using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Movement @@ -21,8 +23,8 @@ namespace Content.Server.GameObjects.Components.Movement internal class ShuttleControllerComponent : Component, IMoverComponent { #pragma warning disable 649 - [Dependency] private readonly IMapManager _mapManager; - [Dependency] private readonly IEntityManager _entityManager; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; #pragma warning restore 649 private bool _movingUp; @@ -30,11 +32,22 @@ namespace Content.Server.GameObjects.Components.Movement private bool _movingLeft; private bool _movingRight; + /// + /// The icon to be displayed when piloting from this chair. + /// + private string _pilotingIcon = default!; + + /// + /// The entity that's currently controlling this component. + /// Changed from and + /// + private IEntity? _controller; + public override string Name => "ShuttleController"; [ViewVariables(VVAccess.ReadWrite)] - public float CurrentWalkSpeed { get; set; } = 8; - public float CurrentSprintSpeed { get; set; } + public float CurrentWalkSpeed { get; } = 8; + public float CurrentSprintSpeed => 0; /// [ViewVariables] @@ -44,7 +57,8 @@ namespace Content.Server.GameObjects.Components.Movement [ViewVariables] public float GrabRange => 0.0f; - public bool Sprinting { get; set; } + public bool Sprinting => false; + public (Vector2 walking, Vector2 sprinting) VelocityDir { get; } = (Vector2.Zero, Vector2.Zero); public GridCoordinates LastPosition { get; set; } public float StepSoundDistance { get; set; } @@ -53,7 +67,8 @@ namespace Content.Server.GameObjects.Components.Movement { var gridId = Owner.Transform.GridID; - if (_mapManager.TryGetGrid(gridId, out var grid) && _entityManager.TryGetEntity(grid.GridEntityId, out var gridEntity)) + if (_mapManager.TryGetGrid(gridId, out var grid) && + _entityManager.TryGetEntity(grid.GridEntityId, out var gridEntity)) { //TODO: Switch to shuttle component if (!gridEntity.TryGetComponent(out IPhysicsComponent physComp)) @@ -115,37 +130,116 @@ namespace Content.Server.GameObjects.Components.Movement // can't normalize zero length vector if (result.LengthSquared > 1.0e-6) + { result = result.Normalized; + } return result; } + /// + /// Changes the entity currently controlling this shuttle controller + /// + /// The entity to set + private void SetController(IEntity entity) + { + if (_controller != null || + !entity.TryGetComponent(out MindComponent mind) || + mind.Mind == null || + !Owner.TryGetComponent(out ServerStatusEffectsComponent status)) + { + return; + } + + mind.Mind.Visit(Owner); + _controller = entity; + + status.ChangeStatusEffectIcon(StatusEffect.Piloting, _pilotingIcon); + } + + /// + /// Removes the current controller + /// + /// The entity to remove, or null to force the removal of any current controller + public void RemoveController(IEntity? entity = null) + { + if (_controller == null) + { + return; + } + + // If we are not forcing a controller removal and the entity is not the current controller + if (entity != null && entity != _controller) + { + return; + } + + UpdateRemovedEntity(entity ?? _controller); + + _controller = null; + } + + /// + /// Updates the state of an entity that is no longer controlling this shuttle controller. + /// Called from + /// + /// The entity to update + private void UpdateRemovedEntity(IEntity entity) + { + if (Owner.TryGetComponent(out ServerStatusEffectsComponent status)) + { + status.RemoveStatusEffect(StatusEffect.Piloting); + } + + if (entity.TryGetComponent(out MindComponent mind)) + { + mind.Mind?.UnVisit(); + } + + if (entity.TryGetComponent(out BuckleComponent buckle)) + { + buckle.TryUnbuckle(entity, true); + } + } + + private void BuckleChanged(IEntity entity, in bool buckled) + { + Logger.DebugS("shuttle", $"Pilot={entity.Name}, buckled={buckled}"); + + if (buckled) + { + SetController(entity); + } + else + { + RemoveController(entity); + } + } + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _pilotingIcon, "pilotingIcon", "/Textures/Interface/StatusEffects/Buckle/buckled.png"); + } + + public override void Initialize() + { + base.Initialize(); + Owner.EnsureComponent(); + } + /// - public override void HandleMessage(ComponentMessage message, IComponent component) + public override void HandleMessage(ComponentMessage message, IComponent? component) { base.HandleMessage(message, component); switch (message) { - case ContainerContentsModifiedMessage contents: - if(contents.Entity.TryGetComponent(out MindComponent mindComp)) - ContentsChanged(contents.Entity, mindComp, contents.Removed); + case StrapChangeMessage strap: + BuckleChanged(strap.Entity, strap.Buckled); break; } } - - private void ContentsChanged(IEntity entity, MindComponent mindComp, in bool removed) - { - Logger.DebugS("shuttle", $"Pilot={entity.Name}, removed={removed}"); - - if (!removed) - { - mindComp.Mind?.Visit(Owner); - } - else - { - mindComp.Mind?.UnVisit(); - } - } } } diff --git a/Content.Server/GameObjects/Components/Strap/StrapComponent.cs b/Content.Server/GameObjects/Components/Strap/StrapComponent.cs index 98dd83cc8d..773d8f1aff 100644 --- a/Content.Server/GameObjects/Components/Strap/StrapComponent.cs +++ b/Content.Server/GameObjects/Components/Strap/StrapComponent.cs @@ -81,6 +81,7 @@ namespace Content.Server.GameObjects.Components.Strap } /// + /// DO NOT CALL THIS DIRECTLY. /// Adds a buckled entity. Called from /// /// The component to add @@ -107,6 +108,8 @@ namespace Content.Server.GameObjects.Components.Strap appearance.SetData(StrapVisuals.RotationAngle, _rotation); } + SendMessage(new StrapMessage(buckle.Owner, Owner)); + return true; } @@ -120,6 +123,7 @@ namespace Content.Server.GameObjects.Components.Strap if (_buckledEntities.Remove(buckle.Owner)) { _occupiedSize -= buckle.Size; + SendMessage(new UnStrapMessage(buckle.Owner, Owner)); } } @@ -157,6 +161,11 @@ namespace Content.Server.GameObjects.Components.Strap _occupiedSize = 0; } + public override ComponentState GetComponentState() + { + return new StrapComponentState(Position); + } + bool IInteractHand.InteractHand(InteractHandEventArgs eventArgs) { if (!eventArgs.User.TryGetComponent(out BuckleComponent buckle)) diff --git a/Content.Shared/GameObjects/Components/Buckle/SharedBuckleComponent.cs b/Content.Shared/GameObjects/Components/Buckle/SharedBuckleComponent.cs index 39c5fd4cbf..d10e77468a 100644 --- a/Content.Shared/GameObjects/Components/Buckle/SharedBuckleComponent.cs +++ b/Content.Shared/GameObjects/Components/Buckle/SharedBuckleComponent.cs @@ -1,6 +1,7 @@ using System; using Content.Shared.GameObjects.EntitySystems; using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Serialization; namespace Content.Shared.GameObjects.Components.Buckle @@ -50,4 +51,62 @@ namespace Content.Shared.GameObjects.Components.Buckle { Buckled } + + [Serializable, NetSerializable] + public abstract class BuckleChangeMessage : ComponentMessage + { + /// + /// Constructs a new instance of + /// + /// The entity that had its buckling status changed + /// The strap that the entity was buckled to or unbuckled from + /// True if the entity was buckled, false otherwise + protected BuckleChangeMessage(IEntity entity, IEntity strap, bool buckled) + { + Entity = entity; + Strap = strap; + Buckled = buckled; + } + + /// + /// The entity that had its buckling status changed + /// + public IEntity Entity { get; } + + /// + /// The strap that the entity was buckled to or unbuckled from + /// + public IEntity Strap { get; } + + /// + /// True if the entity was buckled, false otherwise. + /// + public bool Buckled { get; } + } + + [Serializable, NetSerializable] + public class BuckleMessage : BuckleChangeMessage + { + /// + /// Constructs a new instance of + /// + /// The entity that had its buckling status changed + /// The strap that the entity was buckled to or unbuckled from + public BuckleMessage(IEntity entity, IEntity strap) : base(entity, strap, true) + { + } + } + + [Serializable, NetSerializable] + public class UnbuckleMessage : BuckleChangeMessage + { + /// + /// Constructs a new instance of + /// + /// The entity that had its buckling status changed + /// The strap that the entity was buckled to or unbuckled from + public UnbuckleMessage(IEntity entity, IEntity strap) : base(entity, strap, false) + { + } + } } diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs index 95618e04b4..d068021e85 100644 --- a/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/SharedStatusEffectsComponent.cs @@ -58,5 +58,6 @@ namespace Content.Shared.GameObjects.Components.Mobs Thirst, Stun, Buckled, + Piloting } } diff --git a/Content.Shared/GameObjects/Components/Strap/SharedStrapComponent.cs b/Content.Shared/GameObjects/Components/Strap/SharedStrapComponent.cs index 64d895efa0..cd07ec0942 100644 --- a/Content.Shared/GameObjects/Components/Strap/SharedStrapComponent.cs +++ b/Content.Shared/GameObjects/Components/Strap/SharedStrapComponent.cs @@ -1,5 +1,6 @@ using System; using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Serialization; namespace Content.Shared.GameObjects.Components.Strap @@ -29,9 +30,81 @@ namespace Content.Shared.GameObjects.Components.Strap public sealed override uint? NetID => ContentNetIDs.STRAP; } + [Serializable, NetSerializable] + public sealed class StrapComponentState : ComponentState + { + public StrapComponentState(StrapPosition position) : base(ContentNetIDs.BUCKLE) + { + Position = position; + } + + /// + /// The change in position that this strap makes to the strapped mob + /// + public StrapPosition Position { get; } + } + [Serializable, NetSerializable] public enum StrapVisuals { RotationAngle } + + [Serializable, NetSerializable] + public abstract class StrapChangeMessage : ComponentMessage + { + /// + /// Constructs a new instance of + /// + /// The entity that had its buckling status changed + /// The strap that the entity was buckled to or unbuckled from + /// True if the entity was buckled, false otherwise + protected StrapChangeMessage(IEntity entity, IEntity strap, bool buckled) + { + Entity = entity; + Strap = strap; + Buckled = buckled; + } + + /// + /// The entity that had its buckling status changed + /// + public IEntity Entity { get; } + + /// + /// The strap that the entity was buckled to or unbuckled from + /// + public IEntity Strap { get; } + + /// + /// True if the entity was buckled, false otherwise. + /// + public bool Buckled { get; } + } + + [Serializable, NetSerializable] + public class StrapMessage : StrapChangeMessage + { + /// + /// Constructs a new instance of + /// + /// The entity that had its buckling status changed + /// The strap that the entity was buckled to or unbuckled from + public StrapMessage(IEntity entity, IEntity strap) : base(entity, strap, true) + { + } + } + + [Serializable, NetSerializable] + public class UnStrapMessage : StrapChangeMessage + { + /// + /// Constructs a new instance of + /// + /// The entity that had its buckling status changed + /// The strap that the entity was buckled to or unbuckled from + public UnStrapMessage(IEntity entity, IEntity strap) : base(entity, strap, false) + { + } + } } diff --git a/Resources/Prototypes/Entities/Constructible/Ground/pilot_chair.yml b/Resources/Prototypes/Entities/Constructible/Ground/pilot_chair.yml index c2e9156058..2789e1ce21 100644 --- a/Resources/Prototypes/Entities/Constructible/Ground/pilot_chair.yml +++ b/Resources/Prototypes/Entities/Constructible/Ground/pilot_chair.yml @@ -13,9 +13,6 @@ - type: Collidable - type: Clickable - type: InteractionOutline - - type: EntityStorage - showContents: true - noDoor: true - type: Damageable - type: Destructible thresholdvalue: 100