diff --git a/Content.Client/Guidebook/GuidebookDataSystem.cs b/Content.Client/Guidebook/GuidebookDataSystem.cs
new file mode 100644
index 0000000000..f47ad6ef1b
--- /dev/null
+++ b/Content.Client/Guidebook/GuidebookDataSystem.cs
@@ -0,0 +1,45 @@
+using Content.Shared.Guidebook;
+
+namespace Content.Client.Guidebook;
+
+///
+/// Client system for storing and retrieving values extracted from entity prototypes
+/// for display in the guidebook ().
+/// Requests data from the server on .
+/// Can also be pushed new data when the server reloads prototypes.
+///
+public sealed class GuidebookDataSystem : EntitySystem
+{
+ private GuidebookData? _data;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeNetworkEvent(OnServerUpdated);
+
+ // Request data from the server
+ RaiseNetworkEvent(new RequestGuidebookDataEvent());
+ }
+
+ private void OnServerUpdated(UpdateGuidebookDataEvent args)
+ {
+ // Got new data from the server, either in response to our request, or because prototypes reloaded on the server
+ _data = args.Data;
+ _data.Freeze();
+ }
+
+ ///
+ /// Attempts to retrieve a value using the given identifiers.
+ /// See for more information.
+ ///
+ public bool TryGetValue(string prototype, string component, string field, out object? value)
+ {
+ if (_data == null)
+ {
+ value = null;
+ return false;
+ }
+ return _data.TryGetValue(prototype, component, field, out value);
+ }
+}
diff --git a/Content.Client/Guidebook/Richtext/ProtodataTag.cs b/Content.Client/Guidebook/Richtext/ProtodataTag.cs
new file mode 100644
index 0000000000..a725fd4e4b
--- /dev/null
+++ b/Content.Client/Guidebook/Richtext/ProtodataTag.cs
@@ -0,0 +1,49 @@
+using System.Globalization;
+using Robust.Client.UserInterface.RichText;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Guidebook.RichText;
+
+///
+/// RichText tag that can display values extracted from entity prototypes.
+/// In order to be accessed by this tag, the desired field/property must
+/// be tagged with .
+///
+public sealed class ProtodataTag : IMarkupTag
+{
+ [Dependency] private readonly ILogManager _logMan = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+
+ public string Name => "protodata";
+ private ISawmill Log => _log ??= _logMan.GetSawmill("protodata_tag");
+ private ISawmill? _log;
+
+ public string TextBefore(MarkupNode node)
+ {
+ // Do nothing with an empty tag
+ if (!node.Value.TryGetString(out var prototype))
+ return string.Empty;
+
+ if (!node.Attributes.TryGetValue("comp", out var component))
+ return string.Empty;
+ if (!node.Attributes.TryGetValue("member", out var member))
+ return string.Empty;
+ node.Attributes.TryGetValue("format", out var format);
+
+ var guidebookData = _entMan.System();
+
+ // Try to get the value
+ if (!guidebookData.TryGetValue(prototype, component.StringValue!, member.StringValue!, out var value))
+ {
+ Log.Error($"Failed to find protodata for {component}.{member} in {prototype}");
+ return "???";
+ }
+
+ // If we have a format string and a formattable value, format it as requested
+ if (!string.IsNullOrEmpty(format.StringValue) && value is IFormattable formattable)
+ return formattable.ToString(format.StringValue, CultureInfo.CurrentCulture);
+
+ // No format string given, so just use default ToString
+ return value?.ToString() ?? "NULL";
+ }
+}
diff --git a/Content.Client/PDA/PdaBoundUserInterface.cs b/Content.Client/PDA/PdaBoundUserInterface.cs
index 37ce9c4280..2d4033390c 100644
--- a/Content.Client/PDA/PdaBoundUserInterface.cs
+++ b/Content.Client/PDA/PdaBoundUserInterface.cs
@@ -4,18 +4,20 @@
using Content.Shared.PDA;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
-using Robust.Shared.Configuration;
namespace Content.Client.PDA
{
[UsedImplicitly]
public sealed class PdaBoundUserInterface : CartridgeLoaderBoundUserInterface
{
+ private readonly PdaSystem _pdaSystem;
+
[ViewVariables]
private PdaMenu? _menu;
public PdaBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
+ _pdaSystem = EntMan.System();
}
protected override void Open()
@@ -92,7 +94,13 @@ protected override void UpdateState(BoundUserInterfaceState state)
if (state is not PdaUpdateState updateState)
return;
- _menu?.UpdateState(updateState);
+ if (_menu == null)
+ {
+ _pdaSystem.Log.Error("PDA state received before menu was created.");
+ return;
+ }
+
+ _menu.UpdateState(updateState);
}
protected override void AttachCartridgeUI(Control cartridgeUIFragment, string? title)
diff --git a/Content.Client/PDA/PdaMenu.xaml b/Content.Client/PDA/PdaMenu.xaml
index 8b26860332..8c9b4ae2ee 100644
--- a/Content.Client/PDA/PdaMenu.xaml
+++ b/Content.Client/PDA/PdaMenu.xaml
@@ -67,14 +67,17 @@
Description="{Loc 'comp-pda-ui-ringtone-button-description'}"/>
diff --git a/Content.Client/Physics/Controllers/MoverController.cs b/Content.Client/Physics/Controllers/MoverController.cs
index 03df383eeb..c97110b208 100644
--- a/Content.Client/Physics/Controllers/MoverController.cs
+++ b/Content.Client/Physics/Controllers/MoverController.cs
@@ -8,132 +8,131 @@
using Robust.Shared.Player;
using Robust.Shared.Timing;
-namespace Content.Client.Physics.Controllers
+namespace Content.Client.Physics.Controllers;
+
+public sealed class MoverController : SharedMoverController
{
- public sealed class MoverController : SharedMoverController
- {
- [Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnRelayPlayerAttached);
- SubscribeLocalEvent(OnRelayPlayerDetached);
- SubscribeLocalEvent(OnPlayerAttached);
- SubscribeLocalEvent(OnPlayerDetached);
-
- SubscribeLocalEvent(OnUpdatePredicted);
- SubscribeLocalEvent(OnUpdateRelayTargetPredicted);
- SubscribeLocalEvent(OnUpdatePullablePredicted);
- }
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnRelayPlayerAttached);
+ SubscribeLocalEvent(OnRelayPlayerDetached);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ SubscribeLocalEvent(OnUpdatePredicted);
+ SubscribeLocalEvent(OnUpdateRelayTargetPredicted);
+ SubscribeLocalEvent(OnUpdatePullablePredicted);
+ }
- private void OnUpdatePredicted(Entity entity, ref UpdateIsPredictedEvent args)
- {
- // Enable prediction if an entity is controlled by the player
- if (entity.Owner == _playerManager.LocalEntity)
- args.IsPredicted = true;
- }
+ private void OnUpdatePredicted(Entity entity, ref UpdateIsPredictedEvent args)
+ {
+ // Enable prediction if an entity is controlled by the player
+ if (entity.Owner == _playerManager.LocalEntity)
+ args.IsPredicted = true;
+ }
- private void OnUpdateRelayTargetPredicted(Entity entity, ref UpdateIsPredictedEvent args)
- {
- if (entity.Comp.Source == _playerManager.LocalEntity)
- args.IsPredicted = true;
- }
+ private void OnUpdateRelayTargetPredicted(Entity entity, ref UpdateIsPredictedEvent args)
+ {
+ if (entity.Comp.Source == _playerManager.LocalEntity)
+ args.IsPredicted = true;
+ }
- private void OnUpdatePullablePredicted(Entity entity, ref UpdateIsPredictedEvent args)
- {
- // Enable prediction if an entity is being pulled by the player.
- // Disable prediction if an entity is being pulled by some non-player entity.
+ private void OnUpdatePullablePredicted(Entity entity, ref UpdateIsPredictedEvent args)
+ {
+ // Enable prediction if an entity is being pulled by the player.
+ // Disable prediction if an entity is being pulled by some non-player entity.
- if (entity.Comp.Puller == _playerManager.LocalEntity)
- args.IsPredicted = true;
- else if (entity.Comp.Puller != null)
- args.BlockPrediction = true;
+ if (entity.Comp.Puller == _playerManager.LocalEntity)
+ args.IsPredicted = true;
+ else if (entity.Comp.Puller != null)
+ args.BlockPrediction = true;
- // TODO recursive pulling checks?
- // What if the entity is being pulled by a vehicle controlled by the player?
- }
+ // TODO recursive pulling checks?
+ // What if the entity is being pulled by a vehicle controlled by the player?
+ }
- private void OnRelayPlayerAttached(Entity entity, ref LocalPlayerAttachedEvent args)
- {
- Physics.UpdateIsPredicted(entity.Owner);
- Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
- if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
- SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
- }
+ private void OnRelayPlayerAttached(Entity entity, ref LocalPlayerAttachedEvent args)
+ {
+ Physics.UpdateIsPredicted(entity.Owner);
+ Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
+ if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
+ SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
+ }
- private void OnRelayPlayerDetached(Entity entity, ref LocalPlayerDetachedEvent args)
- {
- Physics.UpdateIsPredicted(entity.Owner);
- Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
- if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
- SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
- }
+ private void OnRelayPlayerDetached(Entity entity, ref LocalPlayerDetachedEvent args)
+ {
+ Physics.UpdateIsPredicted(entity.Owner);
+ Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
+ if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
+ SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
+ }
- private void OnPlayerAttached(Entity entity, ref LocalPlayerAttachedEvent args)
- {
- SetMoveInput(entity, MoveButtons.None);
- }
+ private void OnPlayerAttached(Entity entity, ref LocalPlayerAttachedEvent args)
+ {
+ SetMoveInput(entity, MoveButtons.None);
+ }
- private void OnPlayerDetached(Entity entity, ref LocalPlayerDetachedEvent args)
- {
- SetMoveInput(entity, MoveButtons.None);
- }
+ private void OnPlayerDetached(Entity entity, ref LocalPlayerDetachedEvent args)
+ {
+ SetMoveInput(entity, MoveButtons.None);
+ }
- public override void UpdateBeforeSolve(bool prediction, float frameTime)
- {
- base.UpdateBeforeSolve(prediction, frameTime);
+ public override void UpdateBeforeSolve(bool prediction, float frameTime)
+ {
+ base.UpdateBeforeSolve(prediction, frameTime);
- if (_playerManager.LocalEntity is not {Valid: true} player)
- return;
+ if (_playerManager.LocalEntity is not {Valid: true} player)
+ return;
- if (RelayQuery.TryGetComponent(player, out var relayMover))
- HandleClientsideMovement(relayMover.RelayEntity, frameTime);
+ if (RelayQuery.TryGetComponent(player, out var relayMover))
+ HandleClientsideMovement(relayMover.RelayEntity, frameTime);
- HandleClientsideMovement(player, frameTime);
- }
+ HandleClientsideMovement(player, frameTime);
+ }
- private void HandleClientsideMovement(EntityUid player, float frameTime)
+ private void HandleClientsideMovement(EntityUid player, float frameTime)
+ {
+ if (!MoverQuery.TryGetComponent(player, out var mover) ||
+ !XformQuery.TryGetComponent(player, out var xform))
{
- if (!MoverQuery.TryGetComponent(player, out var mover) ||
- !XformQuery.TryGetComponent(player, out var xform))
- {
- return;
- }
-
- var physicsUid = player;
- PhysicsComponent? body;
- var xformMover = xform;
+ return;
+ }
- if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
- {
- if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
- !XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
- {
- return;
- }
+ var physicsUid = player;
+ PhysicsComponent? body;
+ var xformMover = xform;
- physicsUid = xform.ParentUid;
- }
- else if (!PhysicsQuery.TryGetComponent(player, out body))
+ if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
+ {
+ if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
+ !XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
{
return;
}
- // Server-side should just be handled on its own so we'll just do this shizznit
- HandleMobMovement(
- player,
- mover,
- physicsUid,
- body,
- xformMover,
- frameTime);
+ physicsUid = xform.ParentUid;
}
-
- protected override bool CanSound()
+ else if (!PhysicsQuery.TryGetComponent(player, out body))
{
- return _timing is { IsFirstTimePredicted: true, InSimulation: true };
+ return;
}
+
+ // Server-side should just be handled on its own so we'll just do this shizznit
+ HandleMobMovement(
+ player,
+ mover,
+ physicsUid,
+ body,
+ xformMover,
+ frameTime);
+ }
+
+ protected override bool CanSound()
+ {
+ return _timing is { IsFirstTimePredicted: true, InSimulation: true };
}
}
diff --git a/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
index 5a082485a5..a6a20958f5 100644
--- a/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
+++ b/Content.Client/Power/ActivatableUIRequiresPowerSystem.cs
@@ -18,9 +18,6 @@ protected override void OnActivate(Entity e
return;
}
- if (TryComp(ent.Owner, out var panel) && panel.Open)
- return;
-
_popup.PopupClient(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent.Owner)), args.User, args.User);
args.Cancel();
}
diff --git a/Content.Client/Replay/ContentReplayPlaybackManager.cs b/Content.Client/Replay/ContentReplayPlaybackManager.cs
index f90731bfa7..b96eae44e9 100644
--- a/Content.Client/Replay/ContentReplayPlaybackManager.cs
+++ b/Content.Client/Replay/ContentReplayPlaybackManager.cs
@@ -1,10 +1,8 @@
-using System.IO.Compression;
using Content.Client.Administration.Managers;
using Content.Client.Launcher;
using Content.Client.MainMenu;
using Content.Client.Replay.Spectator;
using Content.Client.Replay.UI.Loading;
-using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.Chat;
using Content.Shared.Chat;
using Content.Shared.Effects;
@@ -26,8 +24,6 @@
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
@@ -60,7 +56,7 @@ public sealed class ContentReplayPlaybackManager
public bool IsScreenshotMode = false;
private bool _initialized;
-
+
///
/// Most recently loaded file, for re-attempting the load with error tolerance.
/// Required because the zip reader auto-disposes and I'm too lazy to change it so that
@@ -96,32 +92,17 @@ private void OnFinishedLoading(Exception? exception)
return;
}
- ReturnToDefaultState();
-
- // Show a popup window with the error message
- var text = Loc.GetString("replay-loading-failed", ("reason", exception));
- var box = new BoxContainer
- {
- Orientation = BoxContainer.LayoutOrientation.Vertical,
- Children = {new Label {Text = text}}
- };
+ if (_client.RunLevel == ClientRunLevel.SinglePlayerGame)
+ _client.StopSinglePlayer();
- var popup = new DefaultWindow { Title = "Error!" };
- popup.Contents.AddChild(box);
+ Action? retryAction = null;
+ Action? cancelAction = null;
- // Add button for attempting to re-load the replay while ignoring some errors.
- if (!_cfg.GetCVar(CVars.ReplayIgnoreErrors) && LastLoad is {} last)
+ if (!_cfg.GetCVar(CVars.ReplayIgnoreErrors) && LastLoad is { } last)
{
- var button = new Button
- {
- Text = Loc.GetString("replay-loading-retry"),
- StyleClasses = { StyleBase.ButtonCaution }
- };
-
- button.OnPressed += _ =>
+ retryAction = () =>
{
_cfg.SetCVar(CVars.ReplayIgnoreErrors, true);
- popup.Dispose();
IReplayFileReader reader = last.Zip == null
? new ReplayFileReaderResources(_resMan, last.Folder)
@@ -129,11 +110,20 @@ private void OnFinishedLoading(Exception? exception)
_loadMan.LoadAndStartReplay(reader);
};
-
- box.AddChild(button);
}
- popup.OpenCentered();
+ // If we have an explicit menu to get back to (e.g. replay browser UI), show a cancel button.
+ if (DefaultState != null)
+ {
+ cancelAction = () =>
+ {
+ _stateMan.RequestStateChange(DefaultState);
+ };
+ }
+
+ // Switch to a new game state to present the error and cancel/retry options.
+ var state = _stateMan.RequestStateChange();
+ state.SetData(exception, cancelAction, retryAction);
}
public void ReturnToDefaultState()
diff --git a/Content.Client/Replay/UI/Loading/ReplayLoadingFailed.cs b/Content.Client/Replay/UI/Loading/ReplayLoadingFailed.cs
new file mode 100644
index 0000000000..223895eb29
--- /dev/null
+++ b/Content.Client/Replay/UI/Loading/ReplayLoadingFailed.cs
@@ -0,0 +1,36 @@
+using Content.Client.Stylesheets;
+using Robust.Client.State;
+using Robust.Client.UserInterface;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Replay.UI.Loading;
+
+///
+/// State used to display an error message if a replay failed to load.
+///
+///
+///
+public sealed class ReplayLoadingFailed : State
+{
+ [Dependency] private readonly IStylesheetManager _stylesheetManager = default!;
+ [Dependency] private readonly IUserInterfaceManager _userInterface = default!;
+
+ private ReplayLoadingFailedControl? _control;
+
+ public void SetData(Exception exception, Action? cancelPressed, Action? retryPressed)
+ {
+ DebugTools.Assert(_control != null);
+ _control.SetData(exception, cancelPressed, retryPressed);
+ }
+
+ protected override void Startup()
+ {
+ _control = new ReplayLoadingFailedControl(_stylesheetManager);
+ _userInterface.StateRoot.AddChild(_control);
+ }
+
+ protected override void Shutdown()
+ {
+ _control?.Orphan();
+ }
+}
diff --git a/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml b/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml
new file mode 100644
index 0000000000..5f77a66e53
--- /dev/null
+++ b/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml.cs b/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml.cs
new file mode 100644
index 0000000000..088c9a291a
--- /dev/null
+++ b/Content.Client/Replay/UI/Loading/ReplayLoadingFailedControl.xaml.cs
@@ -0,0 +1,44 @@
+using Content.Client.Stylesheets;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Replay.UI.Loading;
+
+[GenerateTypedNameReferences]
+public sealed partial class ReplayLoadingFailedControl : Control
+{
+ public ReplayLoadingFailedControl(IStylesheetManager stylesheet)
+ {
+ RobustXamlLoader.Load(this);
+
+ Stylesheet = stylesheet.SheetSpace;
+ LayoutContainer.SetAnchorPreset(this, LayoutContainer.LayoutPreset.Wide);
+ }
+
+ public void SetData(Exception exception, Action? cancelPressed, Action? retryPressed)
+ {
+ ReasonLabel.SetMessage(
+ FormattedMessage.FromUnformatted(Loc.GetString("replay-loading-failed", ("reason", exception))));
+
+ if (cancelPressed != null)
+ {
+ CancelButton.Visible = true;
+ CancelButton.OnPressed += _ =>
+ {
+ cancelPressed();
+ };
+ }
+
+ if (retryPressed != null)
+ {
+ RetryButton.Visible = true;
+ RetryButton.OnPressed += _ =>
+ {
+ retryPressed();
+ };
+ }
+ }
+}
diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs
index 6d8b3a2243..f990c83d7c 100644
--- a/Content.Client/Verbs/VerbSystem.cs
+++ b/Content.Client/Verbs/VerbSystem.cs
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Numerics;
using Content.Client.Examine;
using Content.Client.Gameplay;
using Content.Client.Popups;
@@ -7,6 +8,7 @@
using Content.Shared.Tag;
using Content.Shared.Verbs;
using JetBrains.Annotations;
+using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -21,9 +23,10 @@ public sealed class VerbSystem : SharedVerbSystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly ExamineSystem _examine = default!;
+ [Dependency] private readonly SpriteTreeSystem _tree = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
- [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
///
@@ -31,8 +34,6 @@ public sealed class VerbSystem : SharedVerbSystem
///
public const float EntityMenuLookupSize = 0.25f;
- [Dependency] private readonly IEyeManager _eyeManager = default!;
-
///
/// These flags determine what entities the user can see on the context menu.
///
@@ -40,6 +41,8 @@ public sealed class VerbSystem : SharedVerbSystem
public Action? OnVerbsResponse;
+ private List _entities = new();
+
public override void Initialize()
{
base.Initialize();
@@ -76,49 +79,50 @@ public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true
visibility = ev.Visibility;
// Get entities
- List entities;
- var examineFlags = LookupFlags.All & ~LookupFlags.Sensors;
+ _entities.Clear();
+ var entitiesUnderMouse = _tree.QueryAabb(targetPos.MapId, Box2.CenteredAround(targetPos.Position, new Vector2(EntityMenuLookupSize, EntityMenuLookupSize)));
// Do we have to do FoV checks?
if ((visibility & MenuVisibility.NoFov) == 0)
{
- var entitiesUnderMouse = gameScreenBase.GetClickableEntities(targetPos).ToHashSet();
- bool Predicate(EntityUid e) => e == player || entitiesUnderMouse.Contains(e);
+ bool Predicate(EntityUid e) => e == player;
TryComp(player.Value, out ExaminerComponent? examiner);
- entities = new();
- foreach (var ent in _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize, flags: examineFlags))
+ foreach (var ent in entitiesUnderMouse)
{
- if (_examine.CanExamine(player.Value, targetPos, Predicate, ent, examiner))
- entities.Add(ent);
+ if (_examine.CanExamine(player.Value, targetPos, Predicate, ent.Uid, examiner))
+ _entities.Add(ent.Uid);
}
}
else
{
- entities = _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize, flags: examineFlags).ToList();
+ foreach (var ent in entitiesUnderMouse)
+ {
+ _entities.Add(ent.Uid);
+ }
}
- if (entities.Count == 0)
+ if (_entities.Count == 0)
return false;
if (visibility == MenuVisibility.All)
{
- result = entities;
+ result = new (_entities);
return true;
}
// remove any entities in containers
if ((visibility & MenuVisibility.InContainer) == 0)
{
- for (var i = entities.Count - 1; i >= 0; i--)
+ for (var i = _entities.Count - 1; i >= 0; i--)
{
- var entity = entities[i];
+ var entity = _entities[i];
if (ContainerSystem.IsInSameOrTransparentContainer(player.Value, entity))
continue;
- entities.RemoveSwap(i);
+ _entities.RemoveSwap(i);
}
}
@@ -127,23 +131,23 @@ public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true
{
var spriteQuery = GetEntityQuery();
- for (var i = entities.Count - 1; i >= 0; i--)
+ for (var i = _entities.Count - 1; i >= 0; i--)
{
- var entity = entities[i];
+ var entity = _entities[i];
if (!spriteQuery.TryGetComponent(entity, out var spriteComponent) ||
!spriteComponent.Visible ||
_tagSystem.HasTag(entity, "HideContextMenu"))
{
- entities.RemoveSwap(i);
+ _entities.RemoveSwap(i);
}
}
}
- if (entities.Count == 0)
+ if (_entities.Count == 0)
return false;
- result = entities;
+ result = new(_entities);
return true;
}
diff --git a/Content.IntegrationTests/Tests/ResearchTest.cs b/Content.IntegrationTests/Tests/ResearchTest.cs
index 7ae29a79ff..f50e6111da 100644
--- a/Content.IntegrationTests/Tests/ResearchTest.cs
+++ b/Content.IntegrationTests/Tests/ResearchTest.cs
@@ -98,4 +98,24 @@ await server.WaitAssertion(() =>
await pair.CleanReturnAsync();
}
+
+ [Test]
+ public async Task AllLatheRecipesValidTest()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+
+ var server = pair.Server;
+ var proto = server.ResolveDependency();
+
+ Assert.Multiple(() =>
+ {
+ foreach (var recipe in proto.EnumeratePrototypes())
+ {
+ if (recipe.Result == null)
+ Assert.That(recipe.ResultReagents, Is.Not.Null, $"Recipe '{recipe.ID}' has no result or result reagents.");
+ }
+ });
+
+ await pair.CleanReturnAsync();
+ }
}
diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs
index 99551c714c..ce18a1e4f7 100644
--- a/Content.Server/Administration/Systems/AdminSystem.cs
+++ b/Content.Server/Administration/Systems/AdminSystem.cs
@@ -98,6 +98,7 @@ public override void Initialize()
SubscribeLocalEvent(OnRoleEvent);
SubscribeLocalEvent(OnRoleEvent);
SubscribeLocalEvent(OnRoundRestartCleanup);
+ SubscribeLocalEvent(OnPlayerRenamed);
}
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
@@ -124,6 +125,11 @@ private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
}
}
+ private void OnPlayerRenamed(Entity ent, ref EntityRenamedEvent args)
+ {
+ UpdatePlayerList(ent.Comp.PlayerSession);
+ }
+
public void UpdatePlayerList(ICommonSession player)
{
_playerList[player.UserId] = GetPlayerInfo(player.Data, player);
diff --git a/Content.Server/Atmos/Commands/SetMapAtmosCommand.cs b/Content.Server/Atmos/Commands/SetMapAtmosCommand.cs
index 6f04cfb2da..92dd64eeca 100644
--- a/Content.Server/Atmos/Commands/SetMapAtmosCommand.cs
+++ b/Content.Server/Atmos/Commands/SetMapAtmosCommand.cs
@@ -54,7 +54,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args)
return;
}
- var mix = new GasMixture(Atmospherics.CellVolume) {Temperature = Math.Min(temp, Atmospherics.TCMB)};
+ var mix = new GasMixture(Atmospherics.CellVolume) {Temperature = Math.Max(temp, Atmospherics.TCMB)};
for (var i = 0; i < Atmospherics.TotalNumberOfGases; i++)
{
if (args.Length == 3 + i)
diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs
index 871c84e058..abd34396a0 100644
--- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs
+++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasPressurePumpSystem.cs
@@ -2,9 +2,9 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
-using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
+using Content.Server.Power.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping;
using Content.Shared.Atmos.Piping.Binary.Components;
@@ -13,6 +13,7 @@
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
+using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
@@ -39,6 +40,7 @@ public override void Initialize()
SubscribeLocalEvent(OnPumpLeaveAtmosphere);
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnPumpActivate);
+ SubscribeLocalEvent(OnPowerChanged);
// Bound UI subscriptions
SubscribeLocalEvent(OnOutputPressureChangeMessage);
SubscribeLocalEvent(OnToggleStatusMessage);
@@ -63,9 +65,15 @@ private void OnExamined(EntityUid uid, GasPressurePumpComponent pump, ExaminedEv
}
}
+ private void OnPowerChanged(EntityUid uid, GasPressurePumpComponent component, ref PowerChangedEvent args)
+ {
+ UpdateAppearance(uid, component);
+ }
+
private void OnPumpUpdated(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceUpdateEvent args)
{
if (!pump.Enabled
+ || (TryComp(uid, out var power) && !power.Powered)
|| !_nodeContainer.TryGetNodes(uid, pump.InletName, pump.OutletName, out PipeNode? inlet, out PipeNode? outlet))
{
_ambientSoundSystem.SetAmbience(uid, false);
@@ -154,7 +162,8 @@ private void UpdateAppearance(EntityUid uid, GasPressurePumpComponent? pump = nu
if (!Resolve(uid, ref pump, ref appearance, false))
return;
- _appearance.SetData(uid, PumpVisuals.Enabled, pump.Enabled, appearance);
+ bool pumpOn = pump.Enabled && (TryComp(uid, out var power) && power.Powered);
+ _appearance.SetData(uid, PumpVisuals.Enabled, pumpOn, appearance);
}
}
}
diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs
index d9fbeb474e..9ddd7dce67 100644
--- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs
+++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasVolumePumpSystem.cs
@@ -6,9 +6,9 @@
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
-using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
+using Content.Server.Power.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Visuals;
using Content.Shared.Audio;
@@ -17,6 +17,7 @@
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
+using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
@@ -45,6 +46,7 @@ public override void Initialize()
SubscribeLocalEvent(OnVolumePumpLeaveAtmosphere);
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnPumpActivate);
+ SubscribeLocalEvent(OnPowerChanged);
// Bound UI subscriptions
SubscribeLocalEvent(OnTransferRateChangeMessage);
SubscribeLocalEvent(OnToggleStatusMessage);
@@ -69,9 +71,15 @@ private void OnExamined(EntityUid uid, GasVolumePumpComponent pump, ExaminedEven
args.PushMarkup(str);
}
+ private void OnPowerChanged(EntityUid uid, GasVolumePumpComponent component, ref PowerChangedEvent args)
+ {
+ UpdateAppearance(uid, component);
+ }
+
private void OnVolumePumpUpdated(EntityUid uid, GasVolumePumpComponent pump, ref AtmosDeviceUpdateEvent args)
{
if (!pump.Enabled ||
+ (TryComp(uid, out var power) && !power.Powered) ||
!_nodeContainer.TryGetNodes(uid, pump.InletName, pump.OutletName, out PipeNode? inlet, out PipeNode? outlet))
{
_ambientSoundSystem.SetAmbience(uid, false);
@@ -183,7 +191,8 @@ private void UpdateAppearance(EntityUid uid, GasVolumePumpComponent? pump = null
if (!Resolve(uid, ref pump, ref appearance, false))
return;
- if (!pump.Enabled)
+ bool pumpOn = pump.Enabled && (TryComp(uid, out var power) && power.Powered);
+ if (!pumpOn)
_appearance.SetData(uid, GasVolumePumpVisuals.State, GasVolumePumpState.Off, appearance);
else if (pump.Blocked)
_appearance.SetData(uid, GasVolumePumpVisuals.State, GasVolumePumpState.Blocked, appearance);
diff --git a/Content.Server/Botany/Components/PlantHolderComponent.cs b/Content.Server/Botany/Components/PlantHolderComponent.cs
index 809af737ac..8218bead72 100644
--- a/Content.Server/Botany/Components/PlantHolderComponent.cs
+++ b/Content.Server/Botany/Components/PlantHolderComponent.cs
@@ -6,90 +6,90 @@ namespace Content.Server.Botany.Components;
[RegisterComponent]
public sealed partial class PlantHolderComponent : Component
{
- [DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate = TimeSpan.Zero;
- [ViewVariables(VVAccess.ReadWrite), DataField("updateDelay")]
+ [DataField]
public TimeSpan UpdateDelay = TimeSpan.FromSeconds(3);
- [DataField("lastProduce")]
+ [DataField]
public int LastProduce;
- [ViewVariables(VVAccess.ReadWrite), DataField("missingGas")]
+ [DataField]
public int MissingGas;
- [DataField("cycleDelay")]
+ [DataField]
public TimeSpan CycleDelay = TimeSpan.FromSeconds(15f);
- [DataField("lastCycle", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastCycle = TimeSpan.Zero;
- [ViewVariables(VVAccess.ReadWrite), DataField("updateSpriteAfterUpdate")]
+ [DataField]
public bool UpdateSpriteAfterUpdate;
- [ViewVariables(VVAccess.ReadWrite), DataField("drawWarnings")]
+ [DataField]
public bool DrawWarnings = false;
- [ViewVariables(VVAccess.ReadWrite), DataField("waterLevel")]
+ [DataField]
public float WaterLevel = 100f;
- [ViewVariables(VVAccess.ReadWrite), DataField("nutritionLevel")]
+ [DataField]
public float NutritionLevel = 100f;
- [ViewVariables(VVAccess.ReadWrite), DataField("pestLevel")]
+ [DataField]
public float PestLevel;
- [ViewVariables(VVAccess.ReadWrite), DataField("weedLevel")]
+ [DataField]
public float WeedLevel;
- [ViewVariables(VVAccess.ReadWrite), DataField("toxins")]
+ [DataField]
public float Toxins;
- [ViewVariables(VVAccess.ReadWrite), DataField("age")]
+ [DataField]
public int Age;
- [ViewVariables(VVAccess.ReadWrite), DataField("skipAging")]
+ [DataField]
public int SkipAging;
- [ViewVariables(VVAccess.ReadWrite), DataField("dead")]
+ [DataField]
public bool Dead;
- [ViewVariables(VVAccess.ReadWrite), DataField("harvest")]
+ [DataField]
public bool Harvest;
- [ViewVariables(VVAccess.ReadWrite), DataField("sampled")]
+ [DataField]
public bool Sampled;
- [ViewVariables(VVAccess.ReadWrite), DataField("yieldMod")]
+ [DataField]
public int YieldMod = 1;
- [ViewVariables(VVAccess.ReadWrite), DataField("mutationMod")]
+ [DataField]
public float MutationMod = 1f;
- [ViewVariables(VVAccess.ReadWrite), DataField("mutationLevel")]
+ [DataField]
public float MutationLevel;
- [ViewVariables(VVAccess.ReadWrite), DataField("health")]
+ [DataField]
public float Health;
- [ViewVariables(VVAccess.ReadWrite), DataField("weedCoefficient")]
+ [DataField]
public float WeedCoefficient = 1f;
- [ViewVariables(VVAccess.ReadWrite), DataField("seed")]
+ [DataField]
public SeedData? Seed;
- [ViewVariables(VVAccess.ReadWrite), DataField("improperHeat")]
+ [DataField]
public bool ImproperHeat;
- [ViewVariables(VVAccess.ReadWrite), DataField("improperPressure")]
+ [DataField]
public bool ImproperPressure;
- [ViewVariables(VVAccess.ReadWrite), DataField("improperLight")]
+ [DataField]
public bool ImproperLight;
- [ViewVariables(VVAccess.ReadWrite), DataField("forceUpdate")]
+ [DataField]
public bool ForceUpdate;
- [ViewVariables(VVAccess.ReadWrite), DataField("solution")]
+ [DataField]
public string SoilSolutionName = "soil";
[DataField]
diff --git a/Content.Server/Botany/Components/ProduceComponent.cs b/Content.Server/Botany/Components/ProduceComponent.cs
index b3c4e1c95a..db4ed62dd3 100644
--- a/Content.Server/Botany/Components/ProduceComponent.cs
+++ b/Content.Server/Botany/Components/ProduceComponent.cs
@@ -13,12 +13,12 @@ public sealed partial class ProduceComponent : SharedProduceComponent
///
/// Seed data used to create a when this produce has its seeds extracted.
///
- [DataField("seed")]
+ [DataField]
public SeedData? Seed;
///
/// Seed data used to create a when this produce has its seeds extracted.
///
- [DataField("seedId", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
public string? SeedId;
}
diff --git a/Content.Server/Botany/SeedPrototype.cs b/Content.Server/Botany/SeedPrototype.cs
index 39f06a6436..7a3e08883d 100644
--- a/Content.Server/Botany/SeedPrototype.cs
+++ b/Content.Server/Botany/SeedPrototype.cs
@@ -2,6 +2,7 @@
using Content.Server.Botany.Systems;
using Content.Shared.Atmos;
using Content.Shared.EntityEffects;
+using Content.Shared.Random;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -132,78 +133,67 @@ public partial class SeedData
[DataField("productPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer))]
public List ProductPrototypes = new();
- [DataField("chemicals")] public Dictionary Chemicals = new();
+ [DataField] public Dictionary Chemicals = new();
- [DataField("consumeGasses")] public Dictionary ConsumeGasses = new();
+ [DataField] public Dictionary ConsumeGasses = new();
- [DataField("exudeGasses")] public Dictionary ExudeGasses = new();
+ [DataField] public Dictionary ExudeGasses = new();
#endregion
#region Tolerances
- [DataField("nutrientConsumption")] public float NutrientConsumption = 0.75f;
+ [DataField] public float NutrientConsumption = 0.75f;
- [DataField("waterConsumption")] public float WaterConsumption = 0.5f;
- [DataField("idealHeat")] public float IdealHeat = 293f;
- [DataField("heatTolerance")] public float HeatTolerance = 10f;
- [DataField("idealLight")] public float IdealLight = 7f;
- [DataField("lightTolerance")] public float LightTolerance = 3f;
- [DataField("toxinsTolerance")] public float ToxinsTolerance = 4f;
+ [DataField] public float WaterConsumption = 0.5f;
+ [DataField] public float IdealHeat = 293f;
+ [DataField] public float HeatTolerance = 10f;
+ [DataField] public float IdealLight = 7f;
+ [DataField] public float LightTolerance = 3f;
+ [DataField] public float ToxinsTolerance = 4f;
- [DataField("lowPressureTolerance")] public float LowPressureTolerance = 81f;
+ [DataField] public float LowPressureTolerance = 81f;
- [DataField("highPressureTolerance")] public float HighPressureTolerance = 121f;
+ [DataField] public float HighPressureTolerance = 121f;
- [DataField("pestTolerance")] public float PestTolerance = 5f;
+ [DataField] public float PestTolerance = 5f;
- [DataField("weedTolerance")] public float WeedTolerance = 5f;
+ [DataField] public float WeedTolerance = 5f;
- [DataField("weedHighLevelThreshold")] public float WeedHighLevelThreshold = 10f;
+ [DataField] public float WeedHighLevelThreshold = 10f;
#endregion
#region General traits
- [DataField("endurance")] public float Endurance = 100f;
+ [DataField] public float Endurance = 100f;
- [DataField("yield")] public int Yield;
- [DataField("lifespan")] public float Lifespan;
- [DataField("maturation")] public float Maturation;
- [DataField("production")] public float Production;
- [DataField("growthStages")] public int GrowthStages = 6;
+ [DataField] public int Yield;
+ [DataField] public float Lifespan;
+ [DataField] public float Maturation;
+ [DataField] public float Production;
+ [DataField] public int GrowthStages = 6;
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("harvestRepeat")] public HarvestType HarvestRepeat = HarvestType.NoRepeat;
+ [DataField] public HarvestType HarvestRepeat = HarvestType.NoRepeat;
- [DataField("potency")] public float Potency = 1f;
+ [DataField] public float Potency = 1f;
///
/// If true, cannot be harvested for seeds. Balances hybrids and
/// mutations.
///
- [DataField("seedless")] public bool Seedless = false;
+ [DataField] public bool Seedless = false;
///
/// If false, rapidly decrease health while growing. Used to kill off
/// plants with "bad" mutations.
///
- [DataField("viable")] public bool Viable = true;
-
- ///
- /// If true, fruit slips players.
- ///
- [DataField("slip")] public bool Slip = false;
-
- ///
- /// If true, fruits are sentient.
- ///
- [DataField("sentient")] public bool Sentient = false;
+ [DataField] public bool Viable = true;
///
/// If true, a sharp tool is required to harvest this plant.
///
- [DataField("ligneous")] public bool Ligneous;
+ [DataField] public bool Ligneous;
// No, I'm not removing these.
// if you re-add these, make sure that they get cloned.
@@ -222,36 +212,35 @@ public partial class SeedData
#region Cosmetics
- [DataField("plantRsi", required: true)]
+ [DataField(required: true)]
public ResPath PlantRsi { get; set; } = default!;
- [DataField("plantIconState")] public string PlantIconState { get; set; } = "produce";
+ [DataField] public string PlantIconState { get; set; } = "produce";
///
- /// Screams random sound, could be strict sound SoundPathSpecifier or collection SoundCollectionSpecifier
- /// base class is SoundSpecifier
+ /// Screams random sound from collection SoundCollectionSpecifier
///
- [DataField("screamSound")]
+ [DataField]
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("PlantScreams", AudioParams.Default.WithVolume(-10));
[DataField("screaming")] public bool CanScream;
- [DataField("bioluminescent")] public bool Bioluminescent;
- [DataField("bioluminescentColor")] public Color BioluminescentColor { get; set; } = Color.White;
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string KudzuPrototype = "WeakKudzu";
- public float BioluminescentRadius = 2f;
-
- [DataField("kudzuPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] public string KudzuPrototype = "WeakKudzu";
-
- [DataField("turnIntoKudzu")] public bool TurnIntoKudzu;
- [DataField("splatPrototype")] public string? SplatPrototype { get; set; }
+ [DataField] public bool TurnIntoKudzu;
+ [DataField] public string? SplatPrototype { get; set; }
#endregion
+ ///
+ /// The mutation effects that have been applied to this plant.
+ ///
+ [DataField] public List Mutations { get; set; } = new();
+
///
/// The seed prototypes this seed may mutate into when prompted to.
///
- [DataField("mutationPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer))]
+ [DataField(customTypeSerializer: typeof(PrototypeIdListSerializer))]
public List MutationPrototypes = new();
public SeedData Clone()
@@ -295,17 +284,14 @@ public SeedData Clone()
Seedless = Seedless,
Viable = Viable,
- Slip = Slip,
- Sentient = Sentient,
Ligneous = Ligneous,
PlantRsi = PlantRsi,
PlantIconState = PlantIconState,
- Bioluminescent = Bioluminescent,
CanScream = CanScream,
TurnIntoKudzu = TurnIntoKudzu,
- BioluminescentColor = BioluminescentColor,
SplatPrototype = SplatPrototype,
+ Mutations = Mutations,
// Newly cloned seed is unique. No need to unnecessarily clone if repeatedly modified.
Unique = true,
@@ -356,18 +342,16 @@ public SeedData SpeciesChange(SeedData other)
HarvestRepeat = HarvestRepeat,
Potency = Potency,
+ Mutations = Mutations,
+
Seedless = Seedless,
Viable = Viable,
- Slip = Slip,
- Sentient = Sentient,
Ligneous = Ligneous,
PlantRsi = other.PlantRsi,
PlantIconState = other.PlantIconState,
- Bioluminescent = Bioluminescent,
CanScream = CanScream,
TurnIntoKudzu = TurnIntoKudzu,
- BioluminescentColor = BioluminescentColor,
SplatPrototype = other.SplatPrototype,
// Newly cloned seed is unique. No need to unnecessarily clone if repeatedly modified.
diff --git a/Content.Server/Botany/Systems/BotanySystem.Produce.cs b/Content.Server/Botany/Systems/BotanySystem.Produce.cs
index 34559a8304..8fdf96f57b 100644
--- a/Content.Server/Botany/Systems/BotanySystem.Produce.cs
+++ b/Content.Server/Botany/Systems/BotanySystem.Produce.cs
@@ -1,4 +1,5 @@
using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
namespace Content.Server.Botany.Systems;
@@ -10,6 +11,15 @@ public void ProduceGrown(EntityUid uid, ProduceComponent produce)
if (!TryGetSeed(produce, out var seed))
return;
+ foreach (var mutation in seed.Mutations)
+ {
+ if (mutation.AppliesToProduce)
+ {
+ var args = new EntityEffectBaseArgs(uid, EntityManager);
+ mutation.Effect.Effect(args);
+ }
+ }
+
if (!_solutionContainerSystem.EnsureSolution(uid,
produce.SolutionName,
out var solutionContainer,
diff --git a/Content.Server/Botany/Systems/BotanySystem.Seed.cs b/Content.Server/Botany/Systems/BotanySystem.Seed.cs
index c988e5338c..1487ed71d4 100644
--- a/Content.Server/Botany/Systems/BotanySystem.Seed.cs
+++ b/Content.Server/Botany/Systems/BotanySystem.Seed.cs
@@ -5,16 +5,11 @@
using Content.Shared.Botany;
using Content.Shared.Examine;
using Content.Shared.Hands.EntitySystems;
-using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
-using Content.Shared.Slippery;
-using Content.Shared.StepTrigger.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -34,7 +29,6 @@ public sealed partial class BotanySystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly FixtureSystem _fixtureSystem = default!;
- [Dependency] private readonly CollisionWakeSystem _colWakeSystem = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
public override void Initialize()
@@ -183,30 +177,6 @@ public IEnumerable GenerateProduct(SeedData proto, EntityCoordinates
_metaData.SetEntityDescription(entity,
metaData.EntityDescription + " " + Loc.GetString("botany-mysterious-description-addon"), metaData);
}
-
- if (proto.Bioluminescent)
- {
- var light = _light.EnsureLight(entity);
- _light.SetRadius(entity, proto.BioluminescentRadius, light);
- _light.SetColor(entity, proto.BioluminescentColor, light);
- // TODO: Ayo why you copy-pasting code between here and plantholder?
- _light.SetCastShadows(entity, false, light); // this is expensive, and botanists make lots of plants
- }
-
- if (proto.Slip)
- {
- var slippery = EnsureComp(entity);
- Dirty(entity, slippery);
- EnsureComp(entity);
- // Need a fixture with a slip layer in order to actually do the slipping
- var fixtures = EnsureComp(entity);
- var body = EnsureComp(entity);
- var shape = fixtures.Fixtures["fix1"].Shape;
- _fixtureSystem.TryCreateFixture(entity, shape, "slips", 1, false, (int) CollisionGroup.SlipLayer, manager: fixtures, body: body);
- // Need to disable collision wake so that mobs can collide with and slip on it
- var collisionWake = EnsureComp(entity);
- _colWakeSystem.SetEnabled(entity, false, collisionWake);
- }
}
return products;
diff --git a/Content.Server/Botany/Systems/MutationSystem.cs b/Content.Server/Botany/Systems/MutationSystem.cs
index d3159655f5..07a24d19f6 100644
--- a/Content.Server/Botany/Systems/MutationSystem.cs
+++ b/Content.Server/Botany/Systems/MutationSystem.cs
@@ -1,9 +1,9 @@
+using Content.Shared.Atmos;
+using Content.Shared.EntityEffects;
+using Content.Shared.Random;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
-using Content.Shared.Random;
-using Content.Shared.Random.Helpers;
using System.Linq;
-using Content.Shared.Atmos;
namespace Content.Server.Botany;
@@ -11,25 +11,40 @@ public sealed class MutationSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- private WeightedRandomFillSolutionPrototype _randomChems = default!;
-
+ private RandomPlantMutationListPrototype _randomMutations = default!;
public override void Initialize()
{
- _randomChems = _prototypeManager.Index("RandomPickBotanyReagent");
+ _randomMutations = _prototypeManager.Index("RandomPlantMutations");
+ }
+
+ ///
+ /// For each random mutation, see if it occurs on this plant this check.
+ ///
+ ///
+ ///
+ public void CheckRandomMutations(EntityUid plantHolder, ref SeedData seed, float severity)
+ {
+ foreach (var mutation in _randomMutations.mutations)
+ {
+ if (Random(mutation.BaseOdds * severity))
+ {
+ if (mutation.AppliesToPlant)
+ {
+ var args = new EntityEffectBaseArgs(plantHolder, EntityManager);
+ mutation.Effect.Effect(args);
+ }
+ // Stat adjustments do not persist by being an attached effect, they just change the stat.
+ if (mutation.Persists && !seed.Mutations.Any(m => m.Name == mutation.Name))
+ seed.Mutations.Add(mutation);
+ }
+ }
}
///
- /// Main idea: Simulate genetic mutation using random binary flips. Each
- /// seed attribute can be encoded with a variable number of bits, e.g.
- /// NutrientConsumption is represented by 5 bits randomly distributed in the
- /// plant's genome which thermometer code the floating value between 0.1 and
- /// 5. 1 unit of mutation flips one bit in the plant's genome, which changes
- /// NutrientConsumption if one of those 5 bits gets affected.
- ///
- /// You MUST clone() seed before mutating it!
+ /// Checks all defined mutations against a seed to see which of them are applied.
///
- public void MutateSeed(ref SeedData seed, float severity)
+ public void MutateSeed(EntityUid plantHolder, ref SeedData seed, float severity)
{
if (!seed.Unique)
{
@@ -37,57 +52,7 @@ public void MutateSeed(ref SeedData seed, float severity)
return;
}
- // Add up everything in the bits column and put the number here.
- const int totalbits = 262;
-
- #pragma warning disable IDE0055 // disable formatting warnings because this looks more readable
- // Tolerances (55)
- MutateFloat(ref seed.NutrientConsumption , 0.05f, 1.2f, 5, totalbits, severity);
- MutateFloat(ref seed.WaterConsumption , 3f , 9f , 5, totalbits, severity);
- MutateFloat(ref seed.IdealHeat , 263f , 323f, 5, totalbits, severity);
- MutateFloat(ref seed.HeatTolerance , 2f , 25f , 5, totalbits, severity);
- MutateFloat(ref seed.IdealLight , 0f , 14f , 5, totalbits, severity);
- MutateFloat(ref seed.LightTolerance , 1f , 5f , 5, totalbits, severity);
- MutateFloat(ref seed.ToxinsTolerance , 1f , 10f , 5, totalbits, severity);
- MutateFloat(ref seed.LowPressureTolerance , 60f , 100f, 5, totalbits, severity);
- MutateFloat(ref seed.HighPressureTolerance, 100f , 140f, 5, totalbits, severity);
- MutateFloat(ref seed.PestTolerance , 0f , 15f , 5, totalbits, severity);
- MutateFloat(ref seed.WeedTolerance , 0f , 15f , 5, totalbits, severity);
-
- // Stats (30*2 = 60)
- MutateFloat(ref seed.Endurance , 50f , 150f, 5, totalbits, 2 * severity);
- MutateInt(ref seed.Yield , 3 , 10 , 5, totalbits, 2 * severity);
- MutateFloat(ref seed.Lifespan , 10f , 80f , 5, totalbits, 2 * severity);
- MutateFloat(ref seed.Maturation , 3f , 8f , 5, totalbits, 2 * severity);
- MutateFloat(ref seed.Production , 1f , 10f , 5, totalbits, 2 * severity);
- MutateFloat(ref seed.Potency , 30f , 100f, 5, totalbits, 2 * severity);
-
- // Kill the plant (30)
- MutateBool(ref seed.Viable , false, 30, totalbits, severity);
-
- // Fun (72)
- MutateBool(ref seed.Seedless , true , 10, totalbits, severity);
- MutateBool(ref seed.Slip , true , 10, totalbits, severity);
- MutateBool(ref seed.Sentient , true , 2 , totalbits, severity);
- MutateBool(ref seed.Ligneous , true , 10, totalbits, severity);
- MutateBool(ref seed.Bioluminescent, true , 10, totalbits, severity);
- MutateBool(ref seed.TurnIntoKudzu , true , 10, totalbits, severity);
- MutateBool(ref seed.CanScream , true , 10, totalbits, severity);
- seed.BioluminescentColor = RandomColor(seed.BioluminescentColor, 10, totalbits, severity);
- #pragma warning restore IDE0055
-
- // ConstantUpgade (10)
- MutateHarvestType(ref seed.HarvestRepeat, 10, totalbits, severity);
-
- // Gas (5)
- MutateGasses(ref seed.ExudeGasses, 0.01f, 0.5f, 4, totalbits, severity);
- MutateGasses(ref seed.ConsumeGasses, 0.01f, 0.5f, 1, totalbits, severity);
-
- // Chems (20)
- MutateChemicals(ref seed.Chemicals, 20, totalbits, severity);
-
- // Species (10)
- MutateSpecies(ref seed, 10, totalbits, severity);
+ CheckRandomMutations(plantHolder, ref seed, severity);
}
public SeedData Cross(SeedData a, SeedData b)
@@ -115,19 +80,18 @@ public SeedData Cross(SeedData a, SeedData b)
CrossFloat(ref result.Production, a.Production);
CrossFloat(ref result.Potency, a.Potency);
- // we do not transfer Sentient to another plant to avoid ghost role spam
CrossBool(ref result.Seedless, a.Seedless);
- CrossBool(ref result.Viable, a.Viable);
- CrossBool(ref result.Slip, a.Slip);
CrossBool(ref result.Ligneous, a.Ligneous);
- CrossBool(ref result.Bioluminescent, a.Bioluminescent);
CrossBool(ref result.TurnIntoKudzu, a.TurnIntoKudzu);
CrossBool(ref result.CanScream, a.CanScream);
CrossGasses(ref result.ExudeGasses, a.ExudeGasses);
CrossGasses(ref result.ConsumeGasses, a.ConsumeGasses);
- result.BioluminescentColor = Random(0.5f) ? a.BioluminescentColor : result.BioluminescentColor;
+ // LINQ Explanation
+ // For the list of mutation effects on both plants, use a 50% chance to pick each one.
+ // Union all of the chosen mutations into one list, and pick ones with a Distinct (unique) name.
+ result.Mutations = result.Mutations.Where(m => Random(0.5f)).Union(a.Mutations.Where(m => Random(0.5f))).DistinctBy(m => m.Name).ToList();
// Hybrids have a high chance of being seedless. Balances very
// effective hybrid crossings.
@@ -139,206 +103,6 @@ public SeedData Cross(SeedData a, SeedData b)
return result;
}
- // Mutate reference 'val' between 'min' and 'max' by pretending the value
- // is representable by a thermometer code with 'bits' number of bits and
- // randomly flipping some of them.
- //
- // 'totalbits' and 'mult' are used only to calculate the probability that
- // one bit gets flipped.
- private void MutateFloat(ref float val, float min, float max, int bits, int totalbits, float mult)
- {
- // Probability that a bit flip happens for this value's representation in thermometer code.
- float probBitflip = mult * bits / totalbits;
- probBitflip = Math.Clamp(probBitflip, 0, 1);
- if (!Random(probBitflip))
- return;
-
- if (min == max)
- {
- val = min;
- return;
- }
-
- // Starting number of bits that are high, between 0 and bits.
- // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
- int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
- // val may be outside the range of min/max due to starting prototype values, so clamp.
- valInt = Math.Clamp(valInt, 0, bits);
-
- // Probability that the bit flip increases n.
- // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it.
- // In other words, it tends to go to the middle.
- float probIncrease = 1 - (float)valInt / bits;
- int valIntMutated;
- if (Random(probIncrease))
- {
- valIntMutated = valInt + 1;
- }
- else
- {
- valIntMutated = valInt - 1;
- }
-
- // Set value based on mutated thermometer code.
- float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max);
- val = valMutated;
- }
-
- private void MutateInt(ref int val, int min, int max, int bits, int totalbits, float mult)
- {
- // Probability that a bit flip happens for this value's representation in thermometer code.
- float probBitflip = mult * bits / totalbits;
- probBitflip = Math.Clamp(probBitflip, 0, 1);
- if (!Random(probBitflip))
- return;
-
- if (min == max)
- {
- val = min;
- return;
- }
-
- // Starting number of bits that are high, between 0 and bits.
- // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
- int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
- // val may be outside the range of min/max due to starting prototype values, so clamp.
- valInt = Math.Clamp(valInt, 0, bits);
-
- // Probability that the bit flip increases n.
- // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it.
- // In other words, it tends to go to the middle.
- float probIncrease = 1 - (float)valInt / bits;
- int valMutated;
- if (Random(probIncrease))
- {
- valMutated = val + 1;
- }
- else
- {
- valMutated = val - 1;
- }
-
- valMutated = Math.Clamp(valMutated, min, max);
- val = valMutated;
- }
-
- private void MutateBool(ref bool val, bool polarity, int bits, int totalbits, float mult)
- {
- // Probability that a bit flip happens for this value.
- float probSet = mult * bits / totalbits;
- probSet = Math.Clamp(probSet, 0, 1);
- if (!Random(probSet))
- return;
-
- val = polarity;
- }
-
- private void MutateHarvestType(ref HarvestType val, int bits, int totalbits, float mult)
- {
- float probModify = mult * bits / totalbits;
- probModify = Math.Clamp(probModify, 0, 1);
-
- if (!Random(probModify))
- return;
-
- if (val == HarvestType.NoRepeat)
- val = HarvestType.Repeat;
- else if (val == HarvestType.Repeat)
- val = HarvestType.SelfHarvest;
- }
-
- private void MutateGasses(ref Dictionary gasses, float min, float max, int bits, int totalbits, float mult)
- {
- float probModify = mult * bits / totalbits;
- probModify = Math.Clamp(probModify, 0, 1);
- if (!Random(probModify))
- return;
-
- // Add a random amount of a random gas to this gas dictionary
- float amount = _robustRandom.NextFloat(min, max);
- Gas gas = _robustRandom.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList());
- if (gasses.ContainsKey(gas))
- {
- gasses[gas] += amount;
- }
- else
- {
- gasses.Add(gas, amount);
- }
- }
-
- private void MutateChemicals(ref Dictionary chemicals, int bits, int totalbits, float mult)
- {
- float probModify = mult * bits / totalbits;
- probModify = Math.Clamp(probModify, 0, 1);
- if (!Random(probModify))
- return;
-
- // Add a random amount of a random chemical to this set of chemicals
- if (_randomChems != null)
- {
- var pick = _randomChems.Pick(_robustRandom);
- string chemicalId = pick.reagent;
- int amount = _robustRandom.Next(1, (int)pick.quantity);
- SeedChemQuantity seedChemQuantity = new SeedChemQuantity();
- if (chemicals.ContainsKey(chemicalId))
- {
- seedChemQuantity.Min = chemicals[chemicalId].Min;
- seedChemQuantity.Max = chemicals[chemicalId].Max + amount;
- }
- else
- {
- seedChemQuantity.Min = 1;
- seedChemQuantity.Max = 1 + amount;
- seedChemQuantity.Inherent = false;
- }
- int potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max);
- seedChemQuantity.PotencyDivisor = potencyDivisor;
- chemicals[chemicalId] = seedChemQuantity;
- }
- }
-
- private void MutateSpecies(ref SeedData seed, int bits, int totalbits, float mult)
- {
- float p = mult * bits / totalbits;
- p = Math.Clamp(p, 0, 1);
- if (!Random(p))
- return;
-
- if (seed.MutationPrototypes.Count == 0)
- return;
-
- var targetProto = _robustRandom.Pick(seed.MutationPrototypes);
- _prototypeManager.TryIndex(targetProto, out SeedPrototype? protoSeed);
-
- if (protoSeed == null)
- {
- Log.Error($"Seed prototype could not be found: {targetProto}!");
- return;
- }
-
- seed = seed.SpeciesChange(protoSeed);
- }
-
- private Color RandomColor(Color color, int bits, int totalbits, float mult)
- {
- float probModify = mult * bits / totalbits;
- if (Random(probModify))
- {
- var colors = new List{
- Color.White,
- Color.Red,
- Color.Yellow,
- Color.Green,
- Color.Blue,
- Color.Purple,
- Color.Pink
- };
- return _robustRandom.Pick(colors);
- }
- return color;
- }
-
private void CrossChemicals(ref Dictionary val, Dictionary other)
{
// Go through chemicals from the pollen in swab
diff --git a/Content.Server/Botany/Systems/PlantHolderSystem.cs b/Content.Server/Botany/Systems/PlantHolderSystem.cs
index 002a054339..0fdca029b7 100644
--- a/Content.Server/Botany/Systems/PlantHolderSystem.cs
+++ b/Content.Server/Botany/Systems/PlantHolderSystem.cs
@@ -1,8 +1,6 @@
-using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Botany.Components;
using Content.Server.Fluids.Components;
-using Content.Server.Ghost.Roles.Components;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
@@ -79,7 +77,7 @@ private int GetCurrentGrowthStage(Entity entity)
if (component.Seed == null)
return 0;
- var result = Math.Max(1, (int) (component.Age * component.Seed.GrowthStages / component.Seed.Maturation));
+ var result = Math.Max(1, (int)(component.Age * component.Seed.GrowthStages / component.Seed.Maturation));
return result;
}
@@ -125,9 +123,9 @@ private void OnExamine(Entity entity, ref ExaminedEvent ar
args.PushMarkup(Loc.GetString("plant-holder-component-pest-high-level-message"));
args.PushMarkup(Loc.GetString($"plant-holder-component-water-level-message",
- ("waterLevel", (int) component.WaterLevel)));
+ ("waterLevel", (int)component.WaterLevel)));
args.PushMarkup(Loc.GetString($"plant-holder-component-nutrient-level-message",
- ("nutritionLevel", (int) component.NutritionLevel)));
+ ("nutritionLevel", (int)component.NutritionLevel)));
if (component.DrawWarnings)
{
@@ -299,21 +297,12 @@ private void OnInteractUsing(Entity entity, ref InteractUs
healthOverride = component.Health;
}
var packetSeed = component.Seed;
- if (packetSeed.Sentient)
- {
- packetSeed = packetSeed.Clone(); // clone before modifying the seed
- packetSeed.Sentient = false;
- }
- else
- {
- packetSeed.Unique = false;
- }
var seed = _botany.SpawnSeedPacket(packetSeed, Transform(args.User).Coordinates, args.User, healthOverride);
_randomHelper.RandomOffset(seed, 0.25f);
var displayName = Loc.GetString(component.Seed.DisplayName);
_popup.PopupCursor(Loc.GetString("plant-holder-component-take-sample-message",
("seedName", displayName)), args.User);
-
+
DoScream(entity.Owner, component.Seed);
if (_random.Prob(0.3f))
@@ -459,7 +448,7 @@ public void Update(EntityUid uid, PlantHolderComponent? component = null)
else
{
if (_random.Prob(0.8f))
- component.Age += (int) (1 * HydroponicsSpeedMultiplier);
+ component.Age += (int)(1 * HydroponicsSpeedMultiplier);
component.UpdateSpriteAfterUpdate = true;
}
@@ -632,12 +621,6 @@ public void Update(EntityUid uid, PlantHolderComponent? component = null)
else if (component.Age < 0) // Revert back to seed packet!
{
var packetSeed = component.Seed;
- if (packetSeed.Sentient)
- {
- if (!packetSeed.Unique) // clone if necessary before modifying the seed
- packetSeed = packetSeed.Clone();
- packetSeed.Sentient = false; // remove Sentient to avoid ghost role spam
- }
// will put it in the trays hands if it has any, please do not try doing this
_botany.SpawnSeedPacket(packetSeed, Transform(uid).Coordinates, uid);
RemovePlant(uid, component);
@@ -674,14 +657,6 @@ public void Update(EntityUid uid, PlantHolderComponent? component = null)
CheckLevelSanity(uid, component);
- if (component.Seed.Sentient)
- {
- var ghostRole = EnsureComp(uid);
- EnsureComp(uid);
- ghostRole.RoleName = MetaData(uid).EntityName;
- ghostRole.RoleDescription = Loc.GetString("station-event-random-sentience-role-description", ("name", ghostRole.RoleName));
- }
-
if (component.UpdateSpriteAfterUpdate)
UpdateSprite(uid, component);
}
@@ -911,7 +886,7 @@ private void Mutate(EntityUid uid, float severity, PlantHolderComponent? compone
if (component.Seed != null)
{
EnsureUniqueSeed(uid, component);
- _mutation.MutateSeed(ref component.Seed, severity);
+ _mutation.MutateSeed(uid, ref component.Seed, severity);
}
}
@@ -922,19 +897,6 @@ public void UpdateSprite(EntityUid uid, PlantHolderComponent? component = null)
component.UpdateSpriteAfterUpdate = false;
- if (component.Seed != null && component.Seed.Bioluminescent)
- {
- var light = EnsureComp(uid);
- _pointLight.SetRadius(uid, component.Seed.BioluminescentRadius, light);
- _pointLight.SetColor(uid, component.Seed.BioluminescentColor, light);
- _pointLight.SetCastShadows(uid, false, light);
- Dirty(uid, light);
- }
- else
- {
- RemComp(uid);
- }
-
if (!TryComp(uid, out var app))
return;
diff --git a/Content.Server/Botany/Systems/SeedExtractorSystem.cs b/Content.Server/Botany/Systems/SeedExtractorSystem.cs
index 4a0d56bfe9..93f76473ff 100644
--- a/Content.Server/Botany/Systems/SeedExtractorSystem.cs
+++ b/Content.Server/Botany/Systems/SeedExtractorSystem.cs
@@ -43,12 +43,6 @@ private void OnInteractUsing(EntityUid uid, SeedExtractorComponent seedExtractor
var coords = Transform(uid).Coordinates;
var packetSeed = seed;
- if (packetSeed.Sentient)
- {
- if (!packetSeed.Unique) // clone if necessary before modifying the seed
- packetSeed = packetSeed.Clone();
- packetSeed.Sentient = false; // remove Sentient to avoid ghost role spam
- }
if (amount > 1)
packetSeed.Unique = false;
diff --git a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
index d94faa8123..c5c45daa5b 100644
--- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
@@ -327,8 +327,23 @@ private bool TryDraw(Entity injector, Entity injector, Entity
+/// Makes a mob glow.
+///
+public sealed partial class Glow : EntityEffect
+{
+ [DataField]
+ public float Radius = 2f;
+
+ [DataField]
+ public Color Color = Color.Black;
+
+ private static readonly List Colors = new()
+ {
+ Color.White,
+ Color.Red,
+ Color.Yellow,
+ Color.Green,
+ Color.Blue,
+ Color.Purple,
+ Color.Pink
+ };
+
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ if (Color == Color.Black)
+ {
+ var random = IoCManager.Resolve();
+ Color = random.Pick(Colors);
+ }
+
+ var lightSystem = args.EntityManager.System();
+ var light = lightSystem.EnsureLight(args.TargetEntity);
+ lightSystem.SetRadius(args.TargetEntity, Radius, light);
+ lightSystem.SetColor(args.TargetEntity, Color, light);
+ lightSystem.SetCastShadows(args.TargetEntity, false, light); // this is expensive, and botanists make lots of plants
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PlantChangeStat.cs b/Content.Server/EntityEffects/Effects/PlantChangeStat.cs
new file mode 100644
index 0000000000..9592ff779d
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PlantChangeStat.cs
@@ -0,0 +1,142 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects.PlantMetabolism;
+
+[UsedImplicitly]
+public sealed partial class PlantChangeStat : EntityEffect
+{
+ [DataField]
+ public string TargetValue;
+
+ [DataField]
+ public float MinValue;
+
+ [DataField]
+ public float MaxValue;
+
+ [DataField]
+ public int Steps;
+
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var plantHolder = args.EntityManager.GetComponent(args.TargetEntity);
+ if (plantHolder == null || plantHolder.Seed == null)
+ return;
+
+ var member = plantHolder.Seed.GetType().GetField(TargetValue);
+ var mutationSys = args.EntityManager.System();
+
+ if (member == null)
+ {
+ mutationSys.Log.Error(this.GetType().Name + " Error: Member " + TargetValue + " not found on " + plantHolder.GetType().Name + ". Did you misspell it?");
+ return;
+ }
+
+ var currentValObj = member.GetValue(plantHolder.Seed);
+ if (currentValObj == null)
+ return;
+
+ if (member.FieldType == typeof(float))
+ {
+ var floatVal = (float)currentValObj;
+ MutateFloat(ref floatVal, MinValue, MaxValue, Steps);
+ member.SetValue(plantHolder.Seed, floatVal);
+ }
+ else if (member.FieldType == typeof(int))
+ {
+ var intVal = (int)currentValObj;
+ MutateInt(ref intVal, (int)MinValue, (int)MaxValue, Steps);
+ member.SetValue(plantHolder.Seed, intVal);
+ }
+ else if (member.FieldType == typeof(bool))
+ {
+ var boolVal = (bool)currentValObj;
+ boolVal = !boolVal;
+ member.SetValue(plantHolder.Seed, boolVal);
+ }
+ }
+
+ // Mutate reference 'val' between 'min' and 'max' by pretending the value
+ // is representable by a thermometer code with 'bits' number of bits and
+ // randomly flipping some of them.
+ private void MutateFloat(ref float val, float min, float max, int bits)
+ {
+ if (min == max)
+ {
+ val = min;
+ return;
+ }
+
+ // Starting number of bits that are high, between 0 and bits.
+ // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
+ int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
+ // val may be outside the range of min/max due to starting prototype values, so clamp.
+ valInt = Math.Clamp(valInt, 0, bits);
+
+ // Probability that the bit flip increases n.
+ // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it.
+ // In other words, it tends to go to the middle.
+ float probIncrease = 1 - (float)valInt / bits;
+ int valIntMutated;
+ if (Random(probIncrease))
+ {
+ valIntMutated = valInt + 1;
+ }
+ else
+ {
+ valIntMutated = valInt - 1;
+ }
+
+ // Set value based on mutated thermometer code.
+ float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max);
+ val = valMutated;
+ }
+
+ private void MutateInt(ref int val, int min, int max, int bits)
+ {
+ if (min == max)
+ {
+ val = min;
+ return;
+ }
+
+ // Starting number of bits that are high, between 0 and bits.
+ // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
+ int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
+ // val may be outside the range of min/max due to starting prototype values, so clamp.
+ valInt = Math.Clamp(valInt, 0, bits);
+
+ // Probability that the bit flip increases n.
+ // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it.
+ // In other words, it tends to go to the middle.
+ float probIncrease = 1 - (float)valInt / bits;
+ int valMutated;
+ if (Random(probIncrease))
+ {
+ valMutated = val + 1;
+ }
+ else
+ {
+ valMutated = val - 1;
+ }
+
+ valMutated = Math.Clamp(valMutated, min, max);
+ val = valMutated;
+ }
+
+ private bool Random(float odds)
+ {
+ var random = IoCManager.Resolve();
+ return random.Prob(odds);
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PlantMutateChemicals.cs b/Content.Server/EntityEffects/Effects/PlantMutateChemicals.cs
new file mode 100644
index 0000000000..7ee6cd13d7
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PlantMutateChemicals.cs
@@ -0,0 +1,55 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.Random;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// changes the chemicals available in a plant's produce
+///
+public sealed partial class PlantMutateChemicals : EntityEffect
+{
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var plantholder = args.EntityManager.GetComponent(args.TargetEntity);
+
+ if (plantholder.Seed == null)
+ return;
+
+ var random = IoCManager.Resolve();
+ var prototypeManager = IoCManager.Resolve();
+ var chemicals = plantholder.Seed.Chemicals;
+ var randomChems = prototypeManager.Index("RandomPickBotanyReagent").Fills;
+
+ // Add a random amount of a random chemical to this set of chemicals
+ if (randomChems != null)
+ {
+ var pick = random.Pick(randomChems);
+ var chemicalId = random.Pick(pick.Reagents);
+ var amount = random.Next(1, (int)pick.Quantity);
+ var seedChemQuantity = new SeedChemQuantity();
+ if (chemicals.ContainsKey(chemicalId))
+ {
+ seedChemQuantity.Min = chemicals[chemicalId].Min;
+ seedChemQuantity.Max = chemicals[chemicalId].Max + amount;
+ }
+ else
+ {
+ seedChemQuantity.Min = 1;
+ seedChemQuantity.Max = 1 + amount;
+ seedChemQuantity.Inherent = false;
+ }
+ var potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max);
+ seedChemQuantity.PotencyDivisor = potencyDivisor;
+ chemicals[chemicalId] = seedChemQuantity;
+ }
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PlantMutateGases.cs b/Content.Server/EntityEffects/Effects/PlantMutateGases.cs
new file mode 100644
index 0000000000..52b9da3a85
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PlantMutateGases.cs
@@ -0,0 +1,87 @@
+using Content.Server.Botany.Components;
+using Content.Shared.Atmos;
+using Content.Shared.EntityEffects;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using System.Linq;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// changes the gases that a plant or produce create.
+///
+public sealed partial class PlantMutateExudeGasses : EntityEffect
+{
+ [DataField]
+ public float MinValue = 0.01f;
+
+ [DataField]
+ public float MaxValue = 0.5f;
+
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var plantholder = args.EntityManager.GetComponent(args.TargetEntity);
+
+ if (plantholder.Seed == null)
+ return;
+
+ var random = IoCManager.Resolve();
+ var gasses = plantholder.Seed.ExudeGasses;
+
+ // Add a random amount of a random gas to this gas dictionary
+ float amount = random.NextFloat(MinValue, MaxValue);
+ Gas gas = random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList());
+ if (gasses.ContainsKey(gas))
+ {
+ gasses[gas] += amount;
+ }
+ else
+ {
+ gasses.Add(gas, amount);
+ }
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
+
+///
+/// changes the gases that a plant or produce consumes.
+///
+public sealed partial class PlantMutateConsumeGasses : EntityEffect
+{
+ [DataField]
+ public float MinValue = 0.01f;
+
+ [DataField]
+ public float MaxValue = 0.5f;
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var plantholder = args.EntityManager.GetComponent(args.TargetEntity);
+
+ if (plantholder.Seed == null)
+ return;
+
+ var random = IoCManager.Resolve();
+ var gasses = plantholder.Seed.ConsumeGasses;
+
+ // Add a random amount of a random gas to this gas dictionary
+ float amount = random.NextFloat(MinValue, MaxValue);
+ Gas gas = random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList());
+ if (gasses.ContainsKey(gas))
+ {
+ gasses[gas] += amount;
+ }
+ else
+ {
+ gasses.Add(gas, amount);
+ }
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PlantMutateHarvest.cs b/Content.Server/EntityEffects/Effects/PlantMutateHarvest.cs
new file mode 100644
index 0000000000..e67176ee16
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PlantMutateHarvest.cs
@@ -0,0 +1,30 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// Upgrades a plant's harvest type.
+///
+public sealed partial class PlantMutateHarvest : EntityEffect
+{
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var plantholder = args.EntityManager.GetComponent(args.TargetEntity);
+
+ if (plantholder.Seed == null)
+ return;
+
+ if (plantholder.Seed.HarvestRepeat == HarvestType.NoRepeat)
+ plantholder.Seed.HarvestRepeat = HarvestType.Repeat;
+ else if (plantholder.Seed.HarvestRepeat == HarvestType.Repeat)
+ plantholder.Seed.HarvestRepeat = HarvestType.SelfHarvest;
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PlantSpeciesChange.cs b/Content.Server/EntityEffects/Effects/PlantSpeciesChange.cs
new file mode 100644
index 0000000000..65bd59daa3
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PlantSpeciesChange.cs
@@ -0,0 +1,43 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Serilog;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// Changes a plant into one of the species its able to mutate into.
+///
+public sealed partial class PlantSpeciesChange : EntityEffect
+{
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var prototypeManager = IoCManager.Resolve();
+ var plantholder = args.EntityManager.GetComponent(args.TargetEntity);
+
+ if (plantholder.Seed == null)
+ return;
+
+ if (plantholder.Seed.MutationPrototypes.Count == 0)
+ return;
+
+ var random = IoCManager.Resolve();
+ var targetProto = random.Pick(plantholder.Seed.MutationPrototypes);
+ prototypeManager.TryIndex(targetProto, out SeedPrototype? protoSeed);
+
+ if (protoSeed == null)
+ {
+ Log.Error($"Seed prototype could not be found: {targetProto}!");
+ return;
+ }
+
+ plantholder.Seed = plantholder.Seed.SpeciesChange(protoSeed);
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Slipify.cs b/Content.Server/EntityEffects/Effects/Slipify.cs
new file mode 100644
index 0000000000..bc1cc062a3
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Slipify.cs
@@ -0,0 +1,38 @@
+using Content.Shared.EntityEffects;
+using Content.Shared.Physics;
+using Content.Shared.Slippery;
+using Content.Shared.StepTrigger.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// Makes a mob slippery.
+///
+public sealed partial class Slipify : EntityEffect
+{
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var fixtureSystem = args.EntityManager.System();
+ var colWakeSystem = args.EntityManager.System();
+ var slippery = args.EntityManager.EnsureComponent(args.TargetEntity);
+ args.EntityManager.Dirty(args.TargetEntity, slippery);
+ args.EntityManager.EnsureComponent(args.TargetEntity);
+ // Need a fixture with a slip layer in order to actually do the slipping
+ var fixtures = args.EntityManager.EnsureComponent(args.TargetEntity);
+ var body = args.EntityManager.EnsureComponent(args.TargetEntity);
+ var shape = fixtures.Fixtures["fix1"].Shape;
+ fixtureSystem.TryCreateFixture(args.TargetEntity, shape, "slips", 1, false, (int)CollisionGroup.SlipLayer, manager: fixtures, body: body);
+ // Need to disable collision wake so that mobs can collide with and slip on it
+ var collisionWake = args.EntityManager.EnsureComponent(args.TargetEntity);
+ colWakeSystem.SetEnabled(args.TargetEntity, false, collisionWake);
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Content.Server/Forensics/Systems/ForensicPadSystem.cs b/Content.Server/Forensics/Systems/ForensicPadSystem.cs
index 42512cb1fd..a3f5627cdb 100644
--- a/Content.Server/Forensics/Systems/ForensicPadSystem.cs
+++ b/Content.Server/Forensics/Systems/ForensicPadSystem.cs
@@ -1,10 +1,11 @@
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Inventory;
+using Content.Server.Labels;
using Content.Server.Popups;
using Content.Shared.DoAfter;
+using Content.Shared.Examine;
using Content.Shared.Forensics;
using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Inventory;
namespace Content.Server.Forensics
{
@@ -17,6 +18,7 @@ public sealed class ForensicPadSystem : EntitySystem
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
+ [Dependency] private readonly LabelSystem _label = default!;
public override void Initialize()
{
@@ -99,10 +101,8 @@ private void OnDoAfter(EntityUid uid, ForensicPadComponent padComponent, Forensi
if (args.Args.Target != null)
{
- var name = HasComp(args.Args.Target)
- ? "forensic-pad-fingerprint-name"
- : "forensic-pad-gloves-name";
- _metaData.SetEntityName(uid, Loc.GetString(name, ("entity", args.Args.Target)));
+ string label = Identity.Name(args.Args.Target.Value, EntityManager);
+ _label.Label(uid, label);
}
padComponent.Sample = args.Sample;
diff --git a/Content.Server/Gravity/GravityGeneratorSystem.cs b/Content.Server/Gravity/GravityGeneratorSystem.cs
index 5ab2dc8931..9d58b82d69 100644
--- a/Content.Server/Gravity/GravityGeneratorSystem.cs
+++ b/Content.Server/Gravity/GravityGeneratorSystem.cs
@@ -36,8 +36,10 @@ public override void Update(float frameTime)
private void OnActivated(Entity ent, ref ChargedMachineActivatedEvent args)
{
ent.Comp.GravityActive = true;
- if (TryComp(ent, out var xform) &&
- TryComp(xform.ParentUid, out GravityComponent? gravity))
+
+ var xform = Transform(ent);
+
+ if (TryComp(xform.ParentUid, out GravityComponent? gravity))
{
_gravitySystem.EnableGravity(xform.ParentUid, gravity);
}
@@ -46,8 +48,10 @@ private void OnActivated(Entity ent, ref ChargedMachi
private void OnDeactivated(Entity ent, ref ChargedMachineDeactivatedEvent args)
{
ent.Comp.GravityActive = false;
- if (TryComp(ent, out var xform) &&
- TryComp(xform.ParentUid, out GravityComponent? gravity))
+
+ var xform = Transform(ent);
+
+ if (TryComp(xform.ParentUid, out GravityComponent? gravity))
{
_gravitySystem.RefreshGravity(xform.ParentUid, gravity);
}
diff --git a/Content.Server/Guardian/GuardianSystem.cs b/Content.Server/Guardian/GuardianSystem.cs
index ae4d0ca2b8..7a1b875756 100644
--- a/Content.Server/Guardian/GuardianSystem.cs
+++ b/Content.Server/Guardian/GuardianSystem.cs
@@ -80,6 +80,12 @@ private void OnPerformAction(EntityUid uid, GuardianHostComponent component, Gua
if (args.Handled)
return;
+ if (_container.IsEntityInContainer(uid))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("guardian-inside-container"), uid, uid);
+ return;
+ }
+
if (component.HostedGuardian != null)
ToggleGuardian(uid, component);
diff --git a/Content.Server/Guidebook/GuidebookDataSystem.cs b/Content.Server/Guidebook/GuidebookDataSystem.cs
new file mode 100644
index 0000000000..86a6344156
--- /dev/null
+++ b/Content.Server/Guidebook/GuidebookDataSystem.cs
@@ -0,0 +1,111 @@
+using System.Reflection;
+using Content.Shared.Guidebook;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Guidebook;
+
+///
+/// Server system for identifying component fields/properties to extract values from entity prototypes.
+/// Extracted data is sent to clients when they connect or when prototypes are reloaded.
+///
+public sealed class GuidebookDataSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _protoMan = default!;
+
+ private readonly Dictionary> _tagged = [];
+ private GuidebookData _cachedData = new();
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeNetworkEvent(OnRequestRules);
+ SubscribeLocalEvent(OnPrototypesReloaded);
+
+ // Build initial cache
+ GatherData(ref _cachedData);
+ }
+
+ private void OnRequestRules(RequestGuidebookDataEvent ev, EntitySessionEventArgs args)
+ {
+ // Send cached data to requesting client
+ var sendEv = new UpdateGuidebookDataEvent(_cachedData);
+ RaiseNetworkEvent(sendEv, args.SenderSession);
+ }
+
+ private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
+ {
+ // We only care about entity prototypes
+ if (!args.WasModified())
+ return;
+
+ // The entity prototypes changed! Clear our cache and regather data
+ RebuildDataCache();
+
+ // Send new data to all clients
+ var ev = new UpdateGuidebookDataEvent(_cachedData);
+ RaiseNetworkEvent(ev);
+ }
+
+ private void GatherData(ref GuidebookData cache)
+ {
+ // Just for debug metrics
+ var memberCount = 0;
+ var prototypeCount = 0;
+
+ if (_tagged.Count == 0)
+ {
+ // Scan component registrations to find members tagged for extraction
+ foreach (var registration in EntityManager.ComponentFactory.GetAllRegistrations())
+ {
+ foreach (var member in registration.Type.GetMembers())
+ {
+ if (member.HasCustomAttribute())
+ {
+ // Note this component-member pair for later
+ _tagged.GetOrNew(registration.Name).Add(member);
+ memberCount++;
+ }
+ }
+ }
+ }
+
+ // Scan entity prototypes for the component-member pairs we noted
+ var entityPrototypes = _protoMan.EnumeratePrototypes();
+ foreach (var prototype in entityPrototypes)
+ {
+ foreach (var (component, entry) in prototype.Components)
+ {
+ if (!_tagged.TryGetValue(component, out var members))
+ continue;
+
+ prototypeCount++;
+
+ foreach (var member in members)
+ {
+ // It's dumb that we can't just do member.GetValue, but we can't, so
+ var value = member switch
+ {
+ FieldInfo field => field.GetValue(entry.Component),
+ PropertyInfo property => property.GetValue(entry.Component),
+ _ => throw new NotImplementedException("Unsupported member type")
+ };
+ // Add it into the data cache
+ cache.AddData(prototype.ID, component, member.Name, value);
+ }
+ }
+ }
+
+ Log.Debug($"Collected {cache.Count} Guidebook Protodata value(s) - {prototypeCount} matched prototype(s), {_tagged.Count} component(s), {memberCount} member(s)");
+ }
+
+ ///
+ /// Clears the cached data, then regathers it.
+ ///
+ private void RebuildDataCache()
+ {
+ _cachedData.Clear();
+ GatherData(ref _cachedData);
+ }
+}
diff --git a/Content.Server/IdentityManagement/IdentitySystem.cs b/Content.Server/IdentityManagement/IdentitySystem.cs
index 4766b89172..e110a42483 100644
--- a/Content.Server/IdentityManagement/IdentitySystem.cs
+++ b/Content.Server/IdentityManagement/IdentitySystem.cs
@@ -39,6 +39,7 @@ public override void Initialize()
SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid));
+ SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent(OnMapInit);
}
diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs
index b738d28b46..9da96a76f8 100644
--- a/Content.Server/Mech/Systems/MechSystem.cs
+++ b/Content.Server/Mech/Systems/MechSystem.cs
@@ -211,8 +211,11 @@ private void OnAlternativeVerb(EntityUid uid, MechComponent component, GetVerbsE
return;
}
- var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.ExitDelay,
- new MechExitEvent(), uid, target: uid);
+ var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.ExitDelay, new MechExitEvent(), uid, target: uid)
+ {
+ BreakOnMove = true,
+ };
+ _popup.PopupEntity(Loc.GetString("mech-eject-pilot-alert", ("item", uid), ("user", args.User)), uid, PopupType.Large);
_doAfter.TryStartDoAfter(doAfterEventArgs);
}
diff --git a/Content.Server/Mind/Commands/RenameCommand.cs b/Content.Server/Mind/Commands/RenameCommand.cs
index 834453fb19..f283fe5d19 100644
--- a/Content.Server/Mind/Commands/RenameCommand.cs
+++ b/Content.Server/Mind/Commands/RenameCommand.cs
@@ -1,31 +1,22 @@
using System.Diagnostics.CodeAnalysis;
-using Content.Server.Access.Systems;
using Content.Server.Administration;
-using Content.Server.Administration.Systems;
-using Content.Server.PDA;
-using Content.Server.StationRecords.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Administration;
-using Content.Shared.Mind;
-using Content.Shared.PDA;
-using Content.Shared.StationRecords;
using Robust.Server.Player;
using Robust.Shared.Console;
-using Robust.Shared.Player;
namespace Content.Server.Mind.Commands;
[AdminCommand(AdminFlags.VarEdit)]
-public sealed class RenameCommand : IConsoleCommand
+public sealed class RenameCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly MetaDataSystem _metaSystem = default!;
- public string Command => "rename";
- public string Description => "Renames an entity and its cloner entries, ID cards, and PDAs.";
- public string Help => "rename ";
+ public override string Command => "rename";
- public void Execute(IConsoleShell shell, string argStr, string[] args)
+ public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
@@ -36,69 +27,14 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
var name = args[1];
if (name.Length > IdCardConsoleComponent.MaxFullNameLength)
{
- shell.WriteLine("Name is too long.");
+ shell.WriteLine(Loc.GetString("cmd-rename-too-long"));
return;
}
if (!TryParseUid(args[0], shell, _entManager, out var entityUid))
return;
- // Metadata
- var metadata = _entManager.GetComponent(entityUid.Value);
- var oldName = metadata.EntityName;
- _entManager.System().SetEntityName(entityUid.Value, name, metadata);
-
- var minds = _entManager.System();
-
- if (minds.TryGetMind(entityUid.Value, out var mindId, out var mind))
- {
- // Mind
- mind.CharacterName = name;
- _entManager.Dirty(mindId, mind);
- }
-
- // Id Cards
- if (_entManager.TrySystem(out var idCardSystem))
- {
- if (idCardSystem.TryFindIdCard(entityUid.Value, out var idCard))
- {
- idCardSystem.TryChangeFullName(idCard, name, idCard);
-
- // Records
- // This is done here because ID cards are linked to station records
- if (_entManager.TrySystem(out var recordsSystem)
- && _entManager.TryGetComponent(idCard, out StationRecordKeyStorageComponent? keyStorage)
- && keyStorage.Key is {} key)
- {
- if (recordsSystem.TryGetRecord(key, out var generalRecord))
- {
- generalRecord.Name = name;
- }
-
- recordsSystem.Synchronize(key);
- }
- }
- }
-
- // PDAs
- if (_entManager.TrySystem(out var pdaSystem))
- {
- var query = _entManager.EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var pda))
- {
- if (pda.OwnerName == oldName)
- {
- pdaSystem.SetOwner(uid, pda, name);
- }
- }
- }
-
- // Admin Overlay
- if (_entManager.TrySystem(out var adminSystem)
- && _entManager.TryGetComponent(entityUid, out var actorComp))
- {
- adminSystem.UpdatePlayerList(actorComp.PlayerSession);
- }
+ _metaSystem.SetEntityName(entityUid.Value, name);
}
private bool TryParseUid(string str, IConsoleShell shell,
@@ -114,9 +50,9 @@ private bool TryParseUid(string str, IConsoleShell shell,
}
if (session == null)
- shell.WriteError("Can't find username/uid: " + str);
+ shell.WriteError(Loc.GetString("cmd-rename-not-found", ("target", str)));
else
- shell.WriteError(str + " does not have an entity.");
+ shell.WriteError(Loc.GetString("cmd-rename-no-entity", ("target", str)));
entityUid = EntityUid.Invalid;
return false;
diff --git a/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs
index 367b16a268..ae8215ac6a 100644
--- a/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs
+++ b/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs
@@ -133,7 +133,7 @@ private bool TryAddFoodElement(Entity start, En
var flip = start.Comp.AllowHorizontalFlip && _random.Prob(0.5f);
var layer = new FoodSequenceVisualLayer(elementIndexed,
_random.Pick(elementIndexed.Sprites),
- new Vector2(flip ? -1 : 1, 1),
+ new Vector2(flip ? -elementIndexed.Scale.X : elementIndexed.Scale.X, elementIndexed.Scale.Y),
new Vector2(
_random.NextFloat(start.Comp.MinLayerOffset.X, start.Comp.MaxLayerOffset.X),
_random.NextFloat(start.Comp.MinLayerOffset.Y, start.Comp.MaxLayerOffset.Y))
diff --git a/Content.Server/PDA/PdaSystem.cs b/Content.Server/PDA/PdaSystem.cs
index 691d024ecd..cdcdbc02e5 100644
--- a/Content.Server/PDA/PdaSystem.cs
+++ b/Content.Server/PDA/PdaSystem.cs
@@ -55,9 +55,23 @@ public override void Initialize()
SubscribeLocalEvent(OnNotification);
SubscribeLocalEvent(OnStationRenamed);
+ SubscribeLocalEvent(OnEntityRenamed);
SubscribeLocalEvent(OnAlertLevelChanged);
}
+ private void OnEntityRenamed(ref EntityRenamedEvent ev)
+ {
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ if (comp.PdaOwner == ev.Uid)
+ {
+ SetOwner(uid, comp, ev.Uid, ev.NewName);
+ }
+ }
+ }
+
protected override void OnComponentInit(EntityUid uid, PdaComponent pda, ComponentInit args)
{
base.OnComponentInit(uid, pda, args);
@@ -94,9 +108,10 @@ private void OnLightToggle(EntityUid uid, PdaComponent pda, LightToggleEvent arg
UpdatePdaUi(uid, pda);
}
- public void SetOwner(EntityUid uid, PdaComponent pda, string ownerName)
+ public void SetOwner(EntityUid uid, PdaComponent pda, EntityUid owner, string ownerName)
{
pda.OwnerName = ownerName;
+ pda.PdaOwner = owner;
UpdatePdaUi(uid, pda);
}
@@ -112,7 +127,7 @@ private void OnAlertLevelChanged(AlertLevelChangedEvent args)
private void UpdateAllPdaUisOnStation()
{
- var query = EntityQueryEnumerator();
+ var query = AllEntityQuery();
while (query.MoveNext(out var ent, out var comp))
{
UpdatePdaUi(ent, comp);
diff --git a/Content.Server/Physics/Controllers/MoverController.cs b/Content.Server/Physics/Controllers/MoverController.cs
index 19d58438b3..f927e717a9 100644
--- a/Content.Server/Physics/Controllers/MoverController.cs
+++ b/Content.Server/Physics/Controllers/MoverController.cs
@@ -12,560 +12,559 @@
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
using Robust.Shared.Map.Components;
-namespace Content.Server.Physics.Controllers
+namespace Content.Server.Physics.Controllers;
+
+public sealed class MoverController : SharedMoverController
{
- public sealed class MoverController : SharedMoverController
+ [Dependency] private readonly ThrusterSystem _thruster = default!;
+ [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
+
+ private Dictionary)> _shuttlePilots = new();
+
+ public override void Initialize()
{
- [Dependency] private readonly ThrusterSystem _thruster = default!;
- [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
+ base.Initialize();
+ SubscribeLocalEvent(OnRelayPlayerAttached);
+ SubscribeLocalEvent(OnRelayPlayerDetached);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+ }
- private Dictionary)> _shuttlePilots = new();
+ private void OnRelayPlayerAttached(Entity entity, ref PlayerAttachedEvent args)
+ {
+ if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
+ SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
+ }
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnRelayPlayerAttached);
- SubscribeLocalEvent(OnRelayPlayerDetached);
- SubscribeLocalEvent(OnPlayerAttached);
- SubscribeLocalEvent(OnPlayerDetached);
- }
+ private void OnRelayPlayerDetached(Entity entity, ref PlayerDetachedEvent args)
+ {
+ if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
+ SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
+ }
- private void OnRelayPlayerAttached(Entity entity, ref PlayerAttachedEvent args)
- {
- if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
- SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
- }
+ private void OnPlayerAttached(Entity entity, ref PlayerAttachedEvent args)
+ {
+ SetMoveInput(entity, MoveButtons.None);
+ }
- private void OnRelayPlayerDetached(Entity entity, ref PlayerDetachedEvent args)
- {
- if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
- SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
- }
+ private void OnPlayerDetached(Entity entity, ref PlayerDetachedEvent args)
+ {
+ SetMoveInput(entity, MoveButtons.None);
+ }
- private void OnPlayerAttached(Entity entity, ref PlayerAttachedEvent args)
- {
- SetMoveInput(entity, MoveButtons.None);
- }
+ protected override bool CanSound()
+ {
+ return true;
+ }
- private void OnPlayerDetached(Entity entity, ref PlayerDetachedEvent args)
- {
- SetMoveInput(entity, MoveButtons.None);
- }
+ public override void UpdateBeforeSolve(bool prediction, float frameTime)
+ {
+ base.UpdateBeforeSolve(prediction, frameTime);
- protected override bool CanSound()
- {
- return true;
- }
+ var inputQueryEnumerator = AllEntityQuery();
- public override void UpdateBeforeSolve(bool prediction, float frameTime)
+ while (inputQueryEnumerator.MoveNext(out var uid, out var mover))
{
- base.UpdateBeforeSolve(prediction, frameTime);
+ var physicsUid = uid;
- var inputQueryEnumerator = AllEntityQuery();
+ if (RelayQuery.HasComponent(uid))
+ continue;
- while (inputQueryEnumerator.MoveNext(out var uid, out var mover))
+ if (!XformQuery.TryGetComponent(uid, out var xform))
{
- var physicsUid = uid;
-
- if (RelayQuery.HasComponent(uid))
- continue;
-
- if (!XformQuery.TryGetComponent(uid, out var xform))
- {
- continue;
- }
-
- PhysicsComponent? body;
- var xformMover = xform;
+ continue;
+ }
- if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
- {
- if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
- !XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
- {
- continue;
- }
+ PhysicsComponent? body;
+ var xformMover = xform;
- physicsUid = xform.ParentUid;
- }
- else if (!PhysicsQuery.TryGetComponent(uid, out body))
+ if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
+ {
+ if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
+ !XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
{
continue;
}
- HandleMobMovement(uid,
- mover,
- physicsUid,
- body,
- xformMover,
- frameTime);
+ physicsUid = xform.ParentUid;
}
-
- HandleShuttleMovement(frameTime);
- }
-
- public (Vector2 Strafe, float Rotation, float Brakes) GetPilotVelocityInput(PilotComponent component)
- {
- if (!Timing.InSimulation)
+ else if (!PhysicsQuery.TryGetComponent(uid, out body))
{
- // Outside of simulation we'll be running client predicted movement per-frame.
- // So return a full-length vector as if it's a full tick.
- // Physics system will have the correct time step anyways.
- ResetSubtick(component);
- ApplyTick(component, 1f);
- return (component.CurTickStrafeMovement, component.CurTickRotationMovement, component.CurTickBraking);
+ continue;
}
- float remainingFraction;
-
- if (Timing.CurTick > component.LastInputTick)
- {
- component.CurTickStrafeMovement = Vector2.Zero;
- component.CurTickRotationMovement = 0f;
- component.CurTickBraking = 0f;
- remainingFraction = 1;
- }
- else
- {
- remainingFraction = (ushort.MaxValue - component.LastInputSubTick) / (float) ushort.MaxValue;
- }
+ HandleMobMovement(uid,
+ mover,
+ physicsUid,
+ body,
+ xformMover,
+ frameTime);
+ }
- ApplyTick(component, remainingFraction);
+ HandleShuttleMovement(frameTime);
+ }
- // Logger.Info($"{curDir}{walk}{sprint}");
+ public (Vector2 Strafe, float Rotation, float Brakes) GetPilotVelocityInput(PilotComponent component)
+ {
+ if (!Timing.InSimulation)
+ {
+ // Outside of simulation we'll be running client predicted movement per-frame.
+ // So return a full-length vector as if it's a full tick.
+ // Physics system will have the correct time step anyways.
+ ResetSubtick(component);
+ ApplyTick(component, 1f);
return (component.CurTickStrafeMovement, component.CurTickRotationMovement, component.CurTickBraking);
}
- private void ResetSubtick(PilotComponent component)
- {
- if (Timing.CurTick <= component.LastInputTick) return;
+ float remainingFraction;
+ if (Timing.CurTick > component.LastInputTick)
+ {
component.CurTickStrafeMovement = Vector2.Zero;
component.CurTickRotationMovement = 0f;
component.CurTickBraking = 0f;
- component.LastInputTick = Timing.CurTick;
- component.LastInputSubTick = 0;
+ remainingFraction = 1;
+ }
+ else
+ {
+ remainingFraction = (ushort.MaxValue - component.LastInputSubTick) / (float) ushort.MaxValue;
}
- protected override void HandleShuttleInput(EntityUid uid, ShuttleButtons button, ushort subTick, bool state)
+ ApplyTick(component, remainingFraction);
+
+ // Logger.Info($"{curDir}{walk}{sprint}");
+ return (component.CurTickStrafeMovement, component.CurTickRotationMovement, component.CurTickBraking);
+ }
+
+ private void ResetSubtick(PilotComponent component)
+ {
+ if (Timing.CurTick <= component.LastInputTick) return;
+
+ component.CurTickStrafeMovement = Vector2.Zero;
+ component.CurTickRotationMovement = 0f;
+ component.CurTickBraking = 0f;
+ component.LastInputTick = Timing.CurTick;
+ component.LastInputSubTick = 0;
+ }
+
+ protected override void HandleShuttleInput(EntityUid uid, ShuttleButtons button, ushort subTick, bool state)
+ {
+ if (!TryComp(uid, out var pilot) || pilot.Console == null)
+ return;
+
+ ResetSubtick(pilot);
+
+ if (subTick >= pilot.LastInputSubTick)
{
- if (!TryComp(uid, out var pilot) || pilot.Console == null)
- return;
+ var fraction = (subTick - pilot.LastInputSubTick) / (float) ushort.MaxValue;
- ResetSubtick(pilot);
+ ApplyTick(pilot, fraction);
+ pilot.LastInputSubTick = subTick;
+ }
- if (subTick >= pilot.LastInputSubTick)
- {
- var fraction = (subTick - pilot.LastInputSubTick) / (float) ushort.MaxValue;
+ var buttons = pilot.HeldButtons;
- ApplyTick(pilot, fraction);
- pilot.LastInputSubTick = subTick;
- }
+ if (state)
+ {
+ buttons |= button;
+ }
+ else
+ {
+ buttons &= ~button;
+ }
- var buttons = pilot.HeldButtons;
+ pilot.HeldButtons = buttons;
+ }
- if (state)
- {
- buttons |= button;
- }
- else
- {
- buttons &= ~button;
- }
+ private static void ApplyTick(PilotComponent component, float fraction)
+ {
+ var x = 0;
+ var y = 0;
+ var rot = 0;
+ int brake;
- pilot.HeldButtons = buttons;
+ if ((component.HeldButtons & ShuttleButtons.StrafeLeft) != 0x0)
+ {
+ x -= 1;
}
- private static void ApplyTick(PilotComponent component, float fraction)
+ if ((component.HeldButtons & ShuttleButtons.StrafeRight) != 0x0)
{
- var x = 0;
- var y = 0;
- var rot = 0;
- int brake;
+ x += 1;
+ }
- if ((component.HeldButtons & ShuttleButtons.StrafeLeft) != 0x0)
- {
- x -= 1;
- }
+ component.CurTickStrafeMovement.X += x * fraction;
- if ((component.HeldButtons & ShuttleButtons.StrafeRight) != 0x0)
- {
- x += 1;
- }
+ if ((component.HeldButtons & ShuttleButtons.StrafeUp) != 0x0)
+ {
+ y += 1;
+ }
- component.CurTickStrafeMovement.X += x * fraction;
+ if ((component.HeldButtons & ShuttleButtons.StrafeDown) != 0x0)
+ {
+ y -= 1;
+ }
- if ((component.HeldButtons & ShuttleButtons.StrafeUp) != 0x0)
- {
- y += 1;
- }
+ component.CurTickStrafeMovement.Y += y * fraction;
- if ((component.HeldButtons & ShuttleButtons.StrafeDown) != 0x0)
- {
- y -= 1;
- }
+ if ((component.HeldButtons & ShuttleButtons.RotateLeft) != 0x0)
+ {
+ rot -= 1;
+ }
- component.CurTickStrafeMovement.Y += y * fraction;
+ if ((component.HeldButtons & ShuttleButtons.RotateRight) != 0x0)
+ {
+ rot += 1;
+ }
- if ((component.HeldButtons & ShuttleButtons.RotateLeft) != 0x0)
- {
- rot -= 1;
- }
+ component.CurTickRotationMovement += rot * fraction;
- if ((component.HeldButtons & ShuttleButtons.RotateRight) != 0x0)
- {
- rot += 1;
- }
+ if ((component.HeldButtons & ShuttleButtons.Brake) != 0x0)
+ {
+ brake = 1;
+ }
+ else
+ {
+ brake = 0;
+ }
- component.CurTickRotationMovement += rot * fraction;
+ component.CurTickBraking += brake * fraction;
+ }
+
+ ///
+ /// Helper function to extrapolate max velocity for a given Vector2 (really, its angle) and shuttle.
+ ///
+ private Vector2 ObtainMaxVel(Vector2 vel, ShuttleComponent shuttle)
+ {
+ if (vel.Length() == 0f)
+ return Vector2.Zero;
+
+ // this math could PROBABLY be simplified for performance
+ // probably
+ // __________________________________
+ // / / __ __ \2 / __ __ \2
+ // O = I : _ / |I * | 1/H | | + |I * | 0 | |
+ // V \ |_ 0 _| / \ |_1/V_| /
+
+ var horizIndex = vel.X > 0 ? 1 : 3; // east else west
+ var vertIndex = vel.Y > 0 ? 2 : 0; // north else south
+ var horizComp = vel.X != 0 ? MathF.Pow(Vector2.Dot(vel, new (shuttle.LinearThrust[horizIndex] / shuttle.LinearThrust[horizIndex], 0f)), 2) : 0;
+ var vertComp = vel.Y != 0 ? MathF.Pow(Vector2.Dot(vel, new (0f, shuttle.LinearThrust[vertIndex] / shuttle.LinearThrust[vertIndex])), 2) : 0;
+
+ return shuttle.BaseMaxLinearVelocity * vel * MathF.ReciprocalSqrtEstimate(horizComp + vertComp);
+ }
+
+ private void HandleShuttleMovement(float frameTime)
+ {
+ var newPilots = new Dictionary)>();
+
+ // We just mark off their movement and the shuttle itself does its own movement
+ var activePilotQuery = EntityQueryEnumerator();
+ var shuttleQuery = GetEntityQuery();
+ while (activePilotQuery.MoveNext(out var uid, out var pilot, out var mover))
+ {
+ var consoleEnt = pilot.Console;
- if ((component.HeldButtons & ShuttleButtons.Brake) != 0x0)
+ // TODO: This is terrible. Just make a new mover and also make it remote piloting + device networks
+ if (TryComp(consoleEnt, out var cargoConsole))
{
- brake = 1;
+ consoleEnt = cargoConsole.Entity;
}
- else
+
+ if (!TryComp(consoleEnt, out TransformComponent? xform)) continue;
+
+ var gridId = xform.GridUid;
+ // This tries to see if the grid is a shuttle and if the console should work.
+ if (!TryComp(gridId, out var _) ||
+ !shuttleQuery.TryGetComponent(gridId, out var shuttleComponent) ||
+ !shuttleComponent.Enabled)
+ continue;
+
+ if (!newPilots.TryGetValue(gridId!.Value, out var pilots))
{
- brake = 0;
+ pilots = (shuttleComponent, new List<(EntityUid, PilotComponent, InputMoverComponent, TransformComponent)>());
+ newPilots[gridId.Value] = pilots;
}
- component.CurTickBraking += brake * fraction;
+ pilots.Item2.Add((uid, pilot, mover, xform));
}
- ///
- /// Helper function to extrapolate max velocity for a given Vector2 (really, its angle) and shuttle.
- ///
- private Vector2 ObtainMaxVel(Vector2 vel, ShuttleComponent shuttle)
+ // Reset inputs for non-piloted shuttles.
+ foreach (var (shuttleUid, (shuttle, _)) in _shuttlePilots)
{
- if (vel.Length() == 0f)
- return Vector2.Zero;
-
- // this math could PROBABLY be simplified for performance
- // probably
- // __________________________________
- // / / __ __ \2 / __ __ \2
- // O = I : _ / |I * | 1/H | | + |I * | 0 | |
- // V \ |_ 0 _| / \ |_1/V_| /
-
- var horizIndex = vel.X > 0 ? 1 : 3; // east else west
- var vertIndex = vel.Y > 0 ? 2 : 0; // north else south
- var horizComp = vel.X != 0 ? MathF.Pow(Vector2.Dot(vel, new (shuttle.LinearThrust[horizIndex] / shuttle.LinearThrust[horizIndex], 0f)), 2) : 0;
- var vertComp = vel.Y != 0 ? MathF.Pow(Vector2.Dot(vel, new (0f, shuttle.LinearThrust[vertIndex] / shuttle.LinearThrust[vertIndex])), 2) : 0;
-
- return shuttle.BaseMaxLinearVelocity * vel * MathF.ReciprocalSqrtEstimate(horizComp + vertComp);
+ if (newPilots.ContainsKey(shuttleUid) || CanPilot(shuttleUid))
+ continue;
+
+ _thruster.DisableLinearThrusters(shuttle);
}
- private void HandleShuttleMovement(float frameTime)
+ _shuttlePilots = newPilots;
+
+ // Collate all of the linear / angular velocites for a shuttle
+ // then do the movement input once for it.
+ var xformQuery = GetEntityQuery();
+ foreach (var (shuttleUid, (shuttle, pilots)) in _shuttlePilots)
{
- var newPilots = new Dictionary)>();
+ if (Paused(shuttleUid) || CanPilot(shuttleUid) || !TryComp(shuttleUid, out var body))
+ continue;
- // We just mark off their movement and the shuttle itself does its own movement
- var activePilotQuery = EntityQueryEnumerator();
- var shuttleQuery = GetEntityQuery();
- while (activePilotQuery.MoveNext(out var uid, out var pilot, out var mover))
+ var shuttleNorthAngle = _xformSystem.GetWorldRotation(shuttleUid, xformQuery);
+
+ // Collate movement linear and angular inputs together
+ var linearInput = Vector2.Zero;
+ var brakeInput = 0f;
+ var angularInput = 0f;
+
+ foreach (var (pilotUid, pilot, _, consoleXform) in pilots)
{
- var consoleEnt = pilot.Console;
+ var (strafe, rotation, brakes) = GetPilotVelocityInput(pilot);
- // TODO: This is terrible. Just make a new mover and also make it remote piloting + device networks
- if (TryComp(consoleEnt, out var cargoConsole))
+ if (brakes > 0f)
{
- consoleEnt = cargoConsole.Entity;
+ brakeInput += brakes;
}
- if (!TryComp(consoleEnt, out TransformComponent? xform)) continue;
-
- var gridId = xform.GridUid;
- // This tries to see if the grid is a shuttle and if the console should work.
- if (!TryComp(gridId, out var _) ||
- !shuttleQuery.TryGetComponent(gridId, out var shuttleComponent) ||
- !shuttleComponent.Enabled)
- continue;
-
- if (!newPilots.TryGetValue(gridId!.Value, out var pilots))
+ if (strafe.Length() > 0f)
{
- pilots = (shuttleComponent, new List<(EntityUid, PilotComponent, InputMoverComponent, TransformComponent)>());
- newPilots[gridId.Value] = pilots;
+ var offsetRotation = consoleXform.LocalRotation;
+ linearInput += offsetRotation.RotateVec(strafe);
}
- pilots.Item2.Add((uid, pilot, mover, xform));
- }
-
- // Reset inputs for non-piloted shuttles.
- foreach (var (shuttleUid, (shuttle, _)) in _shuttlePilots)
- {
- if (newPilots.ContainsKey(shuttleUid) || CanPilot(shuttleUid))
- continue;
-
- _thruster.DisableLinearThrusters(shuttle);
+ if (rotation != 0f)
+ {
+ angularInput += rotation;
+ }
}
- _shuttlePilots = newPilots;
+ var count = pilots.Count;
+ linearInput /= count;
+ angularInput /= count;
+ brakeInput /= count;
- // Collate all of the linear / angular velocites for a shuttle
- // then do the movement input once for it.
- var xformQuery = GetEntityQuery();
- foreach (var (shuttleUid, (shuttle, pilots)) in _shuttlePilots)
+ // Handle shuttle movement
+ if (brakeInput > 0f)
{
- if (Paused(shuttleUid) || CanPilot(shuttleUid) || !TryComp(shuttleUid, out var body))
- continue;
-
- var shuttleNorthAngle = _xformSystem.GetWorldRotation(shuttleUid, xformQuery);
-
- // Collate movement linear and angular inputs together
- var linearInput = Vector2.Zero;
- var brakeInput = 0f;
- var angularInput = 0f;
-
- foreach (var (pilotUid, pilot, _, consoleXform) in pilots)
+ if (body.LinearVelocity.Length() > 0f)
{
- var (strafe, rotation, brakes) = GetPilotVelocityInput(pilot);
+ // Minimum brake velocity for a direction to show its thrust appearance.
+ const float appearanceThreshold = 0.1f;
- if (brakes > 0f)
- {
- brakeInput += brakes;
- }
-
- if (strafe.Length() > 0f)
- {
- var offsetRotation = consoleXform.LocalRotation;
- linearInput += offsetRotation.RotateVec(strafe);
- }
+ // Get velocity relative to the shuttle so we know which thrusters to fire
+ var shuttleVelocity = (-shuttleNorthAngle).RotateVec(body.LinearVelocity);
+ var force = Vector2.Zero;
- if (rotation != 0f)
+ if (shuttleVelocity.X < 0f)
{
- angularInput += rotation;
- }
- }
+ _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.West);
- var count = pilots.Count;
- linearInput /= count;
- angularInput /= count;
- brakeInput /= count;
+ if (shuttleVelocity.X < -appearanceThreshold)
+ _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.East);
- // Handle shuttle movement
- if (brakeInput > 0f)
- {
- if (body.LinearVelocity.Length() > 0f)
+ var index = (int) Math.Log2((int) DirectionFlag.East);
+ force.X += shuttle.LinearThrust[index];
+ }
+ else if (shuttleVelocity.X > 0f)
{
- // Minimum brake velocity for a direction to show its thrust appearance.
- const float appearanceThreshold = 0.1f;
+ _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.East);
- // Get velocity relative to the shuttle so we know which thrusters to fire
- var shuttleVelocity = (-shuttleNorthAngle).RotateVec(body.LinearVelocity);
- var force = Vector2.Zero;
+ if (shuttleVelocity.X > appearanceThreshold)
+ _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.West);
- if (shuttleVelocity.X < 0f)
- {
- _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.West);
-
- if (shuttleVelocity.X < -appearanceThreshold)
- _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.East);
-
- var index = (int) Math.Log2((int) DirectionFlag.East);
- force.X += shuttle.LinearThrust[index];
- }
- else if (shuttleVelocity.X > 0f)
- {
- _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.East);
+ var index = (int) Math.Log2((int) DirectionFlag.West);
+ force.X -= shuttle.LinearThrust[index];
+ }
- if (shuttleVelocity.X > appearanceThreshold)
- _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.West);
+ if (shuttleVelocity.Y < 0f)
+ {
+ _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.South);
- var index = (int) Math.Log2((int) DirectionFlag.West);
- force.X -= shuttle.LinearThrust[index];
- }
+ if (shuttleVelocity.Y < -appearanceThreshold)
+ _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.North);
- if (shuttleVelocity.Y < 0f)
- {
- _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.South);
+ var index = (int) Math.Log2((int) DirectionFlag.North);
+ force.Y += shuttle.LinearThrust[index];
+ }
+ else if (shuttleVelocity.Y > 0f)
+ {
+ _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.North);
- if (shuttleVelocity.Y < -appearanceThreshold)
- _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.North);
+ if (shuttleVelocity.Y > appearanceThreshold)
+ _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.South);
- var index = (int) Math.Log2((int) DirectionFlag.North);
- force.Y += shuttle.LinearThrust[index];
- }
- else if (shuttleVelocity.Y > 0f)
- {
- _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.North);
+ var index = (int) Math.Log2((int) DirectionFlag.South);
+ force.Y -= shuttle.LinearThrust[index];
+ }
- if (shuttleVelocity.Y > appearanceThreshold)
- _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.South);
+ var impulse = force * brakeInput * ShuttleComponent.BrakeCoefficient;
+ impulse = shuttleNorthAngle.RotateVec(impulse);
+ var forceMul = frameTime * body.InvMass;
+ var maxVelocity = (-body.LinearVelocity).Length() / forceMul;
- var index = (int) Math.Log2((int) DirectionFlag.South);
- force.Y -= shuttle.LinearThrust[index];
- }
+ // Don't overshoot
+ if (impulse.Length() > maxVelocity)
+ impulse = impulse.Normalized() * maxVelocity;
- var impulse = force * brakeInput * ShuttleComponent.BrakeCoefficient;
- impulse = shuttleNorthAngle.RotateVec(impulse);
- var forceMul = frameTime * body.InvMass;
- var maxVelocity = (-body.LinearVelocity).Length() / forceMul;
+ PhysicsSystem.ApplyForce(shuttleUid, impulse, body: body);
+ }
+ else
+ {
+ _thruster.DisableLinearThrusters(shuttle);
+ }
- // Don't overshoot
- if (impulse.Length() > maxVelocity)
- impulse = impulse.Normalized() * maxVelocity;
+ if (body.AngularVelocity != 0f)
+ {
+ var torque = shuttle.AngularThrust * brakeInput * (body.AngularVelocity > 0f ? -1f : 1f) * ShuttleComponent.BrakeCoefficient;
+ var torqueMul = body.InvI * frameTime;
- PhysicsSystem.ApplyForce(shuttleUid, impulse, body: body);
+ if (body.AngularVelocity > 0f)
+ {
+ torque = MathF.Max(-body.AngularVelocity / torqueMul, torque);
}
else
{
- _thruster.DisableLinearThrusters(shuttle);
+ torque = MathF.Min(-body.AngularVelocity / torqueMul, torque);
}
- if (body.AngularVelocity != 0f)
- {
- var torque = shuttle.AngularThrust * brakeInput * (body.AngularVelocity > 0f ? -1f : 1f) * ShuttleComponent.BrakeCoefficient;
- var torqueMul = body.InvI * frameTime;
-
- if (body.AngularVelocity > 0f)
- {
- torque = MathF.Max(-body.AngularVelocity / torqueMul, torque);
- }
- else
- {
- torque = MathF.Min(-body.AngularVelocity / torqueMul, torque);
- }
-
- if (!torque.Equals(0f))
- {
- PhysicsSystem.ApplyTorque(shuttleUid, torque, body: body);
- _thruster.SetAngularThrust(shuttle, true);
- }
- }
- else
+ if (!torque.Equals(0f))
{
- _thruster.SetAngularThrust(shuttle, false);
+ PhysicsSystem.ApplyTorque(shuttleUid, torque, body: body);
+ _thruster.SetAngularThrust(shuttle, true);
}
}
-
- if (linearInput.Length().Equals(0f))
+ else
{
- PhysicsSystem.SetSleepingAllowed(shuttleUid, body, true);
-
- if (brakeInput.Equals(0f))
- _thruster.DisableLinearThrusters(shuttle);
+ _thruster.SetAngularThrust(shuttle, false);
}
- else
+ }
+
+ if (linearInput.Length().Equals(0f))
+ {
+ PhysicsSystem.SetSleepingAllowed(shuttleUid, body, true);
+
+ if (brakeInput.Equals(0f))
+ _thruster.DisableLinearThrusters(shuttle);
+ }
+ else
+ {
+ PhysicsSystem.SetSleepingAllowed(shuttleUid, body, false);
+ var angle = linearInput.ToWorldAngle();
+ var linearDir = angle.GetDir();
+ var dockFlag = linearDir.AsFlag();
+ var totalForce = Vector2.Zero;
+
+ // Won't just do cardinal directions.
+ foreach (DirectionFlag dir in Enum.GetValues(typeof(DirectionFlag)))
{
- PhysicsSystem.SetSleepingAllowed(shuttleUid, body, false);
- var angle = linearInput.ToWorldAngle();
- var linearDir = angle.GetDir();
- var dockFlag = linearDir.AsFlag();
- var totalForce = Vector2.Zero;
-
- // Won't just do cardinal directions.
- foreach (DirectionFlag dir in Enum.GetValues(typeof(DirectionFlag)))
+ // Brain no worky but I just want cardinals
+ switch (dir)
{
- // Brain no worky but I just want cardinals
- switch (dir)
- {
- case DirectionFlag.South:
- case DirectionFlag.East:
- case DirectionFlag.North:
- case DirectionFlag.West:
- break;
- default:
- continue;
- }
-
- if ((dir & dockFlag) == 0x0)
- {
- _thruster.DisableLinearThrustDirection(shuttle, dir);
+ case DirectionFlag.South:
+ case DirectionFlag.East:
+ case DirectionFlag.North:
+ case DirectionFlag.West:
+ break;
+ default:
continue;
- }
-
- var force = Vector2.Zero;
- var index = (int) Math.Log2((int) dir);
- var thrust = shuttle.LinearThrust[index];
-
- switch (dir)
- {
- case DirectionFlag.North:
- force.Y += thrust;
- break;
- case DirectionFlag.South:
- force.Y -= thrust;
- break;
- case DirectionFlag.East:
- force.X += thrust;
- break;
- case DirectionFlag.West:
- force.X -= thrust;
- break;
- default:
- throw new ArgumentOutOfRangeException($"Attempted to apply thrust to shuttle {shuttleUid} along invalid dir {dir}.");
- }
-
- _thruster.EnableLinearThrustDirection(shuttle, dir);
- var impulse = force * linearInput.Length();
- totalForce += impulse;
}
- var forceMul = frameTime * body.InvMass;
+ if ((dir & dockFlag) == 0x0)
+ {
+ _thruster.DisableLinearThrustDirection(shuttle, dir);
+ continue;
+ }
- var localVel = (-shuttleNorthAngle).RotateVec(body.LinearVelocity);
- var maxVelocity = ObtainMaxVel(localVel, shuttle); // max for current travel dir
- var maxWishVelocity = ObtainMaxVel(totalForce, shuttle);
- var properAccel = (maxWishVelocity - localVel) / forceMul;
+ var force = Vector2.Zero;
+ var index = (int) Math.Log2((int) dir);
+ var thrust = shuttle.LinearThrust[index];
- var finalForce = Vector2Dot(totalForce, properAccel.Normalized()) * properAccel.Normalized();
+ switch (dir)
+ {
+ case DirectionFlag.North:
+ force.Y += thrust;
+ break;
+ case DirectionFlag.South:
+ force.Y -= thrust;
+ break;
+ case DirectionFlag.East:
+ force.X += thrust;
+ break;
+ case DirectionFlag.West:
+ force.X -= thrust;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException($"Attempted to apply thrust to shuttle {shuttleUid} along invalid dir {dir}.");
+ }
- if (localVel.Length() >= maxVelocity.Length() && Vector2.Dot(totalForce, localVel) > 0f)
- finalForce = Vector2.Zero; // burn would be faster if used as such
+ _thruster.EnableLinearThrustDirection(shuttle, dir);
+ var impulse = force * linearInput.Length();
+ totalForce += impulse;
+ }
- if (finalForce.Length() > properAccel.Length())
- finalForce = properAccel; // don't overshoot
+ var forceMul = frameTime * body.InvMass;
- //Log.Info($"shuttle: maxVelocity {maxVelocity} totalForce {totalForce} finalForce {finalForce} forceMul {forceMul} properAccel {properAccel}");
+ var localVel = (-shuttleNorthAngle).RotateVec(body.LinearVelocity);
+ var maxVelocity = ObtainMaxVel(localVel, shuttle); // max for current travel dir
+ var maxWishVelocity = ObtainMaxVel(totalForce, shuttle);
+ var properAccel = (maxWishVelocity - localVel) / forceMul;
- finalForce = shuttleNorthAngle.RotateVec(finalForce);
+ var finalForce = Vector2Dot(totalForce, properAccel.Normalized()) * properAccel.Normalized();
- if (finalForce.Length() > 0f)
- PhysicsSystem.ApplyForce(shuttleUid, finalForce, body: body);
- }
+ if (localVel.Length() >= maxVelocity.Length() && Vector2.Dot(totalForce, localVel) > 0f)
+ finalForce = Vector2.Zero; // burn would be faster if used as such
- if (MathHelper.CloseTo(angularInput, 0f))
- {
- PhysicsSystem.SetSleepingAllowed(shuttleUid, body, true);
+ if (finalForce.Length() > properAccel.Length())
+ finalForce = properAccel; // don't overshoot
- if (brakeInput <= 0f)
- _thruster.SetAngularThrust(shuttle, false);
- }
- else
- {
- PhysicsSystem.SetSleepingAllowed(shuttleUid, body, false);
- var torque = shuttle.AngularThrust * -angularInput;
+ //Log.Info($"shuttle: maxVelocity {maxVelocity} totalForce {totalForce} finalForce {finalForce} forceMul {forceMul} properAccel {properAccel}");
- // Need to cap the velocity if 1 tick of input brings us over cap so we don't continuously
- // edge onto the cap over and over.
- var torqueMul = body.InvI * frameTime;
+ finalForce = shuttleNorthAngle.RotateVec(finalForce);
- torque = Math.Clamp(torque,
- (-ShuttleComponent.MaxAngularVelocity - body.AngularVelocity) / torqueMul,
- (ShuttleComponent.MaxAngularVelocity - body.AngularVelocity) / torqueMul);
+ if (finalForce.Length() > 0f)
+ PhysicsSystem.ApplyForce(shuttleUid, finalForce, body: body);
+ }
- if (!torque.Equals(0f))
- {
- PhysicsSystem.ApplyTorque(shuttleUid, torque, body: body);
- _thruster.SetAngularThrust(shuttle, true);
- }
- }
+ if (MathHelper.CloseTo(angularInput, 0f))
+ {
+ PhysicsSystem.SetSleepingAllowed(shuttleUid, body, true);
+
+ if (brakeInput <= 0f)
+ _thruster.SetAngularThrust(shuttle, false);
}
- }
+ else
+ {
+ PhysicsSystem.SetSleepingAllowed(shuttleUid, body, false);
+ var torque = shuttle.AngularThrust * -angularInput;
- // .NET 8 seem to miscompile usage of Vector2.Dot above. This manual outline fixes it pending an upstream fix.
- // See PR #24008
- [MethodImpl(MethodImplOptions.NoInlining)]
- public static float Vector2Dot(Vector2 value1, Vector2 value2)
- {
- return Vector2.Dot(value1, value2);
- }
+ // Need to cap the velocity if 1 tick of input brings us over cap so we don't continuously
+ // edge onto the cap over and over.
+ var torqueMul = body.InvI * frameTime;
- private bool CanPilot(EntityUid shuttleUid)
- {
- return TryComp(shuttleUid, out var ftl)
- && (ftl.State & (FTLState.Starting | FTLState.Travelling | FTLState.Arriving)) != 0x0
- || HasComp(shuttleUid);
+ torque = Math.Clamp(torque,
+ (-ShuttleComponent.MaxAngularVelocity - body.AngularVelocity) / torqueMul,
+ (ShuttleComponent.MaxAngularVelocity - body.AngularVelocity) / torqueMul);
+
+ if (!torque.Equals(0f))
+ {
+ PhysicsSystem.ApplyTorque(shuttleUid, torque, body: body);
+ _thruster.SetAngularThrust(shuttle, true);
+ }
+ }
}
+ }
+
+ // .NET 8 seem to miscompile usage of Vector2.Dot above. This manual outline fixes it pending an upstream fix.
+ // See PR #24008
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ public static float Vector2Dot(Vector2 value1, Vector2 value2)
+ {
+ return Vector2.Dot(value1, value2);
+ }
+ private bool CanPilot(EntityUid shuttleUid)
+ {
+ return TryComp(shuttleUid, out var ftl)
+ && (ftl.State & (FTLState.Starting | FTLState.Travelling | FTLState.Arriving)) != 0x0
+ || HasComp(shuttleUid);
}
+
}
diff --git a/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs b/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs
index 9fd824a3c4..a33bddcaa3 100644
--- a/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs
+++ b/Content.Server/Power/EntitySystems/ActivatableUIRequiresPowerSystem.cs
@@ -1,9 +1,7 @@
-using Content.Server.Power.Components;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.UserInterface;
-using Content.Shared.Wires;
using ActivatableUISystem = Content.Shared.UserInterface.ActivatableUISystem;
namespace Content.Server.Power.EntitySystems;
@@ -26,9 +24,6 @@ protected override void OnActivate(Entity e
return;
}
- if (TryComp(ent.Owner, out var panel) && panel.Open)
- return;
-
args.Cancel();
}
diff --git a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
index 82a38f5f00..46d2cd69b9 100644
--- a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
+++ b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
@@ -92,7 +92,7 @@ public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent(HandlePlayerSpawning, before: new []{ typeof(ContainerSpawnPointSystem), typeof(SpawnPointSystem)});
+ SubscribeLocalEvent(HandlePlayerSpawning, before: new []{ typeof(SpawnPointSystem)}, after: new [] { typeof(ContainerSpawnPointSystem)});
SubscribeLocalEvent(OnStationPostInit);
@@ -335,8 +335,7 @@ public void HandlePlayerSpawning(PlayerSpawningEvent ev)
if (ev.SpawnResult != null)
return;
- if (ev.HumanoidCharacterProfile?.SpawnPriority != SpawnPriorityPreference.Arrivals)
- return;
+ // We use arrivals as the default spawn so don't check for job prio.
// Only works on latejoin even if enabled.
if (!Enabled || _ticker.RunLevel != GameRunLevel.InRound)
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
index f30cab253a..e544c1538d 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
@@ -70,11 +70,11 @@ public sealed partial class ShuttleSystem
private readonly HashSet _lookupEnts = new();
private readonly HashSet _immuneEnts = new();
+ private readonly HashSet> _noFtls = new();
private EntityQuery _bodyQuery;
private EntityQuery _buckleQuery;
- private EntityQuery _beaconQuery;
- private EntityQuery _ghostQuery;
+ private EntityQuery _immuneQuery;
private EntityQuery _physicsQuery;
private EntityQuery _statusQuery;
private EntityQuery _xformQuery;
@@ -86,8 +86,7 @@ private void InitializeFTL()
_bodyQuery = GetEntityQuery();
_buckleQuery = GetEntityQuery();
- _beaconQuery = GetEntityQuery();
- _ghostQuery = GetEntityQuery();
+ _immuneQuery = GetEntityQuery();
_physicsQuery = GetEntityQuery();
_statusQuery = GetEntityQuery();
_xformQuery = GetEntityQuery();
@@ -102,7 +101,7 @@ private void InitializeFTL()
private void OnFtlShutdown(Entity ent, ref ComponentShutdown args)
{
- Del(ent.Comp.VisualizerEntity);
+ QueueDel(ent.Comp.VisualizerEntity);
ent.Comp.VisualizerEntity = null;
}
@@ -404,7 +403,12 @@ private void UpdateFTLStarting(Entity entity)
// Offset the start by buffer range just to avoid overlap.
var ftlStart = new EntityCoordinates(ftlMap, new Vector2(_index + width / 2f, 0f) - shuttleCenter);
+ // Store the matrix for the grid prior to movement. This means any entities we need to leave behind we can make sure their positions are updated.
+ // Setting the entity to map directly may run grid traversal (at least at time of writing this).
+ var oldMapUid = xform.MapUid;
+ var oldGridMatrix = _transform.GetWorldMatrix(xform);
_transform.SetCoordinates(entity.Owner, ftlStart);
+ LeaveNoFTLBehind((entity.Owner, xform), oldGridMatrix, oldMapUid);
// Reset rotation so they always face the same direction.
xform.LocalRotation = Angle.Zero;
@@ -476,6 +480,9 @@ private void UpdateFTLArriving(Entity entity)
MapId mapId;
+ QueueDel(entity.Comp1.VisualizerEntity);
+ entity.Comp1.VisualizerEntity = null;
+
if (!Exists(entity.Comp1.TargetCoordinates.EntityId))
{
// Uhh good luck
@@ -628,6 +635,31 @@ private void DoTheDinosaur(TransformComponent xform)
}
}
+ private void LeaveNoFTLBehind(Entity grid, Matrix3x2 oldGridMatrix, EntityUid? oldMapUid)
+ {
+ if (oldMapUid == null)
+ return;
+
+ _noFtls.Clear();
+ var oldGridRotation = oldGridMatrix.Rotation();
+ _lookup.GetGridEntities(grid.Owner, _noFtls);
+
+ foreach (var childUid in _noFtls)
+ {
+ if (!_xformQuery.TryComp(childUid, out var childXform))
+ continue;
+
+ // If we're not parented directly to the grid the matrix may be wrong.
+ var relative = _physics.GetRelativePhysicsTransform(childUid.Owner, (grid.Owner, grid.Comp));
+
+ _transform.SetCoordinates(
+ childUid,
+ childXform,
+ new EntityCoordinates(oldMapUid.Value,
+ Vector2.Transform(relative.Position, oldGridMatrix)), rotation: relative.Quaternion2D.Angle + oldGridRotation);
+ }
+ }
+
private void KnockOverKids(TransformComponent xform, ref ValueList toKnock)
{
// Not recursive because probably not necessary? If we need it to be that's why this method is separate.
@@ -924,8 +956,11 @@ private void Smimsh(EntityUid uid, FixturesComponent? manager = null, MapGridCom
if (!Resolve(uid, ref manager, ref grid, ref xform) || xform.MapUid == null)
return;
+ if (!TryComp(xform.MapUid, out BroadphaseComponent? lookup))
+ return;
+
// Flatten anything not parented to a grid.
- var transform = _physics.GetPhysicsTransform(uid, xform);
+ var transform = _physics.GetRelativePhysicsTransform((uid, xform), xform.MapUid.Value);
var aabbs = new List(manager.Fixtures.Count);
var tileSet = new List<(Vector2i, Tile)>();
@@ -946,7 +981,8 @@ private void Smimsh(EntityUid uid, FixturesComponent? manager = null, MapGridCom
_biomes.ReserveTiles(xform.MapUid.Value, aabb, tileSet);
_lookupEnts.Clear();
_immuneEnts.Clear();
- _lookup.GetEntitiesIntersecting(xform.MapUid.Value, aabb, _lookupEnts, LookupFlags.Uncontained);
+ // TODO: Ideally we'd query first BEFORE moving grid but needs adjustments above.
+ _lookup.GetLocalEntitiesIntersecting(xform.MapUid.Value, fixture.Shape, transform, _lookupEnts, flags: LookupFlags.Uncontained, lookup: lookup);
foreach (var ent in _lookupEnts)
{
@@ -955,7 +991,13 @@ private void Smimsh(EntityUid uid, FixturesComponent? manager = null, MapGridCom
continue;
}
- if (_ghostQuery.HasComponent(ent) || _beaconQuery.HasComponent(ent))
+ // If it's on our grid ignore it.
+ if (!_xformQuery.TryComp(ent, out var childXform) || childXform.GridUid == uid)
+ {
+ continue;
+ }
+
+ if (_immuneQuery.HasComponent(ent))
{
continue;
}
@@ -969,9 +1011,6 @@ private void Smimsh(EntityUid uid, FixturesComponent? manager = null, MapGridCom
continue;
}
- if (HasComp(ent))
- continue;
-
QueueDel(ent);
}
}
diff --git a/Content.Server/Speech/EntitySystems/SkeletonAccentSystem.cs b/Content.Server/Speech/EntitySystems/SkeletonAccentSystem.cs
index d143c25fdb..1b773f1a5a 100644
--- a/Content.Server/Speech/EntitySystems/SkeletonAccentSystem.cs
+++ b/Content.Server/Speech/EntitySystems/SkeletonAccentSystem.cs
@@ -7,29 +7,11 @@ namespace Content.Server.Speech.EntitySystems;
public sealed partial class SkeletonAccentSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly ReplacementAccentSystem _replacement = default!;
[GeneratedRegex(@"(? DirectReplacements = new()
- {
- { "fuck you", "I've got a BONE to pick with you" },
- { "fucked", "boned"},
- { "fuck", "RATTLE RATTLE" },
- { "fck", "RATTLE RATTLE" },
- { "shit", "RATTLE RATTLE" }, // Capitalize RATTLE RATTLE regardless of original message case.
- { "definitely", "make no bones about it" },
- { "absolutely", "make no bones about it" },
- { "afraid", "rattled"},
- { "scared", "rattled"},
- { "spooked", "rattled"},
- { "shocked", "rattled"},
- { "killed", "skeletonized"},
- { "humorous", "humerus"},
- { "to be a", "tibia"},
- { "under", "ulna"}
- };
-
public override void Initialize()
{
base.Initialize();
@@ -50,11 +32,8 @@ public string Accentuate(string message, SkeletonAccentComponent component)
// At the start of words, any non-vowel + "one" becomes "bone", e.g. tone -> bone ; lonely -> bonely; clone -> clone (remains unchanged).
msg = BoneRegex().Replace(msg, "bone");
- // Direct word/phrase replacements:
- foreach (var (first, replace) in DirectReplacements)
- {
- msg = Regex.Replace(msg, $@"(? Loc.GetString("ion-storm-law-crew-must-go", ("who", crewAll), ("area", area)),
27 => Loc.GetString("ion-storm-law-crew-only-1", ("who", crew1), ("part", part)),
28 => Loc.GetString("ion-storm-law-crew-only-2", ("who", crew1), ("other", crew2), ("part", part)),
- 29 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", RobustRandom.Prob(0.5f) ? objectsThreats : "PEOPLE"), ("part", part)),
+ 29 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", subjects), ("part", part)),
30 => Loc.GetString("ion-storm-law-crew-must-do", ("must", must), ("part", part)),
31 => Loc.GetString("ion-storm-law-crew-must-have", ("adjective", adjective), ("objects", objects), ("part", part)),
32 => Loc.GetString("ion-storm-law-crew-must-eat", ("who", who), ("adjective", adjective), ("food", food), ("part", part)),
diff --git a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
index c7d5665464..e941e65c41 100644
--- a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
+++ b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs
@@ -1,6 +1,9 @@
using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using Content.Server.Access.Systems;
using Content.Server.Forensics;
using Content.Server.GameTicking;
+using Content.Shared.Access.Components;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Preferences;
@@ -35,12 +38,14 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly StationRecordKeyStorageSystem _keyStorage = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IdCardSystem _idCard = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnPlayerSpawn);
+ SubscribeLocalEvent(OnRename);
}
private void OnPlayerSpawn(PlayerSpawnCompleteEvent args)
@@ -51,6 +56,30 @@ private void OnPlayerSpawn(PlayerSpawnCompleteEvent args)
CreateGeneralRecord(args.Station, args.Mob, args.Profile, args.JobId, stationRecords);
}
+ private void OnRename(ref EntityRenamedEvent ev)
+ {
+ // When a player gets renamed their card gets changed to match.
+ // Unfortunately this means that an event is called for it as well, and since TryFindIdCard will succeed if the
+ // given entity is a card and the card itself is the key the record will be mistakenly renamed to the card's name
+ // if we don't return early.
+ if (HasComp(ev.Uid))
+ return;
+
+ if (_idCard.TryFindIdCard(ev.Uid, out var idCard))
+ {
+ if (TryComp(idCard, out StationRecordKeyStorageComponent? keyStorage)
+ && keyStorage.Key is {} key)
+ {
+ if (TryGetRecord(key, out var generalRecord))
+ {
+ generalRecord.Name = ev.NewName;
+ }
+
+ Synchronize(key);
+ }
+ }
+ }
+
private void CreateGeneralRecord(EntityUid station, EntityUid player, HumanoidCharacterProfile profile,
string? jobId, StationRecordsComponent records)
{
diff --git a/Content.Server/Traits/TraitSystem.cs b/Content.Server/Traits/TraitSystem.cs
index 3bd540a304..e19f736f06 100644
--- a/Content.Server/Traits/TraitSystem.cs
+++ b/Content.Server/Traits/TraitSystem.cs
@@ -1,6 +1,7 @@
using Content.Server.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Roles;
using Content.Shared.Traits;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
@@ -24,6 +25,14 @@ public override void Initialize()
// When the player is spawned in, add all trait components selected during character creation
private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
{
+ // Check if player's job allows to apply traits
+ if (args.JobId == null ||
+ !_prototypeManager.TryIndex(args.JobId ?? string.Empty, out var protoJob) ||
+ !protoJob.ApplyTraits)
+ {
+ return;
+ }
+
foreach (var traitId in args.Profile.TraitPreferences)
{
if (!_prototypeManager.TryIndex(traitId, out var traitPrototype))
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ChemicalPuddleArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ChemicalPuddleArtifactSystem.cs
index cd312797ce..542d8bb84c 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ChemicalPuddleArtifactSystem.cs
+++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/ChemicalPuddleArtifactSystem.cs
@@ -18,7 +18,7 @@ public sealed class ChemicalPuddleArtifactSystem : EntitySystem
/// The key for the node data entry containing
/// the chemicals that the puddle is made of.
///
- public const string NodeDataChemicalList = "nodeDataSpawnAmount";
+ public const string NodeDataChemicalList = "nodeDataChemicalList";
///
public override void Initialize()
diff --git a/Content.Shared/Access/Systems/SharedIdCardSystem.cs b/Content.Shared/Access/Systems/SharedIdCardSystem.cs
index 5a90d4ea35..8bdc548e35 100644
--- a/Content.Shared/Access/Systems/SharedIdCardSystem.cs
+++ b/Content.Shared/Access/Systems/SharedIdCardSystem.cs
@@ -25,6 +25,19 @@ public override void Initialize()
SubscribeLocalEvent(OnMapInit);
SubscribeLocalEvent(OnTryGetIdentityShortInfo);
+ SubscribeLocalEvent(OnRename);
+ }
+
+ private void OnRename(ref EntityRenamedEvent ev)
+ {
+ // When a player gets renamed their id card is renamed as well to match.
+ // Unfortunately since TryFindIdCard will succeed if the entity is also a card this means that the card will
+ // keep renaming itself unless we return early.
+ if (HasComp(ev.Uid))
+ return;
+
+ if (TryFindIdCard(ev.Uid, out var idCard))
+ TryChangeFullName(idCard, ev.NewName, idCard);
}
private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args)
diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
index 83c24016ce..7f6c39eafc 100644
--- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
+++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
@@ -242,8 +242,9 @@ private bool CanBuckle(EntityUid buckleUid,
if (_whitelistSystem.IsWhitelistFail(strapComp.Whitelist, buckleUid) ||
_whitelistSystem.IsBlacklistPass(strapComp.Blacklist, buckleUid))
{
- if (_netManager.IsServer && popup && user != null)
- _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), user.Value, user.Value, PopupType.Medium);
+ if (popup)
+ _popup.PopupClient(Loc.GetString("buckle-component-cannot-fit-message"), user, PopupType.Medium);
+
return false;
}
@@ -261,23 +262,24 @@ private bool CanBuckle(EntityUid buckleUid,
if (user != null && !HasComp(user))
{
- // PopupPredicted when
- if (_netManager.IsServer && popup)
- _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user.Value, user.Value);
+ if (popup)
+ _popup.PopupClient(Loc.GetString("buckle-component-no-hands-message"), user);
+
return false;
}
if (buckleComp.Buckled)
{
- if (_netManager.IsClient || popup || user == null)
- return false;
-
- var message = Loc.GetString(buckleUid == user
+ if (popup)
+ {
+ var message = Loc.GetString(buckleUid == user
? "buckle-component-already-buckled-message"
: "buckle-component-other-already-buckled-message",
("owner", Identity.Entity(buckleUid, EntityManager)));
- _popup.PopupEntity(message, user.Value, user.Value);
+ _popup.PopupClient(message, user);
+ }
+
return false;
}
@@ -291,29 +293,30 @@ private bool CanBuckle(EntityUid buckleUid,
continue;
}
- if (_netManager.IsClient || popup || user == null)
- return false;
-
- var message = Loc.GetString(buckleUid == user
+ if (popup)
+ {
+ var message = Loc.GetString(buckleUid == user
? "buckle-component-cannot-buckle-message"
: "buckle-component-other-cannot-buckle-message",
("owner", Identity.Entity(buckleUid, EntityManager)));
- _popup.PopupEntity(message, user.Value, user.Value);
+ _popup.PopupClient(message, user);
+ }
+
return false;
}
if (!StrapHasSpace(strapUid, buckleComp, strapComp))
{
- if (_netManager.IsClient || popup || user == null)
- return false;
-
- var message = Loc.GetString(buckleUid == user
- ? "buckle-component-cannot-fit-message"
- : "buckle-component-other-cannot-fit-message",
+ if (popup)
+ {
+ var message = Loc.GetString(buckleUid == user
+ ? "buckle-component-cannot-buckle-message"
+ : "buckle-component-other-cannot-buckle-message",
("owner", Identity.Entity(buckleUid, EntityManager)));
- _popup.PopupEntity(message, user.Value, user.Value);
+ _popup.PopupClient(message, user);
+ }
return false;
}
diff --git a/Content.Shared/Buckle/SharedBuckleSystem.cs b/Content.Shared/Buckle/SharedBuckleSystem.cs
index d190f685ed..da1d111f97 100644
--- a/Content.Shared/Buckle/SharedBuckleSystem.cs
+++ b/Content.Shared/Buckle/SharedBuckleSystem.cs
@@ -9,7 +9,6 @@
using Content.Shared.Standing;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
-using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -18,7 +17,6 @@ namespace Content.Shared.Buckle;
public abstract partial class SharedBuckleSystem : EntitySystem
{
- [Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
diff --git a/Content.Shared/Chemistry/Components/InjectorComponent.cs b/Content.Shared/Chemistry/Components/InjectorComponent.cs
index 1f2716356c..17a65ef1c1 100644
--- a/Content.Shared/Chemistry/Components/InjectorComponent.cs
+++ b/Content.Shared/Chemistry/Components/InjectorComponent.cs
@@ -1,7 +1,9 @@
using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.Chemistry.Reagent;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Chemistry.Components;
@@ -88,6 +90,14 @@ public sealed partial class InjectorComponent : Component
[DataField]
public InjectorToggleMode ToggleState = InjectorToggleMode.Draw;
+ ///
+ /// Reagents that are allowed to be within this injector.
+ /// If a solution has both allowed and non-allowed reagents, only allowed reagents will be drawn into this injector.
+ /// A null ReagentWhitelist indicates all reagents are allowed.
+ ///
+ [DataField]
+ public List>? ReagentWhitelist = null;
+
#region Arguments for injection doafter
///
diff --git a/Content.Shared/Chemistry/Components/Solution.cs b/Content.Shared/Chemistry/Components/Solution.cs
index fc25781005..c65ba0e80e 100644
--- a/Content.Shared/Chemistry/Components/Solution.cs
+++ b/Content.Shared/Chemistry/Components/Solution.cs
@@ -612,7 +612,7 @@ public Solution SplitSolutionWithout(FixedPoint2 toTake, params string[] exclude
}
///
- /// Splits a solution without the specified reagent prototypes.
+ /// Splits a solution with only the specified reagent prototypes.
///
public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params string[] includedPrototypes)
{
diff --git a/Content.Shared/Doors/Components/AirlockComponent.cs b/Content.Shared/Doors/Components/AirlockComponent.cs
index b2fa7574f7..6577b1942a 100644
--- a/Content.Shared/Doors/Components/AirlockComponent.cs
+++ b/Content.Shared/Doors/Components/AirlockComponent.cs
@@ -48,7 +48,7 @@ public sealed partial class AirlockComponent : Component
///
/// Whether the airlock should auto close. This value is reset every time the airlock closes.
///
- [ViewVariables(VVAccess.ReadWrite)]
+ [DataField, AutoNetworkedField]
public bool AutoClose = true;
///
diff --git a/Content.Shared/Doors/Systems/SharedAirlockSystem.cs b/Content.Shared/Doors/Systems/SharedAirlockSystem.cs
index 5a6d45d9ec..c0c274207b 100644
--- a/Content.Shared/Doors/Systems/SharedAirlockSystem.cs
+++ b/Content.Shared/Doors/Systems/SharedAirlockSystem.cs
@@ -56,7 +56,10 @@ private void OnStateChanged(EntityUid uid, AirlockComponent component, DoorState
// Make sure the airlock auto closes again next time it is opened
if (args.State == DoorState.Closed)
+ {
component.AutoClose = true;
+ Dirty(uid, component);
+ }
}
private void OnBeforeDoorOpened(EntityUid uid, AirlockComponent component, BeforeDoorOpenedEvent args)
diff --git a/Content.Shared/Explosion/Components/OnUseTimerTriggerComponent.cs b/Content.Shared/Explosion/Components/OnUseTimerTriggerComponent.cs
index c4e6e787a4..983b8a31ee 100644
--- a/Content.Shared/Explosion/Components/OnUseTimerTriggerComponent.cs
+++ b/Content.Shared/Explosion/Components/OnUseTimerTriggerComponent.cs
@@ -1,3 +1,5 @@
+using System.Linq;
+using Content.Shared.Guidebook;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
@@ -50,5 +52,15 @@ public sealed partial class OnUseTimerTriggerComponent : Component
/// Whether or not to show the user a popup when starting the timer.
///
[DataField] public bool DoPopup = true;
+
+ #region GuidebookData
+
+ [GuidebookData]
+ public float? ShortestDelayOption => DelayOptions?.Min();
+
+ [GuidebookData]
+ public float? LongestDelayOption => DelayOptions?.Max();
+
+ #endregion GuidebookData
}
}
diff --git a/Content.Shared/Ghost/SharedGhostSystem.cs b/Content.Shared/Ghost/SharedGhostSystem.cs
index 2410704304..d2e2cd668a 100644
--- a/Content.Shared/Ghost/SharedGhostSystem.cs
+++ b/Content.Shared/Ghost/SharedGhostSystem.cs
@@ -69,7 +69,6 @@ public void SetCanReturnToBody(GhostComponent component, bool value)
public void SetColor(GhostComponent component, Color value)
{
component.color = value;
- Dirty(component);
}
}
diff --git a/Content.Shared/Guidebook/Events.cs b/Content.Shared/Guidebook/Events.cs
new file mode 100644
index 0000000000..e43bf4392c
--- /dev/null
+++ b/Content.Shared/Guidebook/Events.cs
@@ -0,0 +1,25 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Guidebook;
+
+///
+/// Raised by the client on GuidebookDataSystem Initialize to request a
+/// full set of guidebook data from the server.
+///
+[Serializable, NetSerializable]
+public sealed class RequestGuidebookDataEvent : EntityEventArgs { }
+
+///
+/// Raised by the server at a specific client in response to .
+/// Also raised by the server at ALL clients when prototype data is hot-reloaded.
+///
+[Serializable, NetSerializable]
+public sealed class UpdateGuidebookDataEvent : EntityEventArgs
+{
+ public GuidebookData Data;
+
+ public UpdateGuidebookDataEvent(GuidebookData data)
+ {
+ Data = data;
+ }
+}
diff --git a/Content.Shared/Guidebook/GuidebookData.cs b/Content.Shared/Guidebook/GuidebookData.cs
new file mode 100644
index 0000000000..703940ed1e
--- /dev/null
+++ b/Content.Shared/Guidebook/GuidebookData.cs
@@ -0,0 +1,99 @@
+using System.Collections.Frozen;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Guidebook;
+
+///
+/// Used by GuidebookDataSystem to hold data extracted from prototype values,
+/// both for storage and for network transmission.
+///
+[Serializable, NetSerializable]
+[DataDefinition]
+public sealed partial class GuidebookData
+{
+ ///
+ /// Total number of data values stored.
+ ///
+ [DataField]
+ public int Count { get; private set; }
+
+ ///
+ /// The data extracted by the system.
+ ///
+ ///
+ /// Structured as PrototypeName, ComponentName, FieldName, Value
+ ///
+ [DataField]
+ public Dictionary>> Data = [];
+
+ ///
+ /// The data extracted by the system, converted to a FrozenDictionary for faster lookup.
+ ///
+ public FrozenDictionary>> FrozenData;
+
+ ///
+ /// Has the data been converted to a FrozenDictionary for faster lookup?
+ /// This should only be done on clients, as FrozenDictionary isn't serializable.
+ ///
+ public bool IsFrozen;
+
+ ///
+ /// Adds a new value using the given identifiers.
+ ///
+ public void AddData(string prototype, string component, string field, object? value)
+ {
+ if (IsFrozen)
+ throw new InvalidOperationException("Attempted to add data to GuidebookData while it is frozen!");
+ Data.GetOrNew(prototype).GetOrNew(component).Add(field, value);
+ Count++;
+ }
+
+ ///
+ /// Attempts to retrieve a value using the given identifiers.
+ ///
+ /// true if the value was retrieved, otherwise false
+ public bool TryGetValue(string prototype, string component, string field, out object? value)
+ {
+ if (!IsFrozen)
+ throw new InvalidOperationException("Freeze the GuidebookData before calling TryGetValue!");
+
+ // Look in frozen dictionary
+ if (FrozenData.TryGetValue(prototype, out var p)
+ && p.TryGetValue(component, out var c)
+ && c.TryGetValue(field, out value))
+ {
+ return true;
+ }
+
+ value = null;
+ return false;
+ }
+
+ ///
+ /// Deletes all data.
+ ///
+ public void Clear()
+ {
+ Data.Clear();
+ Count = 0;
+ IsFrozen = false;
+ }
+
+ public void Freeze()
+ {
+ var protos = new Dictionary>>();
+ foreach (var (protoId, protoData) in Data)
+ {
+ var comps = new Dictionary>();
+ foreach (var (compId, compData) in protoData)
+ {
+ comps.Add(compId, FrozenDictionary.ToFrozenDictionary(compData));
+ }
+ protos.Add(protoId, FrozenDictionary.ToFrozenDictionary(comps));
+ }
+ FrozenData = FrozenDictionary.ToFrozenDictionary(protos);
+ Data.Clear();
+ IsFrozen = true;
+ }
+}
diff --git a/Content.Shared/Guidebook/GuidebookDataAttribute.cs b/Content.Shared/Guidebook/GuidebookDataAttribute.cs
new file mode 100644
index 0000000000..2b83892b88
--- /dev/null
+++ b/Content.Shared/Guidebook/GuidebookDataAttribute.cs
@@ -0,0 +1,12 @@
+namespace Content.Shared.Guidebook;
+
+///
+/// Indicates that GuidebookDataSystem should include this field/property when
+/// scanning entity prototypes for values to extract.
+///
+///
+/// Note that this will not work for client-only components, because the data extraction
+/// is done on the server (it uses reflection, which is blocked by the sandbox on clients).
+///
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
+public sealed class GuidebookDataAttribute : Attribute { }
diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs
index 32e090c137..2d7781bae5 100644
--- a/Content.Shared/Mind/SharedMindSystem.cs
+++ b/Content.Shared/Mind/SharedMindSystem.cs
@@ -39,6 +39,7 @@ public override void Initialize()
SubscribeLocalEvent(OnVisitingTerminating);
SubscribeLocalEvent(OnReset);
SubscribeLocalEvent(OnMindStartup);
+ SubscribeLocalEvent(OnRenamed);
}
public override void Shutdown()
@@ -181,6 +182,12 @@ private void OnSuicide(EntityUid uid, MindContainerComponent component, SuicideE
args.Handled = true;
}
+ private void OnRenamed(Entity ent, ref EntityRenamedEvent args)
+ {
+ ent.Comp.CharacterName = args.NewName;
+ Dirty(ent);
+ }
+
public EntityUid? GetMind(EntityUid uid, MindContainerComponent? mind = null)
{
if (!Resolve(uid, ref mind))
diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
index 5f35adb333..6392956d63 100644
--- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
+++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
@@ -438,7 +438,7 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid,
if (!CanPull(pullerUid, pullableUid))
return false;
- if (!HasComp(pullerUid) || !TryComp(pullableUid, out PhysicsComponent? pullablePhysics))
+ if (!TryComp(pullerUid, out PhysicsComponent? pullerPhysics) || !TryComp(pullableUid, out PhysicsComponent? pullablePhysics))
return false;
// Ensure that the puller is not currently pulling anything.
@@ -485,17 +485,19 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid,
// joint state handling will manage its own state
if (!_timing.ApplyingState)
{
- // Joint startup
- var union = _physics.GetHardAABB(pullerUid).Union(_physics.GetHardAABB(pullableUid, body: pullablePhysics));
- var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f;
-
- var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid, id: pullableComp.PullJointId);
+ var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid,
+ pullablePhysics.LocalCenter, pullerPhysics.LocalCenter,
+ id: pullableComp.PullJointId);
joint.CollideConnected = false;
// This maximum has to be there because if the object is constrained too closely, the clamping goes backwards and asserts.
- joint.MaxLength = Math.Max(1.0f, length);
- joint.Length = length * 0.75f;
+ // Internally, the joint length has been set to the distance between the pivots.
+ // Add an additional 15cm (pretty arbitrary) to the maximum length for the hard limit.
+ joint.MaxLength = joint.Length + 0.15f;
joint.MinLength = 0f;
- joint.Stiffness = 1f;
+ // Set the spring stiffness to zero. The joint won't have any effect provided
+ // the current length is beteen MinLength and MaxLength. At those limits, the
+ // joint will have infinite stiffness.
+ joint.Stiffness = 0f;
_physics.SetFixedRotation(pullableUid, pullableComp.FixedRotationOnPull, body: pullablePhysics);
}
diff --git a/Content.Shared/Movement/Systems/SharedMoverController.cs b/Content.Shared/Movement/Systems/SharedMoverController.cs
index c41db21b01..472d56b1d6 100644
--- a/Content.Shared/Movement/Systems/SharedMoverController.cs
+++ b/Content.Shared/Movement/Systems/SharedMoverController.cs
@@ -24,492 +24,491 @@
using Robust.Shared.Utility;
using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent;
-namespace Content.Shared.Movement.Systems
+namespace Content.Shared.Movement.Systems;
+
+///
+/// Handles player and NPC mob movement.
+/// NPCs are handled server-side only.
+///
+public abstract partial class SharedMoverController : VirtualController
{
+ [Dependency] private readonly IConfigurationManager _configManager = default!;
+ [Dependency] protected readonly IGameTiming Timing = default!;
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
+ [Dependency] private readonly EntityLookupSystem _lookup = default!;
+ [Dependency] private readonly InventorySystem _inventory = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly SharedMapSystem _mapSystem = default!;
+ [Dependency] private readonly SharedGravitySystem _gravity = default!;
+ [Dependency] protected readonly SharedPhysicsSystem Physics = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly TagSystem _tags = default!;
+
+ protected EntityQuery MoverQuery;
+ protected EntityQuery MobMoverQuery;
+ protected EntityQuery RelayTargetQuery;
+ protected EntityQuery ModifierQuery;
+ protected EntityQuery PhysicsQuery;
+ protected EntityQuery RelayQuery;
+ protected EntityQuery PullableQuery;
+ protected EntityQuery XformQuery;
+ protected EntityQuery CanMoveInAirQuery;
+ protected EntityQuery NoRotateQuery;
+ protected EntityQuery FootstepModifierQuery;
+ protected EntityQuery MapGridQuery;
+
+ ///
+ ///
+ ///
+ private float _stopSpeed;
+
+ private bool _relativeMovement;
+
///
- /// Handles player and NPC mob movement.
- /// NPCs are handled server-side only.
+ /// Cache the mob movement calculation to re-use elsewhere.
///
- public abstract partial class SharedMoverController : VirtualController
+ public Dictionary UsedMobMovement = new();
+
+ public override void Initialize()
{
- [Dependency] private readonly IConfigurationManager _configManager = default!;
- [Dependency] protected readonly IGameTiming Timing = default!;
- [Dependency] private readonly IMapManager _mapManager = default!;
- [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
- [Dependency] private readonly EntityLookupSystem _lookup = default!;
- [Dependency] private readonly InventorySystem _inventory = default!;
- [Dependency] private readonly MobStateSystem _mobState = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedContainerSystem _container = default!;
- [Dependency] private readonly SharedMapSystem _mapSystem = default!;
- [Dependency] private readonly SharedGravitySystem _gravity = default!;
- [Dependency] protected readonly SharedPhysicsSystem Physics = default!;
- [Dependency] private readonly SharedTransformSystem _transform = default!;
- [Dependency] private readonly TagSystem _tags = default!;
-
- protected EntityQuery MoverQuery;
- protected EntityQuery MobMoverQuery;
- protected EntityQuery RelayTargetQuery;
- protected EntityQuery ModifierQuery;
- protected EntityQuery PhysicsQuery;
- protected EntityQuery RelayQuery;
- protected EntityQuery PullableQuery;
- protected EntityQuery XformQuery;
- protected EntityQuery CanMoveInAirQuery;
- protected EntityQuery NoRotateQuery;
- protected EntityQuery FootstepModifierQuery;
- protected EntityQuery MapGridQuery;
-
- ///
- ///
- ///
- private float _stopSpeed;
-
- private bool _relativeMovement;
-
- ///
- /// Cache the mob movement calculation to re-use elsewhere.
- ///
- public Dictionary UsedMobMovement = new();
-
- public override void Initialize()
- {
- base.Initialize();
-
- MoverQuery = GetEntityQuery();
- MobMoverQuery = GetEntityQuery();
- ModifierQuery = GetEntityQuery();
- RelayTargetQuery = GetEntityQuery();
- PhysicsQuery = GetEntityQuery();
- RelayQuery = GetEntityQuery();
- PullableQuery = GetEntityQuery();
- XformQuery = GetEntityQuery();
- NoRotateQuery = GetEntityQuery();
- CanMoveInAirQuery = GetEntityQuery();
- FootstepModifierQuery = GetEntityQuery();
- MapGridQuery = GetEntityQuery();
-
- InitializeInput();
- InitializeRelay();
- Subs.CVar(_configManager, CCVars.RelativeMovement, value => _relativeMovement = value, true);
- Subs.CVar(_configManager, CCVars.StopSpeed, value => _stopSpeed = value, true);
- UpdatesBefore.Add(typeof(TileFrictionController));
- }
+ base.Initialize();
+
+ MoverQuery = GetEntityQuery();
+ MobMoverQuery = GetEntityQuery();
+ ModifierQuery = GetEntityQuery();
+ RelayTargetQuery = GetEntityQuery();
+ PhysicsQuery = GetEntityQuery();
+ RelayQuery = GetEntityQuery();
+ PullableQuery = GetEntityQuery();
+ XformQuery = GetEntityQuery();
+ NoRotateQuery = GetEntityQuery();
+ CanMoveInAirQuery = GetEntityQuery();
+ FootstepModifierQuery = GetEntityQuery();
+ MapGridQuery = GetEntityQuery();
+
+ InitializeInput();
+ InitializeRelay();
+ Subs.CVar(_configManager, CCVars.RelativeMovement, value => _relativeMovement = value, true);
+ Subs.CVar(_configManager, CCVars.StopSpeed, value => _stopSpeed = value, true);
+ UpdatesBefore.Add(typeof(TileFrictionController));
+ }
- public override void Shutdown()
- {
- base.Shutdown();
- ShutdownInput();
- }
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ ShutdownInput();
+ }
- public override void UpdateAfterSolve(bool prediction, float frameTime)
- {
- base.UpdateAfterSolve(prediction, frameTime);
- UsedMobMovement.Clear();
- }
+ public override void UpdateAfterSolve(bool prediction, float frameTime)
+ {
+ base.UpdateAfterSolve(prediction, frameTime);
+ UsedMobMovement.Clear();
+ }
- ///
- /// Movement while considering actionblockers, weightlessness, etc.
- ///
- protected void HandleMobMovement(
- EntityUid uid,
- InputMoverComponent mover,
- EntityUid physicsUid,
- PhysicsComponent physicsComponent,
- TransformComponent xform,
- float frameTime)
+ ///
+ /// Movement while considering actionblockers, weightlessness, etc.
+ ///
+ protected void HandleMobMovement(
+ EntityUid uid,
+ InputMoverComponent mover,
+ EntityUid physicsUid,
+ PhysicsComponent physicsComponent,
+ TransformComponent xform,
+ float frameTime)
+ {
+ var canMove = mover.CanMove;
+ if (RelayTargetQuery.TryGetComponent(uid, out var relayTarget))
{
- var canMove = mover.CanMove;
- if (RelayTargetQuery.TryGetComponent(uid, out var relayTarget))
+ if (_mobState.IsIncapacitated(relayTarget.Source) ||
+ TryComp(relayTarget.Source, out _) ||
+ !MoverQuery.TryGetComponent(relayTarget.Source, out var relayedMover))
{
- if (_mobState.IsIncapacitated(relayTarget.Source) ||
- TryComp(relayTarget.Source, out _) ||
- !MoverQuery.TryGetComponent(relayTarget.Source, out var relayedMover))
- {
- canMove = false;
- }
- else
- {
- mover.RelativeEntity = relayedMover.RelativeEntity;
- mover.RelativeRotation = relayedMover.RelativeRotation;
- mover.TargetRelativeRotation = relayedMover.TargetRelativeRotation;
- }
+ canMove = false;
}
-
- // Update relative movement
- if (mover.LerpTarget < Timing.CurTime)
+ else
{
- if (TryUpdateRelative(mover, xform))
- {
- Dirty(uid, mover);
- }
+ mover.RelativeEntity = relayedMover.RelativeEntity;
+ mover.RelativeRotation = relayedMover.RelativeRotation;
+ mover.TargetRelativeRotation = relayedMover.TargetRelativeRotation;
}
+ }
- LerpRotation(uid, mover, frameTime);
-
- if (!canMove
- || physicsComponent.BodyStatus != BodyStatus.OnGround && !CanMoveInAirQuery.HasComponent(uid)
- || PullableQuery.TryGetComponent(uid, out var pullable) && pullable.BeingPulled)
+ // Update relative movement
+ if (mover.LerpTarget < Timing.CurTime)
+ {
+ if (TryUpdateRelative(mover, xform))
{
- UsedMobMovement[uid] = false;
- return;
+ Dirty(uid, mover);
}
+ }
+ LerpRotation(uid, mover, frameTime);
- UsedMobMovement[uid] = true;
- // Specifically don't use mover.Owner because that may be different to the actual physics body being moved.
- var weightless = _gravity.IsWeightless(physicsUid, physicsComponent, xform);
- var (walkDir, sprintDir) = GetVelocityInput(mover);
- var touching = false;
-
- // Handle wall-pushes.
- if (weightless)
- {
- if (xform.GridUid != null)
- touching = true;
+ if (!canMove
+ || physicsComponent.BodyStatus != BodyStatus.OnGround && !CanMoveInAirQuery.HasComponent(uid)
+ || PullableQuery.TryGetComponent(uid, out var pullable) && pullable.BeingPulled)
+ {
+ UsedMobMovement[uid] = false;
+ return;
+ }
- if (!touching)
- {
- var ev = new CanWeightlessMoveEvent(uid);
- RaiseLocalEvent(uid, ref ev, true);
- // No gravity: is our entity touching anything?
- touching = ev.CanMove;
- if (!touching && TryComp(uid, out var mobMover))
- touching |= IsAroundCollider(PhysicsSystem, xform, mobMover, physicsUid, physicsComponent);
- }
- }
+ UsedMobMovement[uid] = true;
+ // Specifically don't use mover.Owner because that may be different to the actual physics body being moved.
+ var weightless = _gravity.IsWeightless(physicsUid, physicsComponent, xform);
+ var (walkDir, sprintDir) = GetVelocityInput(mover);
+ var touching = false;
- // Get current tile def for things like speed/friction mods
- ContentTileDefinition? tileDef = null;
+ // Handle wall-pushes.
+ if (weightless)
+ {
+ if (xform.GridUid != null)
+ touching = true;
- // Don't bother getting the tiledef here if we're weightless or in-air
- // since no tile-based modifiers should be applying in that situation
- if (MapGridQuery.TryComp(xform.GridUid, out var gridComp)
- && _mapSystem.TryGetTileRef(xform.GridUid.Value, gridComp, xform.Coordinates, out var tile)
- && !(weightless || physicsComponent.BodyStatus == BodyStatus.InAir))
+ if (!touching)
{
- tileDef = (ContentTileDefinition) _tileDefinitionManager[tile.Tile.TypeId];
+ var ev = new CanWeightlessMoveEvent(uid);
+ RaiseLocalEvent(uid, ref ev, true);
+ // No gravity: is our entity touching anything?
+ touching = ev.CanMove;
+
+ if (!touching && TryComp(uid, out var mobMover))
+ touching |= IsAroundCollider(PhysicsSystem, xform, mobMover, physicsUid, physicsComponent);
}
+ }
- // Regular movement.
- // Target velocity.
- // This is relative to the map / grid we're on.
- var moveSpeedComponent = ModifierQuery.CompOrNull(uid);
+ // Get current tile def for things like speed/friction mods
+ ContentTileDefinition? tileDef = null;
- var walkSpeed = moveSpeedComponent?.CurrentWalkSpeed ?? MovementSpeedModifierComponent.DefaultBaseWalkSpeed;
- var sprintSpeed = moveSpeedComponent?.CurrentSprintSpeed ?? MovementSpeedModifierComponent.DefaultBaseSprintSpeed;
+ // Don't bother getting the tiledef here if we're weightless or in-air
+ // since no tile-based modifiers should be applying in that situation
+ if (MapGridQuery.TryComp(xform.GridUid, out var gridComp)
+ && _mapSystem.TryGetTileRef(xform.GridUid.Value, gridComp, xform.Coordinates, out var tile)
+ && !(weightless || physicsComponent.BodyStatus == BodyStatus.InAir))
+ {
+ tileDef = (ContentTileDefinition) _tileDefinitionManager[tile.Tile.TypeId];
+ }
- var total = walkDir * walkSpeed + sprintDir * sprintSpeed;
+ // Regular movement.
+ // Target velocity.
+ // This is relative to the map / grid we're on.
+ var moveSpeedComponent = ModifierQuery.CompOrNull(uid);
- var parentRotation = GetParentGridAngle(mover);
- var worldTotal = _relativeMovement ? parentRotation.RotateVec(total) : total;
+ var walkSpeed = moveSpeedComponent?.CurrentWalkSpeed ?? MovementSpeedModifierComponent.DefaultBaseWalkSpeed;
+ var sprintSpeed = moveSpeedComponent?.CurrentSprintSpeed ?? MovementSpeedModifierComponent.DefaultBaseSprintSpeed;
- DebugTools.Assert(MathHelper.CloseToPercent(total.Length(), worldTotal.Length()));
+ var total = walkDir * walkSpeed + sprintDir * sprintSpeed;
- var velocity = physicsComponent.LinearVelocity;
- float friction;
- float weightlessModifier;
- float accel;
+ var parentRotation = GetParentGridAngle(mover);
+ var worldTotal = _relativeMovement ? parentRotation.RotateVec(total) : total;
- if (weightless)
- {
- if (gridComp == null && !MapGridQuery.HasComp(xform.GridUid))
- friction = moveSpeedComponent?.OffGridFriction ?? MovementSpeedModifierComponent.DefaultOffGridFriction;
- else if (worldTotal != Vector2.Zero && touching)
- friction = moveSpeedComponent?.WeightlessFriction ?? MovementSpeedModifierComponent.DefaultWeightlessFriction;
- else
- friction = moveSpeedComponent?.WeightlessFrictionNoInput ?? MovementSpeedModifierComponent.DefaultWeightlessFrictionNoInput;
+ DebugTools.Assert(MathHelper.CloseToPercent(total.Length(), worldTotal.Length()));
+
+ var velocity = physicsComponent.LinearVelocity;
+ float friction;
+ float weightlessModifier;
+ float accel;
+
+ if (weightless)
+ {
+ if (gridComp == null && !MapGridQuery.HasComp(xform.GridUid))
+ friction = moveSpeedComponent?.OffGridFriction ?? MovementSpeedModifierComponent.DefaultOffGridFriction;
+ else if (worldTotal != Vector2.Zero && touching)
+ friction = moveSpeedComponent?.WeightlessFriction ?? MovementSpeedModifierComponent.DefaultWeightlessFriction;
+ else
+ friction = moveSpeedComponent?.WeightlessFrictionNoInput ?? MovementSpeedModifierComponent.DefaultWeightlessFrictionNoInput;
- weightlessModifier = moveSpeedComponent?.WeightlessModifier ?? MovementSpeedModifierComponent.DefaultWeightlessModifier;
- accel = moveSpeedComponent?.WeightlessAcceleration ?? MovementSpeedModifierComponent.DefaultWeightlessAcceleration;
+ weightlessModifier = moveSpeedComponent?.WeightlessModifier ?? MovementSpeedModifierComponent.DefaultWeightlessModifier;
+ accel = moveSpeedComponent?.WeightlessAcceleration ?? MovementSpeedModifierComponent.DefaultWeightlessAcceleration;
+ }
+ else
+ {
+ if (worldTotal != Vector2.Zero || moveSpeedComponent?.FrictionNoInput == null)
+ {
+ friction = tileDef?.MobFriction ?? moveSpeedComponent?.Friction ?? MovementSpeedModifierComponent.DefaultFriction;
}
else
{
- if (worldTotal != Vector2.Zero || moveSpeedComponent?.FrictionNoInput == null)
- {
- friction = tileDef?.MobFriction ?? moveSpeedComponent?.Friction ?? MovementSpeedModifierComponent.DefaultFriction;
- }
- else
- {
- friction = tileDef?.MobFrictionNoInput ?? moveSpeedComponent.FrictionNoInput ?? MovementSpeedModifierComponent.DefaultFrictionNoInput;
- }
-
- weightlessModifier = 1f;
- accel = tileDef?.MobAcceleration ?? moveSpeedComponent?.Acceleration ?? MovementSpeedModifierComponent.DefaultAcceleration;
+ friction = tileDef?.MobFrictionNoInput ?? moveSpeedComponent.FrictionNoInput ?? MovementSpeedModifierComponent.DefaultFrictionNoInput;
}
- var minimumFrictionSpeed = moveSpeedComponent?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed;
- Friction(minimumFrictionSpeed, frameTime, friction, ref velocity);
+ weightlessModifier = 1f;
+ accel = tileDef?.MobAcceleration ?? moveSpeedComponent?.Acceleration ?? MovementSpeedModifierComponent.DefaultAcceleration;
+ }
- if (worldTotal != Vector2.Zero)
+ var minimumFrictionSpeed = moveSpeedComponent?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed;
+ Friction(minimumFrictionSpeed, frameTime, friction, ref velocity);
+
+ if (worldTotal != Vector2.Zero)
+ {
+ if (!NoRotateQuery.HasComponent(uid))
+ {
+ // TODO apparently this results in a duplicate move event because "This should have its event run during
+ // island solver"??. So maybe SetRotation needs an argument to avoid raising an event?
+ var worldRot = _transform.GetWorldRotation(xform);
+ _transform.SetLocalRotation(xform, xform.LocalRotation + worldTotal.ToWorldAngle() - worldRot);
+ }
+
+ if (!weightless && MobMoverQuery.TryGetComponent(uid, out var mobMover) &&
+ TryGetSound(weightless, uid, mover, mobMover, xform, out var sound, tileDef: tileDef))
{
- if (!NoRotateQuery.HasComponent(uid))
+ var soundModifier = mover.Sprinting ? 3.5f : 1.5f;
+
+ var audioParams = sound.Params
+ .WithVolume(sound.Params.Volume + soundModifier)
+ .WithVariation(sound.Params.Variation ?? mobMover.FootstepVariation);
+
+ // If we're a relay target then predict the sound for all relays.
+ if (relayTarget != null)
{
- // TODO apparently this results in a duplicate move event because "This should have its event run during
- // island solver"??. So maybe SetRotation needs an argument to avoid raising an event?
- var worldRot = _transform.GetWorldRotation(xform);
- _transform.SetLocalRotation(xform, xform.LocalRotation + worldTotal.ToWorldAngle() - worldRot);
+ _audio.PlayPredicted(sound, uid, relayTarget.Source, audioParams);
}
-
- if (!weightless && MobMoverQuery.TryGetComponent(uid, out var mobMover) &&
- TryGetSound(weightless, uid, mover, mobMover, xform, out var sound, tileDef: tileDef))
+ else
{
- var soundModifier = mover.Sprinting ? 3.5f : 1.5f;
-
- var audioParams = sound.Params
- .WithVolume(sound.Params.Volume + soundModifier)
- .WithVariation(sound.Params.Variation ?? mobMover.FootstepVariation);
-
- // If we're a relay target then predict the sound for all relays.
- if (relayTarget != null)
- {
- _audio.PlayPredicted(sound, uid, relayTarget.Source, audioParams);
- }
- else
- {
- _audio.PlayPredicted(sound, uid, uid, audioParams);
- }
+ _audio.PlayPredicted(sound, uid, uid, audioParams);
}
}
+ }
- worldTotal *= weightlessModifier;
+ worldTotal *= weightlessModifier;
- if (!weightless || touching)
- Accelerate(ref velocity, in worldTotal, accel, frameTime);
+ if (!weightless || touching)
+ Accelerate(ref velocity, in worldTotal, accel, frameTime);
- PhysicsSystem.SetLinearVelocity(physicsUid, velocity, body: physicsComponent);
+ PhysicsSystem.SetLinearVelocity(physicsUid, velocity, body: physicsComponent);
- // Ensures that players do not spiiiiiiin
- PhysicsSystem.SetAngularVelocity(physicsUid, 0, body: physicsComponent);
- }
+ // Ensures that players do not spiiiiiiin
+ PhysicsSystem.SetAngularVelocity(physicsUid, 0, body: physicsComponent);
+ }
- public void LerpRotation(EntityUid uid, InputMoverComponent mover, float frameTime)
+ public void LerpRotation(EntityUid uid, InputMoverComponent mover, float frameTime)
+ {
+ var angleDiff = Angle.ShortestDistance(mover.RelativeRotation, mover.TargetRelativeRotation);
+
+ // if we've just traversed then lerp to our target rotation.
+ if (!angleDiff.EqualsApprox(Angle.Zero, 0.001))
{
- var angleDiff = Angle.ShortestDistance(mover.RelativeRotation, mover.TargetRelativeRotation);
+ var adjustment = angleDiff * 5f * frameTime;
+ var minAdjustment = 0.01 * frameTime;
- // if we've just traversed then lerp to our target rotation.
- if (!angleDiff.EqualsApprox(Angle.Zero, 0.001))
+ if (angleDiff < 0)
{
- var adjustment = angleDiff * 5f * frameTime;
- var minAdjustment = 0.01 * frameTime;
-
- if (angleDiff < 0)
- {
- adjustment = Math.Min(adjustment, -minAdjustment);
- adjustment = Math.Clamp(adjustment, angleDiff, -angleDiff);
- }
- else
- {
- adjustment = Math.Max(adjustment, minAdjustment);
- adjustment = Math.Clamp(adjustment, -angleDiff, angleDiff);
- }
-
- mover.RelativeRotation += adjustment;
- mover.RelativeRotation.FlipPositive();
- Dirty(uid, mover);
+ adjustment = Math.Min(adjustment, -minAdjustment);
+ adjustment = Math.Clamp(adjustment, angleDiff, -angleDiff);
}
- else if (!angleDiff.Equals(Angle.Zero))
+ else
{
- mover.TargetRelativeRotation.FlipPositive();
- mover.RelativeRotation = mover.TargetRelativeRotation;
- Dirty(uid, mover);
+ adjustment = Math.Max(adjustment, minAdjustment);
+ adjustment = Math.Clamp(adjustment, -angleDiff, angleDiff);
}
- }
- private void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity)
+ mover.RelativeRotation += adjustment;
+ mover.RelativeRotation.FlipPositive();
+ Dirty(uid, mover);
+ }
+ else if (!angleDiff.Equals(Angle.Zero))
{
- var speed = velocity.Length();
+ mover.TargetRelativeRotation.FlipPositive();
+ mover.RelativeRotation = mover.TargetRelativeRotation;
+ Dirty(uid, mover);
+ }
+ }
- if (speed < minimumFrictionSpeed)
- return;
+ private void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity)
+ {
+ var speed = velocity.Length();
- var drop = 0f;
+ if (speed < minimumFrictionSpeed)
+ return;
- var control = MathF.Max(_stopSpeed, speed);
- drop += control * friction * frameTime;
+ var drop = 0f;
- var newSpeed = MathF.Max(0f, speed - drop);
+ var control = MathF.Max(_stopSpeed, speed);
+ drop += control * friction * frameTime;
- if (newSpeed.Equals(speed))
- return;
+ var newSpeed = MathF.Max(0f, speed - drop);
- newSpeed /= speed;
- velocity *= newSpeed;
- }
+ if (newSpeed.Equals(speed))
+ return;
- private void Accelerate(ref Vector2 currentVelocity, in Vector2 velocity, float accel, float frameTime)
- {
- var wishDir = velocity != Vector2.Zero ? velocity.Normalized() : Vector2.Zero;
- var wishSpeed = velocity.Length();
+ newSpeed /= speed;
+ velocity *= newSpeed;
+ }
- var currentSpeed = Vector2.Dot(currentVelocity, wishDir);
- var addSpeed = wishSpeed - currentSpeed;
+ private void Accelerate(ref Vector2 currentVelocity, in Vector2 velocity, float accel, float frameTime)
+ {
+ var wishDir = velocity != Vector2.Zero ? velocity.Normalized() : Vector2.Zero;
+ var wishSpeed = velocity.Length();
- if (addSpeed <= 0f)
- return;
+ var currentSpeed = Vector2.Dot(currentVelocity, wishDir);
+ var addSpeed = wishSpeed - currentSpeed;
- var accelSpeed = accel * frameTime * wishSpeed;
- accelSpeed = MathF.Min(accelSpeed, addSpeed);
+ if (addSpeed <= 0f)
+ return;
- currentVelocity += wishDir * accelSpeed;
- }
+ var accelSpeed = accel * frameTime * wishSpeed;
+ accelSpeed = MathF.Min(accelSpeed, addSpeed);
- public bool UseMobMovement(EntityUid uid)
- {
- return UsedMobMovement.TryGetValue(uid, out var used) && used;
- }
+ currentVelocity += wishDir * accelSpeed;
+ }
- ///
- /// Used for weightlessness to determine if we are near a wall.
- ///
- private bool IsAroundCollider(SharedPhysicsSystem broadPhaseSystem, TransformComponent transform, MobMoverComponent mover, EntityUid physicsUid, PhysicsComponent collider)
- {
- var enlargedAABB = _lookup.GetWorldAABB(physicsUid, transform).Enlarged(mover.GrabRangeVV);
+ public bool UseMobMovement(EntityUid uid)
+ {
+ return UsedMobMovement.TryGetValue(uid, out var used) && used;
+ }
- foreach (var otherCollider in broadPhaseSystem.GetCollidingEntities(transform.MapID, enlargedAABB))
- {
- if (otherCollider == collider)
- continue; // Don't try to push off of yourself!
-
- // Only allow pushing off of anchored things that have collision.
- if (otherCollider.BodyType != BodyType.Static ||
- !otherCollider.CanCollide ||
- ((collider.CollisionMask & otherCollider.CollisionLayer) == 0 &&
- (otherCollider.CollisionMask & collider.CollisionLayer) == 0) ||
- (TryComp(otherCollider.Owner, out PullableComponent? pullable) && pullable.BeingPulled))
- {
- continue;
- }
+ ///
+ /// Used for weightlessness to determine if we are near a wall.
+ ///
+ private bool IsAroundCollider(SharedPhysicsSystem broadPhaseSystem, TransformComponent transform, MobMoverComponent mover, EntityUid physicsUid, PhysicsComponent collider)
+ {
+ var enlargedAABB = _lookup.GetWorldAABB(physicsUid, transform).Enlarged(mover.GrabRangeVV);
- return true;
+ foreach (var otherCollider in broadPhaseSystem.GetCollidingEntities(transform.MapID, enlargedAABB))
+ {
+ if (otherCollider == collider)
+ continue; // Don't try to push off of yourself!
+
+ // Only allow pushing off of anchored things that have collision.
+ if (otherCollider.BodyType != BodyType.Static ||
+ !otherCollider.CanCollide ||
+ ((collider.CollisionMask & otherCollider.CollisionLayer) == 0 &&
+ (otherCollider.CollisionMask & collider.CollisionLayer) == 0) ||
+ (TryComp(otherCollider.Owner, out PullableComponent? pullable) && pullable.BeingPulled))
+ {
+ continue;
}
- return false;
+ return true;
}
- protected abstract bool CanSound();
+ return false;
+ }
+
+ protected abstract bool CanSound();
- private bool TryGetSound(
- bool weightless,
- EntityUid uid,
- InputMoverComponent mover,
- MobMoverComponent mobMover,
- TransformComponent xform,
- [NotNullWhen(true)] out SoundSpecifier? sound,
- ContentTileDefinition? tileDef = null)
- {
- sound = null;
+ private bool TryGetSound(
+ bool weightless,
+ EntityUid uid,
+ InputMoverComponent mover,
+ MobMoverComponent mobMover,
+ TransformComponent xform,
+ [NotNullWhen(true)] out SoundSpecifier? sound,
+ ContentTileDefinition? tileDef = null)
+ {
+ sound = null;
- if (!CanSound() || !_tags.HasTag(uid, "FootstepSound"))
- return false;
+ if (!CanSound() || !_tags.HasTag(uid, "FootstepSound"))
+ return false;
- var coordinates = xform.Coordinates;
- var distanceNeeded = mover.Sprinting
- ? mobMover.StepSoundMoveDistanceRunning
- : mobMover.StepSoundMoveDistanceWalking;
+ var coordinates = xform.Coordinates;
+ var distanceNeeded = mover.Sprinting
+ ? mobMover.StepSoundMoveDistanceRunning
+ : mobMover.StepSoundMoveDistanceWalking;
- // Handle footsteps.
- if (!weightless)
+ // Handle footsteps.
+ if (!weightless)
+ {
+ // Can happen when teleporting between grids.
+ if (!coordinates.TryDistance(EntityManager, mobMover.LastPosition, out var distance) ||
+ distance > distanceNeeded)
{
- // Can happen when teleporting between grids.
- if (!coordinates.TryDistance(EntityManager, mobMover.LastPosition, out var distance) ||
- distance > distanceNeeded)
- {
- mobMover.StepSoundDistance = distanceNeeded;
- }
- else
- {
- mobMover.StepSoundDistance += distance;
- }
+ mobMover.StepSoundDistance = distanceNeeded;
}
else
{
- // In space no one can hear you squeak
- return false;
+ mobMover.StepSoundDistance += distance;
}
+ }
+ else
+ {
+ // In space no one can hear you squeak
+ return false;
+ }
- mobMover.LastPosition = coordinates;
+ mobMover.LastPosition = coordinates;
- if (mobMover.StepSoundDistance < distanceNeeded)
- return false;
+ if (mobMover.StepSoundDistance < distanceNeeded)
+ return false;
- mobMover.StepSoundDistance -= distanceNeeded;
+ mobMover.StepSoundDistance -= distanceNeeded;
- if (FootstepModifierQuery.TryComp(uid, out var moverModifier))
- {
- sound = moverModifier.FootstepSoundCollection;
- return true;
- }
+ if (FootstepModifierQuery.TryComp(uid, out var moverModifier))
+ {
+ sound = moverModifier.FootstepSoundCollection;
+ return true;
+ }
- if (_inventory.TryGetSlotEntity(uid, "shoes", out var shoes) &&
- FootstepModifierQuery.TryComp(shoes, out var modifier))
+ if (_inventory.TryGetSlotEntity(uid, "shoes", out var shoes) &&
+ FootstepModifierQuery.TryComp(shoes, out var modifier))
+ {
+ sound = modifier.FootstepSoundCollection;
+ return true;
+ }
+
+ return TryGetFootstepSound(uid, xform, shoes != null, out sound, tileDef: tileDef);
+ }
+
+ private bool TryGetFootstepSound(
+ EntityUid uid,
+ TransformComponent xform,
+ bool haveShoes,
+ [NotNullWhen(true)] out SoundSpecifier? sound,
+ ContentTileDefinition? tileDef = null)
+ {
+ sound = null;
+
+ // Fallback to the map?
+ if (!MapGridQuery.TryComp(xform.GridUid, out var grid))
+ {
+ if (FootstepModifierQuery.TryComp(xform.MapUid, out var modifier))
{
sound = modifier.FootstepSoundCollection;
return true;
}
- return TryGetFootstepSound(uid, xform, shoes != null, out sound, tileDef: tileDef);
+ return false;
}
- private bool TryGetFootstepSound(
- EntityUid uid,
- TransformComponent xform,
- bool haveShoes,
- [NotNullWhen(true)] out SoundSpecifier? sound,
- ContentTileDefinition? tileDef = null)
- {
- sound = null;
+ var position = grid.LocalToTile(xform.Coordinates);
+ var soundEv = new GetFootstepSoundEvent(uid);
- // Fallback to the map?
- if (!MapGridQuery.TryComp(xform.GridUid, out var grid))
- {
- if (FootstepModifierQuery.TryComp(xform.MapUid, out var modifier))
- {
- sound = modifier.FootstepSoundCollection;
- return true;
- }
-
- return false;
- }
-
- var position = grid.LocalToTile(xform.Coordinates);
- var soundEv = new GetFootstepSoundEvent(uid);
+ // If the coordinates have a FootstepModifier component
+ // i.e. component that emit sound on footsteps emit that sound
+ var anchored = grid.GetAnchoredEntitiesEnumerator(position);
- // If the coordinates have a FootstepModifier component
- // i.e. component that emit sound on footsteps emit that sound
- var anchored = grid.GetAnchoredEntitiesEnumerator(position);
+ while (anchored.MoveNext(out var maybeFootstep))
+ {
+ RaiseLocalEvent(maybeFootstep.Value, ref soundEv);
- while (anchored.MoveNext(out var maybeFootstep))
+ if (soundEv.Sound != null)
{
- RaiseLocalEvent(maybeFootstep.Value, ref soundEv);
-
- if (soundEv.Sound != null)
- {
- sound = soundEv.Sound;
- return true;
- }
-
- if (FootstepModifierQuery.TryComp(maybeFootstep, out var footstep))
- {
- sound = footstep.FootstepSoundCollection;
- return true;
- }
+ sound = soundEv.Sound;
+ return true;
}
- // Walking on a tile.
- // Tile def might have been passed in already from previous methods, so use that
- // if we have it
- if (tileDef == null && grid.TryGetTileRef(position, out var tileRef))
+ if (FootstepModifierQuery.TryComp(maybeFootstep, out var footstep))
{
- tileDef = (ContentTileDefinition) _tileDefinitionManager[tileRef.Tile.TypeId];
+ sound = footstep.FootstepSoundCollection;
+ return true;
}
+ }
- if (tileDef == null)
- return false;
-
- sound = haveShoes ? tileDef.FootstepSounds : tileDef.BarestepSounds;
- return sound != null;
+ // Walking on a tile.
+ // Tile def might have been passed in already from previous methods, so use that
+ // if we have it
+ if (tileDef == null && grid.TryGetTileRef(position, out var tileRef))
+ {
+ tileDef = (ContentTileDefinition) _tileDefinitionManager[tileRef.Tile.TypeId];
}
+
+ if (tileDef == null)
+ return false;
+
+ sound = haveShoes ? tileDef.FootstepSounds : tileDef.BarestepSounds;
+ return sound != null;
}
}
diff --git a/Content.Shared/NameModifier/EntitySystems/NameModifierSystem.cs b/Content.Shared/NameModifier/EntitySystems/NameModifierSystem.cs
index 4dffb51805..2e7c8054b3 100644
--- a/Content.Shared/NameModifier/EntitySystems/NameModifierSystem.cs
+++ b/Content.Shared/NameModifier/EntitySystems/NameModifierSystem.cs
@@ -5,7 +5,7 @@
namespace Content.Shared.NameModifier.EntitySystems;
///
-public sealed partial class NameModifierSystem : EntitySystem
+public sealed class NameModifierSystem : EntitySystem
{
[Dependency] private readonly MetaDataSystem _metaData = default!;
@@ -16,10 +16,10 @@ public override void Initialize()
SubscribeLocalEvent(OnEntityRenamed);
}
- private void OnEntityRenamed(Entity entity, ref EntityRenamedEvent args)
+ private void OnEntityRenamed(Entity ent, ref EntityRenamedEvent args)
{
- SetBaseName((entity, entity.Comp), args.NewName);
- RefreshNameModifiers((entity, entity.Comp));
+ SetBaseName(ent, args.NewName);
+ RefreshNameModifiers((ent.Owner, ent.Comp));
}
private void SetBaseName(Entity entity, string name)
diff --git a/Content.Shared/Nutrition/Prototypes/FoodSequenceElementPrototype.cs b/Content.Shared/Nutrition/Prototypes/FoodSequenceElementPrototype.cs
index a3448715e4..931d8a3532 100644
--- a/Content.Shared/Nutrition/Prototypes/FoodSequenceElementPrototype.cs
+++ b/Content.Shared/Nutrition/Prototypes/FoodSequenceElementPrototype.cs
@@ -1,6 +1,7 @@
using Content.Shared.Tag;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
+using System.Numerics;
namespace Content.Shared.Nutrition.Prototypes;
@@ -18,6 +19,12 @@ public sealed partial class FoodSequenceElementPrototype : IPrototype
[DataField]
public List Sprites { get; private set; } = new();
+ ///
+ /// Relative size of the sprite displayed in the food sequence.
+ ///
+ [DataField]
+ public Vector2 Scale { get; private set; } = Vector2.One;
+
///
/// A localized name piece to build into the item name generator.
///
@@ -34,5 +41,5 @@ public sealed partial class FoodSequenceElementPrototype : IPrototype
/// Tag list of this layer. Used for recipes for food metamorphosis.
///
[DataField]
- public List> Tags { get; set; } = new();
+ public List> Tags { get; set; } = new();
}
diff --git a/Content.Shared/PDA/PdaComponent.cs b/Content.Shared/PDA/PdaComponent.cs
index d4cfc4fc0d..6aeb245e27 100644
--- a/Content.Shared/PDA/PdaComponent.cs
+++ b/Content.Shared/PDA/PdaComponent.cs
@@ -37,6 +37,10 @@ public sealed partial class PdaComponent : Component
[ViewVariables] public bool FlashlightOn;
[ViewVariables(VVAccess.ReadWrite)] public string? OwnerName;
+ // The Entity that "owns" the PDA, usually a player's character.
+ // This is useful when we are doing stuff like renaming a player and want to find their PDA to change the name
+ // as well.
+ [ViewVariables(VVAccess.ReadWrite)] public EntityUid? PdaOwner;
[ViewVariables] public string? StationName;
[ViewVariables] public string? StationAlertLevel;
[ViewVariables] public Color StationAlertColor = Color.White;
diff --git a/Content.Shared/Power/Generator/FuelGeneratorComponent.cs b/Content.Shared/Power/Generator/FuelGeneratorComponent.cs
index cdf97fb085..1cdb22a109 100644
--- a/Content.Shared/Power/Generator/FuelGeneratorComponent.cs
+++ b/Content.Shared/Power/Generator/FuelGeneratorComponent.cs
@@ -1,4 +1,5 @@
-using Robust.Shared.GameStates;
+using Content.Shared.Guidebook;
+using Robust.Shared.GameStates;
namespace Content.Shared.Power.Generator;
@@ -17,19 +18,20 @@ public sealed partial class FuelGeneratorComponent : Component
///
/// Is the generator currently running?
///
- [DataField("on"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ [DataField, AutoNetworkedField]
public bool On;
///
/// The generator's target power.
///
- [DataField("targetPower"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public float TargetPower = 15_000.0f;
///
/// The maximum target power.
///
- [DataField("maxTargetPower"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ [GuidebookData]
public float MaxTargetPower = 30_000.0f;
///
@@ -38,24 +40,24 @@ public sealed partial class FuelGeneratorComponent : Component
///