From 06581a004541e12caabe49164fd62533d8ab47ee Mon Sep 17 00:00:00 2001
From: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Date: Sat, 2 Aug 2025 17:34:36 -0700
Subject: [PATCH] Fix rotate verbs not being predicted (#38165)
* Fix rotate verbs not being predicted
* fixes
---------
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
---
.../Rotatable/FlippableComponent.cs | 15 --
Content.Server/Rotatable/RotatableSystem.cs | 197 ----------------
.../Rotatable/FlippableComponent.cs | 17 ++
.../Rotatable/RotatableComponent.cs | 47 ++--
Content.Shared/Rotatable/RotatableSystem.cs | 213 ++++++++++++++++++
.../components/rotatable-component.ftl | 3 +
6 files changed, 257 insertions(+), 235 deletions(-)
delete mode 100644 Content.Server/Rotatable/FlippableComponent.cs
delete mode 100644 Content.Server/Rotatable/RotatableSystem.cs
create mode 100644 Content.Shared/Rotatable/FlippableComponent.cs
create mode 100644 Content.Shared/Rotatable/RotatableSystem.cs
diff --git a/Content.Server/Rotatable/FlippableComponent.cs b/Content.Server/Rotatable/FlippableComponent.cs
deleted file mode 100644
index 98eb0f6d5c..0000000000
--- a/Content.Server/Rotatable/FlippableComponent.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Rotatable
-{
- [RegisterComponent]
- public sealed partial class FlippableComponent : Component
- {
- ///
- /// Entity to replace this entity with when the current one is 'flipped'.
- ///
- [DataField("mirrorEntity", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string MirrorEntity = default!;
- }
-}
diff --git a/Content.Server/Rotatable/RotatableSystem.cs b/Content.Server/Rotatable/RotatableSystem.cs
deleted file mode 100644
index 5190b0e92e..0000000000
--- a/Content.Server/Rotatable/RotatableSystem.cs
+++ /dev/null
@@ -1,197 +0,0 @@
-using Content.Server.Popups;
-using Content.Shared.ActionBlocker;
-using Content.Shared.Input;
-using Content.Shared.Interaction;
-using Content.Shared.Rotatable;
-using Content.Shared.Verbs;
-using Robust.Shared.Input.Binding;
-using Robust.Shared.Map;
-using Robust.Shared.Player;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Utility;
-
-namespace Content.Server.Rotatable
-{
- ///
- /// Handles verbs for the and components.
- ///
- public sealed class RotatableSystem : EntitySystem
- {
- [Dependency] private readonly PopupSystem _popup = default!;
- [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
- [Dependency] private readonly SharedInteractionSystem _interaction = default!;
- [Dependency] private readonly SharedTransformSystem _transform = default!;
-
- public override void Initialize()
- {
- SubscribeLocalEvent>(AddFlipVerb);
- SubscribeLocalEvent>(AddRotateVerbs);
-
- CommandBinds.Builder
- .Bind(ContentKeyFunctions.RotateObjectClockwise, new PointerInputCmdHandler(HandleRotateObjectClockwise))
- .Bind(ContentKeyFunctions.RotateObjectCounterclockwise, new PointerInputCmdHandler(HandleRotateObjectCounterclockwise))
- .Bind(ContentKeyFunctions.FlipObject, new PointerInputCmdHandler(HandleFlipObject))
- .Register();
- }
-
- private void AddFlipVerb(EntityUid uid, FlippableComponent component, GetVerbsEvent args)
- {
- if (!args.CanAccess
- || !args.CanInteract
- || !args.CanComplexInteract)
- return;
-
- // Check if the object is anchored.
- if (TryComp(uid, out PhysicsComponent? physics) && physics.BodyType == BodyType.Static)
- return;
-
- Verb verb = new()
- {
- Act = () => Flip(uid, component),
- Text = Loc.GetString("flippable-verb-get-data-text"),
- Category = VerbCategory.Rotate,
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/flip.svg.192dpi.png")),
- Priority = -3, // show flip last
- DoContactInteraction = true
- };
- args.Verbs.Add(verb);
- }
-
- private void AddRotateVerbs(EntityUid uid, RotatableComponent component, GetVerbsEvent args)
- {
- if (!args.CanAccess
- || !args.CanInteract
- || !args.CanComplexInteract
- || Transform(uid).NoLocalRotation) // Good ol prototype inheritance, eh?
- return;
-
- // Check if the object is anchored, and whether we are still allowed to rotate it.
- if (!component.RotateWhileAnchored &&
- TryComp(uid, out PhysicsComponent? physics) &&
- physics.BodyType == BodyType.Static)
- return;
-
- Verb resetRotation = new()
- {
- DoContactInteraction = true,
- Act = () => Comp(uid).LocalRotation = Angle.Zero,
- Category = VerbCategory.Rotate,
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
- Text = "Reset",
- Priority = -2, // show CCW, then CW, then reset
- CloseMenu = false,
- };
- args.Verbs.Add(resetRotation);
-
- // rotate clockwise
- Verb rotateCW = new()
- {
- Act = () => Comp(uid).LocalRotation -= component.Increment,
- Category = VerbCategory.Rotate,
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png")),
- Priority = -1,
- CloseMenu = false, // allow for easy double rotations.
- };
- args.Verbs.Add(rotateCW);
-
- // rotate counter-clockwise
- Verb rotateCCW = new()
- {
- Act = () => Comp(uid).LocalRotation += component.Increment,
- Category = VerbCategory.Rotate,
- Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png")),
- Priority = 0,
- CloseMenu = false, // allow for easy double rotations.
- };
- args.Verbs.Add(rotateCCW);
- }
-
- ///
- /// Replace a flippable entity with it's flipped / mirror-symmetric entity.
- ///
- public void Flip(EntityUid uid, FlippableComponent component)
- {
- var oldTransform = Comp(uid);
- var entity = Spawn(component.MirrorEntity, oldTransform.Coordinates);
- var newTransform = Comp(entity);
- newTransform.LocalRotation = oldTransform.LocalRotation;
- _transform.Unanchor(entity, newTransform);
- Del(uid);
- }
-
- public bool HandleRotateObjectClockwise(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
- {
- if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
- return false;
-
- if (!TryComp(entity, out var rotatableComp))
- return false;
-
- if (!_actionBlocker.CanInteract(player, entity)
- || !_actionBlocker.CanComplexInteract(player)
- || !_interaction.InRangeAndAccessible(player, entity))
- return false;
-
- // Check if the object is anchored, and whether we are still allowed to rotate it.
- if (!rotatableComp.RotateWhileAnchored && TryComp(entity, out PhysicsComponent? physics) &&
- physics.BodyType == BodyType.Static)
- {
- _popup.PopupEntity(Loc.GetString("rotatable-component-try-rotate-stuck"), entity, player);
- return false;
- }
-
- Transform(entity).LocalRotation -= rotatableComp.Increment;
- return true;
- }
-
- public bool HandleRotateObjectCounterclockwise(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
- {
- if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
- return false;
-
- if (!TryComp(entity, out var rotatableComp))
- return false;
-
- if (!_actionBlocker.CanInteract(player, entity)
- || !_actionBlocker.CanComplexInteract(player)
- || !_interaction.InRangeAndAccessible(player, entity))
- return false;
-
- // Check if the object is anchored, and whether we are still allowed to rotate it.
- if (!rotatableComp.RotateWhileAnchored && TryComp(entity, out PhysicsComponent? physics) &&
- physics.BodyType == BodyType.Static)
- {
- _popup.PopupEntity(Loc.GetString("rotatable-component-try-rotate-stuck"), entity, player);
- return false;
- }
-
- Transform(entity).LocalRotation += rotatableComp.Increment;
- return true;
- }
-
- public bool HandleFlipObject(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
- {
- if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
- return false;
-
- if (!TryComp(entity, out var flippableComp))
- return false;
-
- if (!_actionBlocker.CanInteract(player, entity)
- || !_actionBlocker.CanComplexInteract(player)
- || !_interaction.InRangeAndAccessible(player, entity))
- return false;
-
- // Check if the object is anchored.
- if (TryComp(entity, out PhysicsComponent? physics) && physics.BodyType == BodyType.Static)
- {
- _popup.PopupEntity(Loc.GetString("flippable-component-try-flip-is-stuck"), entity, player);
- return false;
- }
-
- Flip(entity, flippableComp);
- return true;
- }
- }
-}
diff --git a/Content.Shared/Rotatable/FlippableComponent.cs b/Content.Shared/Rotatable/FlippableComponent.cs
new file mode 100644
index 0000000000..fdc8c15c8a
--- /dev/null
+++ b/Content.Shared/Rotatable/FlippableComponent.cs
@@ -0,0 +1,17 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Rotatable;
+
+///
+/// Allows an entity to be flipped (mirrored) by using a verb.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class FlippableComponent : Component
+{
+ ///
+ /// Entity to replace this entity with when the current one is 'flipped'.
+ ///
+ [DataField(required: true), AutoNetworkedField]
+ public EntProtoId MirrorEntity = default!;
+}
diff --git a/Content.Shared/Rotatable/RotatableComponent.cs b/Content.Shared/Rotatable/RotatableComponent.cs
index b008c28160..336c484d8c 100644
--- a/Content.Shared/Rotatable/RotatableComponent.cs
+++ b/Content.Shared/Rotatable/RotatableComponent.cs
@@ -1,27 +1,28 @@
-namespace Content.Shared.Rotatable
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Rotatable;
+
+///
+/// Allows an entity to be rotated by using a verb.
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class RotatableComponent : Component
{
- [RegisterComponent]
- public sealed partial class RotatableComponent : Component
- {
- ///
- /// If true, this entity can be rotated even while anchored.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("rotateWhileAnchored")]
- public bool RotateWhileAnchored { get; private set; }
+ ///
+ /// If true, this entity can be rotated even while anchored.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool RotateWhileAnchored;
- ///
- /// If true, will rotate entity in players direction when pulled
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("rotateWhilePulling")]
- public bool RotateWhilePulling { get; private set; } = true;
+ ///
+ /// If true, will rotate entity in players direction when pulled
+ ///
+ [DataField, AutoNetworkedField]
+ public bool RotateWhilePulling = true;
- ///
- /// The angular value to change when using the rotate verbs.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("increment")]
- public Angle Increment { get; private set; } = Angle.FromDegrees(90);
- }
+ ///
+ /// The angular value to change when using the rotate verbs.
+ ///
+ [DataField, AutoNetworkedField]
+ public Angle Increment = Angle.FromDegrees(90);
}
diff --git a/Content.Shared/Rotatable/RotatableSystem.cs b/Content.Shared/Rotatable/RotatableSystem.cs
new file mode 100644
index 0000000000..94d9206247
--- /dev/null
+++ b/Content.Shared/Rotatable/RotatableSystem.cs
@@ -0,0 +1,213 @@
+using Content.Shared.ActionBlocker;
+using Content.Shared.Input;
+using Content.Shared.Interaction;
+using Content.Shared.Popups;
+using Content.Shared.Verbs;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Map;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Rotatable;
+
+///
+/// Handles verbs for the and components.
+///
+public sealed class RotatableSystem : EntitySystem
+{
+ [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
+ [Dependency] private readonly SharedInteractionSystem _interaction = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent>(AddFlipVerb);
+ SubscribeLocalEvent>(AddRotateVerbs);
+
+ CommandBinds.Builder
+ .Bind(ContentKeyFunctions.RotateObjectClockwise, new PointerInputCmdHandler(HandleRotateObjectClockwise))
+ .Bind(ContentKeyFunctions.RotateObjectCounterclockwise, new PointerInputCmdHandler(HandleRotateObjectCounterclockwise))
+ .Bind(ContentKeyFunctions.FlipObject, new PointerInputCmdHandler(HandleFlipObject))
+ .Register();
+ }
+
+ private void AddFlipVerb(EntityUid uid, FlippableComponent component, GetVerbsEvent args)
+ {
+ if (!args.CanAccess
+ || !args.CanInteract
+ || !args.CanComplexInteract)
+ return;
+
+ // Check if the object is anchored.
+ if (TryComp(uid, out var physics) && physics.BodyType == BodyType.Static)
+ return;
+
+ Verb verb = new()
+ {
+ Act = () => Flip(uid, component),
+ Text = Loc.GetString("flippable-verb-get-data-text"),
+ Category = VerbCategory.Rotate,
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/flip.svg.192dpi.png")),
+ Priority = -3, // show flip last
+ DoContactInteraction = true
+ };
+ args.Verbs.Add(verb);
+ }
+
+ private void AddRotateVerbs(EntityUid uid, RotatableComponent component, GetVerbsEvent args)
+ {
+ if (!args.CanAccess
+ || !args.CanInteract
+ || !args.CanComplexInteract
+ || Transform(uid).NoLocalRotation) // Good ol prototype inheritance, eh?
+ return;
+
+ // Check if the object is anchored, and whether we are still allowed to rotate it.
+ if (!component.RotateWhileAnchored &&
+ TryComp(uid, out var physics) &&
+ physics.BodyType == BodyType.Static)
+ return;
+
+ Verb resetRotation = new()
+ {
+ DoContactInteraction = true,
+ Act = () => ResetRotation(uid),
+ Category = VerbCategory.Rotate,
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
+ Text = Loc.GetString("rotate-reset-verb-get-data-text"),
+ Priority = -2, // show CCW, then CW, then reset
+ CloseMenu = false,
+ };
+ args.Verbs.Add(resetRotation);
+
+ // rotate clockwise
+ Verb rotateCW = new()
+ {
+ Act = () => Rotate(uid, -component.Increment),
+ Category = VerbCategory.Rotate,
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png")),
+ Text = Loc.GetString("rotate-verb-get-data-text"),
+ Priority = -1,
+ CloseMenu = false, // allow for easy double rotations.
+ };
+ args.Verbs.Add(rotateCW);
+
+ // rotate counter-clockwise
+ Verb rotateCCW = new()
+ {
+ Act = () => Rotate(uid, component.Increment),
+ Category = VerbCategory.Rotate,
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png")),
+ Text = Loc.GetString("rotate-counter-verb-get-data-text"),
+ Priority = 0,
+ CloseMenu = false, // allow for easy double rotations.
+ };
+ args.Verbs.Add(rotateCCW);
+ }
+
+ ///
+ /// Replace a flippable entity with it's flipped / mirror-symmetric entity.
+ ///
+ public void Flip(EntityUid uid, FlippableComponent component)
+ {
+ var oldTransform = Comp(uid);
+ var entity = PredictedSpawnAtPosition(component.MirrorEntity, oldTransform.Coordinates);
+ var newTransform = Comp(entity);
+ _transform.SetLocalRotation(entity, oldTransform.LocalRotation);
+ _transform.Unanchor(entity, newTransform);
+ PredictedDel(uid);
+ }
+
+ private bool HandleRotateObjectClockwise(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
+ {
+ if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
+ return false;
+
+ if (!TryComp(entity, out var rotatableComp))
+ return false;
+
+ if (!_actionBlocker.CanInteract(player, entity)
+ || !_actionBlocker.CanComplexInteract(player)
+ || !_interaction.InRangeAndAccessible(player, entity))
+ return false;
+
+ // Check if the object is anchored, and whether we are still allowed to rotate it.
+ if (!rotatableComp.RotateWhileAnchored && TryComp(entity, out var physics) &&
+ physics.BodyType == BodyType.Static)
+ {
+ _popup.PopupClient(Loc.GetString("rotatable-component-try-rotate-stuck"), entity, player);
+ return false;
+ }
+
+ Rotate(entity, -rotatableComp.Increment);
+ return false;
+ }
+
+ private bool HandleRotateObjectCounterclockwise(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
+ {
+ if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
+ return false;
+
+ if (!TryComp(entity, out var rotatableComp))
+ return false;
+
+ if (!_actionBlocker.CanInteract(player, entity)
+ || !_actionBlocker.CanComplexInteract(player)
+ || !_interaction.InRangeAndAccessible(player, entity))
+ return false;
+
+ // Check if the object is anchored, and whether we are still allowed to rotate it.
+ if (!rotatableComp.RotateWhileAnchored && TryComp(entity, out var physics) &&
+ physics.BodyType == BodyType.Static)
+ {
+ _popup.PopupClient(Loc.GetString("rotatable-component-try-rotate-stuck"), entity, player);
+ return false;
+ }
+
+ Rotate(entity, rotatableComp.Increment);
+ return false;
+ }
+
+ private bool HandleFlipObject(ICommonSession? playerSession, EntityCoordinates coordinates, EntityUid entity)
+ {
+ if (playerSession?.AttachedEntity is not { Valid: true } player || !Exists(player))
+ return false;
+
+ if (!TryComp(entity, out var flippableComp))
+ return false;
+
+ if (!_actionBlocker.CanInteract(player, entity)
+ || !_actionBlocker.CanComplexInteract(player)
+ || !_interaction.InRangeAndAccessible(player, entity))
+ return false;
+
+ // Check if the object is anchored.
+ if (TryComp(entity, out var physics) && physics.BodyType == BodyType.Static)
+ {
+ _popup.PopupClient(Loc.GetString("flippable-component-try-flip-is-stuck"), entity, player);
+ return false;
+ }
+
+ Flip(entity, flippableComp);
+ return false;
+ }
+
+ private void Rotate(Entity ent, Angle angle)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return;
+
+ _transform.SetLocalRotation(ent.Owner, ent.Comp.LocalRotation + angle);
+ }
+
+ private void ResetRotation(Entity ent)
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return;
+
+ _transform.SetLocalRotation(ent.Owner, Angle.Zero);
+ }
+}
diff --git a/Resources/Locale/en-US/rotation/components/rotatable-component.ftl b/Resources/Locale/en-US/rotation/components/rotatable-component.ftl
index bc23e488fb..18d8d55bdd 100644
--- a/Resources/Locale/en-US/rotation/components/rotatable-component.ftl
+++ b/Resources/Locale/en-US/rotation/components/rotatable-component.ftl
@@ -6,3 +6,6 @@ rotate-verb-get-data-text = Rotate clockwise
# RotateCounterVerb
rotate-counter-verb-get-data-text = Rotate counter-clockwise
+
+# ResetVerb
+rotate-reset-verb-get-data-text = Reset