diff --git a/.github/workflows/update-credits.yml b/.github/workflows/update-credits.yml
index e35d4def7d..87babfa2a6 100644
--- a/.github/workflows/update-credits.yml
+++ b/.github/workflows/update-credits.yml
@@ -19,6 +19,8 @@ jobs:
- name: Get this week's Contributors
shell: pwsh
+ env:
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: Tools/dump_github_contributors.ps1 > Resources/Credits/GitHub.txt
# TODO
diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs
index a941f0acff..1c1f1984de 100644
--- a/Content.Client/Examine/ExamineSystem.cs
+++ b/Content.Client/Examine/ExamineSystem.cs
@@ -1,7 +1,12 @@
+using System.Linq;
+using System.Numerics;
+using System.Threading;
using Content.Client.Verbs;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Input;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Item;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@@ -12,13 +17,8 @@ using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Utility;
-using System.Linq;
-using System.Numerics;
-using System.Threading;
using static Content.Shared.Interaction.SharedInteractionSystem;
using static Robust.Client.UserInterface.Controls.BoxContainer;
-using Content.Shared.Interaction.Events;
-using Content.Shared.Item;
using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Examine
@@ -35,7 +35,6 @@ namespace Content.Client.Examine
private EntityUid _examinedEntity;
private EntityUid _lastExaminedEntity;
- private EntityUid _playerEntity;
private Popup? _examineTooltipOpen;
private ScreenCoordinates _popupPos;
private CancellationTokenSource? _requestCancelTokenSource;
@@ -74,9 +73,9 @@ namespace Content.Client.Examine
public override void Update(float frameTime)
{
if (_examineTooltipOpen is not {Visible: true}) return;
- if (!_examinedEntity.Valid || !_playerEntity.Valid) return;
+ if (!_examinedEntity.Valid || _playerManager.LocalEntity is not { } player) return;
- if (!CanExamine(_playerEntity, _examinedEntity))
+ if (!CanExamine(player, _examinedEntity))
CloseTooltip();
}
@@ -114,9 +113,8 @@ namespace Content.Client.Examine
return false;
}
- _playerEntity = _playerManager.LocalEntity ?? default;
-
- if (_playerEntity == default || !CanExamine(_playerEntity, entity))
+ if (_playerManager.LocalEntity is not { } player ||
+ !CanExamine(player, entity))
{
return false;
}
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/MassMedia/Ui/ArticleEditorPanel.xaml b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml
index 2b600845ca..f4fb9da062 100644
--- a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml
+++ b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml
@@ -47,8 +47,10 @@
+
+ StyleClasses="OpenBoth" Text="{Loc news-write-ui-preview-text}"/>
diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
index 5e068f1e9c..90a66bec7f 100644
--- a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
+++ b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
@@ -14,6 +14,7 @@ namespace Content.Client.MassMedia.Ui;
public sealed partial class ArticleEditorPanel : Control
{
public event Action? PublishButtonPressed;
+ public event Action? ArticleDraftUpdated;
private bool _preview;
@@ -45,6 +46,7 @@ public sealed partial class ArticleEditorPanel : Control
ButtonPreview.OnPressed += OnPreview;
ButtonCancel.OnPressed += OnCancel;
ButtonPublish.OnPressed += OnPublish;
+ ButtonSaveDraft.OnPressed += OnDraftSaved;
TitleField.OnTextChanged += args => OnTextChanged(args.Text.Length, args.Control, SharedNewsSystem.MaxTitleLength);
ContentField.OnTextChanged += args => OnTextChanged(Rope.CalcTotalLength(args.TextRope), args.Control, SharedNewsSystem.MaxContentLength);
@@ -68,6 +70,9 @@ public sealed partial class ArticleEditorPanel : Control
ButtonPublish.Disabled = false;
ButtonPreview.Disabled = false;
}
+
+ // save draft regardless; they can edit down the length later
+ ArticleDraftUpdated?.Invoke(TitleField.Text, Rope.Collapse(ContentField.TextRope));
}
private void OnPreview(BaseButton.ButtonEventArgs eventArgs)
@@ -92,6 +97,12 @@ public sealed partial class ArticleEditorPanel : Control
Visible = false;
}
+ private void OnDraftSaved(BaseButton.ButtonEventArgs eventArgs)
+ {
+ ArticleDraftUpdated?.Invoke(TitleField.Text, Rope.Collapse(ContentField.TextRope));
+ Visible = false;
+ }
+
private void Reset()
{
_preview = false;
@@ -100,6 +111,7 @@ public sealed partial class ArticleEditorPanel : Control
PreviewLabel.SetMarkup("");
TitleField.Text = "";
ContentField.TextRope = Rope.Leaf.Empty;
+ ArticleDraftUpdated?.Invoke(string.Empty, string.Empty);
}
protected override void Dispose(bool disposing)
diff --git a/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs b/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs
index 22e5bc452a..4f21361990 100644
--- a/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs
+++ b/Content.Client/MassMedia/Ui/NewsWriterBoundUserInterface.cs
@@ -25,6 +25,9 @@ public sealed class NewsWriterBoundUserInterface : BoundUserInterface
_menu.ArticleEditorPanel.PublishButtonPressed += OnPublishButtonPressed;
_menu.DeleteButtonPressed += OnDeleteButtonPressed;
+ _menu.CreateButtonPressed += OnCreateButtonPressed;
+ _menu.ArticleEditorPanel.ArticleDraftUpdated += OnArticleDraftUpdated;
+
SendMessage(new NewsWriterArticlesRequestMessage());
}
@@ -34,7 +37,7 @@ public sealed class NewsWriterBoundUserInterface : BoundUserInterface
if (state is not NewsWriterBoundUserInterfaceState cast)
return;
- _menu?.UpdateUI(cast.Articles, cast.PublishEnabled, cast.NextPublish);
+ _menu?.UpdateUI(cast.Articles, cast.PublishEnabled, cast.NextPublish, cast.DraftTitle, cast.DraftContent);
}
private void OnPublishButtonPressed()
@@ -67,4 +70,14 @@ public sealed class NewsWriterBoundUserInterface : BoundUserInterface
SendMessage(new NewsWriterDeleteMessage(articleNum));
}
+
+ private void OnCreateButtonPressed()
+ {
+ SendMessage(new NewsWriterRequestDraftMessage());
+ }
+
+ private void OnArticleDraftUpdated(string title, string content)
+ {
+ SendMessage(new NewsWriterSaveDraftMessage(title, content));
+ }
}
diff --git a/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs b/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs
index c059ce785a..af1f9a9441 100644
--- a/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs
+++ b/Content.Client/MassMedia/Ui/NewsWriterMenu.xaml.cs
@@ -4,6 +4,7 @@ using Robust.Client.UserInterface.XAML;
using Content.Shared.MassMedia.Systems;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Client.MassMedia.Ui;
@@ -16,6 +17,8 @@ public sealed partial class NewsWriterMenu : FancyWindow
public event Action? DeleteButtonPressed;
+ public event Action? CreateButtonPressed;
+
public NewsWriterMenu()
{
RobustXamlLoader.Load(this);
@@ -31,7 +34,7 @@ public sealed partial class NewsWriterMenu : FancyWindow
ButtonCreate.OnPressed += OnCreate;
}
- public void UpdateUI(NewsArticle[] articles, bool publishEnabled, TimeSpan nextPublish)
+ public void UpdateUI(NewsArticle[] articles, bool publishEnabled, TimeSpan nextPublish, string draftTitle, string draftContent)
{
ArticlesContainer.Children.Clear();
ArticleCount.Text = Loc.GetString("news-write-ui-article-count-text", ("count", articles.Length));
@@ -54,6 +57,9 @@ public sealed partial class NewsWriterMenu : FancyWindow
ButtonCreate.Disabled = !publishEnabled;
_nextPublish = nextPublish;
+
+ ArticleEditorPanel.TitleField.Text = draftTitle;
+ ArticleEditorPanel.ContentField.TextRope = new Rope.Leaf(draftContent);
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -93,5 +99,6 @@ public sealed partial class NewsWriterMenu : FancyWindow
private void OnCreate(BaseButton.ButtonEventArgs buttonEventArgs)
{
ArticleEditorPanel.Visible = true;
+ CreateButtonPressed?.Invoke();
}
}
diff --git a/Content.Client/Nuke/NukeMenu.xaml.cs b/Content.Client/Nuke/NukeMenu.xaml.cs
index b498d0e3bb..aa75758473 100644
--- a/Content.Client/Nuke/NukeMenu.xaml.cs
+++ b/Content.Client/Nuke/NukeMenu.xaml.cs
@@ -107,7 +107,7 @@ namespace Content.Client.Nuke
FirstStatusLabel.Text = firstMsg;
SecondStatusLabel.Text = secondMsg;
- EjectButton.Disabled = !state.DiskInserted || state.Status == NukeStatus.ARMED;
+ EjectButton.Disabled = !state.DiskInserted || state.Status == NukeStatus.ARMED || !state.IsAnchored;
AnchorButton.Disabled = state.Status == NukeStatus.ARMED;
AnchorButton.Pressed = state.IsAnchored;
ArmButton.Disabled = !state.AllowArm || !state.IsAnchored;
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.Containers.ItemSlots;
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 @@ namespace Content.Client.PDA
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.Physics.Components;
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!;
+
+ public override void Initialize()
{
- [Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly IPlayerManager _playerManager = default!;
+ base.Initialize();
+ SubscribeLocalEvent(OnRelayPlayerAttached);
+ SubscribeLocalEvent(OnRelayPlayerDetached);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
- public override void Initialize()
+ 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 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.
+
+ 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?
+ }
+
+ 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 OnPlayerAttached(Entity entity, ref LocalPlayerAttachedEvent 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);
+
+ if (_playerManager.LocalEntity is not {Valid: true} player)
+ return;
+
+ if (RelayQuery.TryGetComponent(player, out var relayMover))
+ HandleClientsideMovement(relayMover.RelayEntity, frameTime);
+
+ HandleClientsideMovement(player, frameTime);
+ }
+
+ private void HandleClientsideMovement(EntityUid player, float frameTime)
+ {
+ if (!MoverQuery.TryGetComponent(player, out var mover) ||
+ !XformQuery.TryGetComponent(player, out var xform))
{
- base.Initialize();
- SubscribeLocalEvent(OnRelayPlayerAttached);
- SubscribeLocalEvent(OnRelayPlayerDetached);
- SubscribeLocalEvent(OnPlayerAttached);
- SubscribeLocalEvent(OnPlayerDetached);
-
- SubscribeLocalEvent(OnUpdatePredicted);
- SubscribeLocalEvent(OnUpdateRelayTargetPredicted);
- SubscribeLocalEvent(OnUpdatePullablePredicted);
+ return;
}
- private void OnUpdatePredicted(Entity entity, ref UpdateIsPredictedEvent args)
+ var physicsUid = player;
+ PhysicsComponent? body;
+ var xformMover = xform;
+
+ if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
{
- // 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 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;
-
- // 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 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 OnPlayerDetached(Entity entity, ref LocalPlayerDetachedEvent args)
- {
- SetMoveInput(entity, MoveButtons.None);
- }
-
- public override void UpdateBeforeSolve(bool prediction, float frameTime)
- {
- base.UpdateBeforeSolve(prediction, frameTime);
-
- if (_playerManager.LocalEntity is not {Valid: true} player)
- return;
-
- if (RelayQuery.TryGetComponent(player, out var relayMover))
- HandleClientsideMovement(relayMover.RelayEntity, frameTime);
-
- HandleClientsideMovement(player, frameTime);
- }
-
- private void HandleClientsideMovement(EntityUid player, float frameTime)
- {
- if (!MoverQuery.TryGetComponent(player, out var mover) ||
- !XformQuery.TryGetComponent(player, out var xform))
+ if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
+ !XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
{
return;
}
- var physicsUid = player;
- PhysicsComponent? body;
- var xformMover = xform;
-
- if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
- {
- if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
- !XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
- {
- return;
- }
-
- physicsUid = xform.ParentUid;
- }
- else if (!PhysicsQuery.TryGetComponent(player, out body))
- {
- 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/APC/ApcBoundUserInterface.cs b/Content.Client/Power/APC/ApcBoundUserInterface.cs
index 759a5949ba..a790c5d984 100644
--- a/Content.Client/Power/APC/ApcBoundUserInterface.cs
+++ b/Content.Client/Power/APC/ApcBoundUserInterface.cs
@@ -19,8 +19,8 @@ namespace Content.Client.Power.APC
protected override void Open()
{
base.Open();
-
_menu = this.CreateWindow();
+ _menu.SetEntity(Owner);
_menu.OnBreaker += BreakerPressed;
}
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 @@ public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequir
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.Replays.Playback;
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 @@ public sealed class ContentReplayPlaybackManager
return;
}
- ReturnToDefaultState();
+ if (_client.RunLevel == ClientRunLevel.SinglePlayerGame)
+ _client.StopSinglePlayer();
- // Show a popup window with the error message
- var text = Loc.GetString("replay-loading-failed", ("reason", exception));
- var box = new BoxContainer
+ Action? retryAction = null;
+ Action? cancelAction = null;
+
+ if (!_cfg.GetCVar(CVars.ReplayIgnoreErrors) && LastLoad is { } last)
{
- Orientation = BoxContainer.LayoutOrientation.Vertical,
- Children = {new Label {Text = text}}
- };
-
- var popup = new DefaultWindow { Title = "Error!" };
- popup.Contents.AddChild(box);
-
- // Add button for attempting to re-load the replay while ignoring some errors.
- 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 @@ public sealed class ContentReplayPlaybackManager
_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/VendingMachines/UI/VendingMachineItem.xaml b/Content.Client/VendingMachines/UI/VendingMachineItem.xaml
index a665b72c73..a61edfb79b 100644
--- a/Content.Client/VendingMachines/UI/VendingMachineItem.xaml
+++ b/Content.Client/VendingMachines/UI/VendingMachineItem.xaml
@@ -4,7 +4,7 @@
SeparationOverride="4">
-
+
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.Examine;
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 @@ namespace Content.Client.Verbs
{
[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 @@ namespace Content.Client.Verbs
///
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 @@ namespace Content.Client.Verbs
public Action? OnVerbsResponse;
+ private List _entities = new();
+
public override void Initialize()
{
base.Initialize();
@@ -76,49 +79,50 @@ namespace Content.Client.Verbs
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 @@ namespace Content.Client.Verbs
{
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 @@ public sealed class ResearchTest
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 sealed class AdminSystem : EntitySystem
SubscribeLocalEvent(OnRoleEvent);
SubscribeLocalEvent(OnRoleEvent);
SubscribeLocalEvent(OnRoundRestartCleanup);
+ SubscribeLocalEvent(OnPlayerRenamed);
}
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
@@ -124,6 +125,11 @@ public sealed class AdminSystem : EntitySystem
}
}
+ 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/Anomaly/Effects/TechAnomalySystem.cs b/Content.Server/Anomaly/Effects/TechAnomalySystem.cs
index 63174930df..1b2849f1d7 100644
--- a/Content.Server/Anomaly/Effects/TechAnomalySystem.cs
+++ b/Content.Server/Anomaly/Effects/TechAnomalySystem.cs
@@ -35,9 +35,9 @@ public sealed class TechAnomalySystem : EntitySystem
while (query.MoveNext(out var uid, out var tech, out var anom))
{
if (_timing.CurTime < tech.NextTimer)
- return;
+ continue;
- tech.NextTimer = _timing.CurTime + TimeSpan.FromSeconds(tech.TimerFrequency * anom.Stability);
+ tech.NextTimer += TimeSpan.FromSeconds(tech.TimerFrequency * anom.Stability);
_signal.InvokePort(uid, tech.TimerPort);
}
@@ -61,7 +61,7 @@ public sealed class TechAnomalySystem : EntitySystem
var devices = _lookup.GetEntitiesInRange(Transform(tech).Coordinates, range);
if (devices.Count < 1)
return;
-
+
for (var i = 0; i < count; i++)
{
var device = _random.Pick(devices);
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 sealed class AddMapAtmosCommand : LocalizedCommands
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/EntitySystems/GasTankSystem.cs b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
index 8c5cded0f1..4d409a7082 100644
--- a/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/GasTankSystem.cs
@@ -17,6 +17,8 @@ using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Random;
+using Robust.Shared.Configuration;
+using Content.Shared.CCVar;
namespace Content.Server.Atmos.EntitySystems
{
@@ -32,10 +34,12 @@ namespace Content.Server.Atmos.EntitySystems
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
private const float TimerDelay = 0.5f;
private float _timer = 0f;
private const float MinimumSoundValvePressure = 10.0f;
+ private float _maxExplosionRange;
public override void Initialize()
{
@@ -51,6 +55,12 @@ namespace Content.Server.Atmos.EntitySystems
SubscribeLocalEvent(OnAnalyzed);
SubscribeLocalEvent(OnGasTankPrice);
SubscribeLocalEvent>(OnGetAlternativeVerb);
+ Subs.CVar(_cfg, CCVars.AtmosTankFragment, UpdateMaxRange, true);
+ }
+
+ private void UpdateMaxRange(float value)
+ {
+ _maxExplosionRange = value;
}
private void OnGasShutdown(Entity gasTank, ref ComponentShutdown args)
@@ -320,7 +330,7 @@ namespace Content.Server.Atmos.EntitySystems
var pressure = component.Air.Pressure;
- if (pressure > component.TankFragmentPressure)
+ if (pressure > component.TankFragmentPressure && _maxExplosionRange > 0)
{
// Give the gas a chance to build up more pressure.
for (var i = 0; i < 3; i++)
@@ -333,10 +343,7 @@ namespace Content.Server.Atmos.EntitySystems
// Let's cap the explosion, yeah?
// !1984
- if (range > GasTankComponent.MaxExplosionRange)
- {
- range = GasTankComponent.MaxExplosionRange;
- }
+ range = Math.Min(Math.Min(range, GasTankComponent.MaxExplosionRange), _maxExplosionRange);
_explosions.TriggerExplosive(owner, radius: range);
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.Administration.Logs;
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.Database;
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 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
SubscribeLocalEvent(OnPumpLeaveAtmosphere);
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnPumpActivate);
+ SubscribeLocalEvent(OnPowerChanged);
// Bound UI subscriptions
SubscribeLocalEvent(OnOutputPressureChangeMessage);
SubscribeLocalEvent(OnToggleStatusMessage);
@@ -63,9 +65,15 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
}
}
+ 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 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
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.Atmos.Piping.Components;
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.DeviceNetwork;
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 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
SubscribeLocalEvent(OnVolumePumpLeaveAtmosphere);
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnPumpActivate);
+ SubscribeLocalEvent(OnPowerChanged);
// Bound UI subscriptions
SubscribeLocalEvent(OnTransferRateChangeMessage);
SubscribeLocalEvent(OnToggleStatusMessage);
@@ -69,9 +71,15 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
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 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
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/Body/Components/BloodstreamComponent.cs b/Content.Server/Body/Components/BloodstreamComponent.cs
index a6d2afab21..a6c8ad90df 100644
--- a/Content.Server/Body/Components/BloodstreamComponent.cs
+++ b/Content.Server/Body/Components/BloodstreamComponent.cs
@@ -111,6 +111,13 @@ namespace Content.Server.Body.Components
[DataField]
public SoundSpecifier BloodHealedSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg");
+ ///
+ /// The minimum amount damage reduction needed to play the healing sound/popup.
+ /// This prevents tiny amounts of heat damage from spamming the sound, e.g. spacing.
+ ///
+ [DataField]
+ public float BloodHealedSoundThreshold = -0.1f;
+
// TODO probably damage bleed thresholds.
///
diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs
index 3da343aaa8..18790e7326 100644
--- a/Content.Server/Body/Systems/BloodstreamSystem.cs
+++ b/Content.Server/Body/Systems/BloodstreamSystem.cs
@@ -241,7 +241,7 @@ public sealed class BloodstreamSystem : EntitySystem
}
// Heat damage will cauterize, causing the bleed rate to be reduced.
- else if (totalFloat < 0 && oldBleedAmount > 0)
+ else if (totalFloat <= ent.Comp.BloodHealedSoundThreshold && oldBleedAmount > 0)
{
// Magically, this damage has healed some bleeding, likely
// because it's burn damage that cauterized their wounds.
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.Components;
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 partial class SeedData
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 partial class SeedData
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 sealed partial class BotanySystem
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.Chemistry.EntitySystems;
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 sealed partial class BotanySystem : EntitySystem
_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");
}
///
- /// 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!
+ /// For each random mutation, see if it occurs on this plant this check.
///
- public void MutateSeed(ref SeedData seed, float severity)
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// Checks all defined mutations against a seed to see which of them are applied.
+ ///
+ public void MutateSeed(EntityUid plantHolder, ref SeedData seed, float severity)
{
if (!seed.Unique)
{
@@ -37,57 +52,7 @@ public sealed class MutationSystem : EntitySystem
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 sealed class MutationSystem : EntitySystem
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 sealed class MutationSystem : EntitySystem
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 @@ public sealed class PlantHolderSystem : EntitySystem
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 @@ public sealed class PlantHolderSystem : EntitySystem
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 @@ public sealed class PlantHolderSystem : EntitySystem
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 sealed class PlantHolderSystem : EntitySystem
else
{
if (_random.Prob(0.8f))
- component.Age += (int) (1 * HydroponicsSpeedMultiplier);
+ component.Age += (int)(1 * HydroponicsSpeedMultiplier);
component.UpdateSpriteAfterUpdate = true;
}
@@ -632,12 +621,6 @@ public sealed class PlantHolderSystem : EntitySystem
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 sealed class PlantHolderSystem : EntitySystem
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 @@ public sealed class PlantHolderSystem : EntitySystem
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 sealed class PlantHolderSystem : EntitySystem
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 @@ public sealed class SeedExtractorSystem : EntitySystem
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 96d8ca86b5..a62b68e953 100644
--- a/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/InjectorSystem.cs
@@ -330,8 +330,23 @@ public sealed class InjectorSystem : SharedInjectorSystem
return false;
}
+ var applicableTargetSolution = targetSolution.Comp.Solution;
+ // If a whitelist exists, remove all non-whitelisted reagents from the target solution temporarily
+ var temporarilyRemovedSolution = new Solution();
+ if (injector.Comp.ReagentWhitelist is { } reagentWhitelist)
+ {
+ string[] reagentPrototypeWhitelistArray = new string[reagentWhitelist.Count];
+ var i = 0;
+ foreach (var reagent in reagentWhitelist)
+ {
+ reagentPrototypeWhitelistArray[i] = reagent;
+ ++i;
+ }
+ temporarilyRemovedSolution = applicableTargetSolution.SplitSolutionWithout(applicableTargetSolution.Volume, reagentPrototypeWhitelistArray);
+ }
+
// Get transfer amount. May be smaller than _transferAmount if not enough room, also make sure there's room in the injector
- var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, targetSolution.Comp.Solution.Volume,
+ var realTransferAmount = FixedPoint2.Min(injector.Comp.TransferAmount, applicableTargetSolution.Volume,
solution.AvailableVolume);
if (realTransferAmount <= 0)
@@ -353,6 +368,9 @@ public sealed class InjectorSystem : SharedInjectorSystem
// Move units from attackSolution to targetSolution
var removedSolution = SolutionContainers.Draw(target.Owner, targetSolution, realTransferAmount);
+ // Add back non-whitelisted reagents to the target solution
+ applicableTargetSolution.AddSolution(temporarilyRemovedSolution, null);
+
if (!SolutionContainers.TryAddSolution(soln.Value, removedSolution))
{
return false;
diff --git a/Content.Server/Doors/Systems/AirlockSystem.cs b/Content.Server/Doors/Systems/AirlockSystem.cs
index e9f1db13ff..ca3d133bd0 100644
--- a/Content.Server/Doors/Systems/AirlockSystem.cs
+++ b/Content.Server/Doors/Systems/AirlockSystem.cs
@@ -35,9 +35,10 @@ public sealed class AirlockSystem : SharedAirlockSystem
private void OnSignalReceived(EntityUid uid, AirlockComponent component, ref SignalReceivedEvent args)
{
- if (args.Port == component.AutoClosePort)
+ if (args.Port == component.AutoClosePort && component.AutoClose)
{
component.AutoClose = false;
+ Dirty(uid, component);
}
}
@@ -84,10 +85,11 @@ public sealed class AirlockSystem : SharedAirlockSystem
return;
}
- if (component.KeepOpenIfClicked)
+ if (component.KeepOpenIfClicked && component.AutoClose)
{
// Disable auto close
component.AutoClose = false;
+ Dirty(uid, component);
}
}
}
diff --git a/Content.Server/Dragon/Components/DragonRuleComponent.cs b/Content.Server/Dragon/Components/DragonRuleComponent.cs
deleted file mode 100644
index a87232bebc..0000000000
--- a/Content.Server/Dragon/Components/DragonRuleComponent.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.Dragon;
-
-[RegisterComponent]
-public sealed partial class DragonRuleComponent : Component
-{
-
-}
diff --git a/Content.Server/EntityEffects/Effects/ActivateArtifact.cs b/Content.Server/EntityEffects/Effects/ActivateArtifact.cs
index 3e97388499..8540478362 100644
--- a/Content.Server/EntityEffects/Effects/ActivateArtifact.cs
+++ b/Content.Server/EntityEffects/Effects/ActivateArtifact.cs
@@ -10,7 +10,7 @@ public sealed partial class ActivateArtifact : EntityEffect
public override void Effect(EntityEffectBaseArgs args)
{
var artifact = args.EntityManager.EntitySysManager.GetEntitySystem();
- artifact.TryActivateArtifact(args.TargetEntity);
+ artifact.TryActivateArtifact(args.TargetEntity, logMissing: false);
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) =>
diff --git a/Content.Server/EntityEffects/Effects/Glow.cs b/Content.Server/EntityEffects/Effects/Glow.cs
new file mode 100644
index 0000000000..9f03476729
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Glow.cs
@@ -0,0 +1,48 @@
+using Content.Shared.EntityEffects;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// 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 @@ namespace Content.Server.Forensics
[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 @@ namespace Content.Server.Forensics
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/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs
index f13ade3d1b..79896197b9 100644
--- a/Content.Server/GameTicking/GameTicker.Spawning.cs
+++ b/Content.Server/GameTicking/GameTicker.Spawning.cs
@@ -238,13 +238,29 @@ namespace Content.Server.GameTicking
if (lateJoin && !silent)
{
- _chatSystem.DispatchStationAnnouncement(station,
- Loc.GetString("latejoin-arrival-announcement",
- ("character", MetaData(mob).EntityName),
- ("gender", character.Gender), // CrystallPunk-LastnameGender
- ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
- Loc.GetString("latejoin-arrival-sender"),
- playDefaultSound: false);
+ if (jobPrototype.JoinNotifyCrew)
+ {
+ _chatSystem.DispatchStationAnnouncement(station,
+ Loc.GetString("latejoin-arrival-announcement-special",
+ ("character", MetaData(mob).EntityName),
+ ("gender", character.Gender), // CrystallPunk-LastnameGender
+ ("entity", mob),
+ ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
+ Loc.GetString("latejoin-arrival-sender"),
+ playDefaultSound: false,
+ colorOverride: Color.Gold);
+ }
+ else
+ {
+ _chatSystem.DispatchStationAnnouncement(station,
+ Loc.GetString("latejoin-arrival-announcement",
+ ("character", MetaData(mob).EntityName),
+ ("gender", character.Gender), // CrystallPunk-LastnameGender
+ ("entity", mob),
+ ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
+ Loc.GetString("latejoin-arrival-sender"),
+ playDefaultSound: false);
+ }
}
if (player.UserId == new Guid("{e887eb93-f503-4b65-95b6-2f282c014192}"))
diff --git a/Content.Server/GameTicking/Rules/Components/DragonRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/DragonRuleComponent.cs
new file mode 100644
index 0000000000..e3fab85f2d
--- /dev/null
+++ b/Content.Server/GameTicking/Rules/Components/DragonRuleComponent.cs
@@ -0,0 +1,4 @@
+namespace Content.Server.GameTicking.Rules.Components;
+
+[RegisterComponent]
+public sealed partial class DragonRuleComponent : Component;
diff --git a/Content.Server/GameTicking/Rules/DragonRuleSystem.cs b/Content.Server/GameTicking/Rules/DragonRuleSystem.cs
new file mode 100644
index 0000000000..96021e6123
--- /dev/null
+++ b/Content.Server/GameTicking/Rules/DragonRuleSystem.cs
@@ -0,0 +1,52 @@
+using Content.Server.Antag;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Station.Components;
+using Content.Server.Station.Systems;
+using Content.Shared.Localizations;
+using Robust.Server.GameObjects;
+
+namespace Content.Server.GameTicking.Rules;
+
+public sealed class DragonRuleSystem : GameRuleSystem
+{
+ [Dependency] private readonly TransformSystem _transform = default!;
+ [Dependency] private readonly AntagSelectionSystem _antag = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(AfterAntagEntitySelected);
+ }
+
+ private void AfterAntagEntitySelected(Entity ent, ref AfterAntagEntitySelectedEvent args)
+ {
+ _antag.SendBriefing(args.EntityUid, MakeBriefing(args.EntityUid), null, null);
+ }
+
+ private string MakeBriefing(EntityUid dragon)
+ {
+ var direction = string.Empty;
+
+ var dragonXform = Transform(dragon);
+
+ var station = _station.GetStationInMap(dragonXform.MapID);
+ EntityUid? stationGrid = null;
+ if (TryComp(station, out var stationData))
+ stationGrid = _station.GetLargestGrid(stationData);
+
+ if (stationGrid is not null)
+ {
+ var stationPosition = _transform.GetWorldPosition((EntityUid)stationGrid);
+ var dragonPosition = _transform.GetWorldPosition(dragon);
+
+ var vectorToStation = stationPosition - dragonPosition;
+ direction = ContentLocalizationManager.FormatDirection(vectorToStation.GetDir());
+ }
+
+ var briefing = Loc.GetString("dragon-role-briefing", ("direction", direction));
+
+ return briefing;
+ }
+}
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 sealed class GravityGeneratorSystem : EntitySystem
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 @@ public sealed class GravityGeneratorSystem : EntitySystem
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 @@ namespace Content.Server.Guardian
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 sealed class IdentitySystem : SharedIdentitySystem
SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid));
+ SubscribeLocalEvent((uid, _, _) => QueueIdentityUpdate(uid));
SubscribeLocalEvent(OnMapInit);
}
diff --git a/Content.Server/MassMedia/Components/NewsWriterComponent.cs b/Content.Server/MassMedia/Components/NewsWriterComponent.cs
index 7005600c10..fcd1250e8b 100644
--- a/Content.Server/MassMedia/Components/NewsWriterComponent.cs
+++ b/Content.Server/MassMedia/Components/NewsWriterComponent.cs
@@ -22,4 +22,16 @@ public sealed partial class NewsWriterComponent : Component
[DataField]
public SoundSpecifier ConfirmSound = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
+
+ ///
+ /// This stores the working title of the current article
+ ///
+ [DataField, ViewVariables]
+ public string DraftTitle = "";
+
+ ///
+ /// This stores the working content of the current article
+ ///
+ [DataField, ViewVariables]
+ public string DraftContent = "";
}
diff --git a/Content.Server/MassMedia/Systems/NewsSystem.cs b/Content.Server/MassMedia/Systems/NewsSystem.cs
index fd49ebf188..899e941993 100644
--- a/Content.Server/MassMedia/Systems/NewsSystem.cs
+++ b/Content.Server/MassMedia/Systems/NewsSystem.cs
@@ -51,6 +51,8 @@ public sealed class NewsSystem : SharedNewsSystem
subs.Event(OnWriteUiDeleteMessage);
subs.Event(OnRequestArticlesUiMessage);
subs.Event(OnWriteUiPublishMessage);
+ subs.Event(OnNewsWriterDraftUpdatedMessage);
+ subs.Event(OnRequestArticleDraftMessage);
});
// News reader
@@ -256,7 +258,7 @@ public sealed class NewsSystem : SharedNewsSystem
if (!TryGetArticles(ent, out var articles))
return;
- var state = new NewsWriterBoundUserInterfaceState(articles.ToArray(), ent.Comp.PublishEnabled, ent.Comp.NextPublish);
+ var state = new NewsWriterBoundUserInterfaceState(articles.ToArray(), ent.Comp.PublishEnabled, ent.Comp.NextPublish, ent.Comp.DraftTitle, ent.Comp.DraftContent);
_ui.SetUiState(ent.Owner, NewsWriterUiKey.Key, state);
}
@@ -318,4 +320,14 @@ public sealed class NewsSystem : SharedNewsSystem
return true;
}
+ private void OnNewsWriterDraftUpdatedMessage(Entity ent, ref NewsWriterSaveDraftMessage args)
+ {
+ ent.Comp.DraftTitle = args.DraftTitle;
+ ent.Comp.DraftContent = args.DraftContent;
+ }
+
+ private void OnRequestArticleDraftMessage(Entity ent, ref NewsWriterRequestDraftMessage msg)
+ {
+ UpdateWriterUi(ent);
+ }
}
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 @@ public sealed partial class MechSystem : SharedMechSystem
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 sealed class RenameCommand : IConsoleCommand
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 @@ public sealed class RenameCommand : IConsoleCommand
}
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/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs
index 109d790be1..38c1070a8a 100644
--- a/Content.Server/Nuke/NukeSystem.cs
+++ b/Content.Server/Nuke/NukeSystem.cs
@@ -167,12 +167,21 @@ public sealed class NukeSystem : EntitySystem
if (component.Status == NukeStatus.ARMED)
return;
+ // Nuke has to have the disk in it to be moved
+ if (!component.DiskSlot.HasItem)
+ {
+ var msg = Loc.GetString("nuke-component-cant-anchor-toggle");
+ _popups.PopupEntity(msg, uid, args.Actor, PopupType.MediumCaution);
+ return;
+ }
+
// manually set transform anchor (bypassing anchorable)
// todo: it will break pullable system
var xform = Transform(uid);
if (xform.Anchored)
{
_transform.Unanchor(uid, xform);
+ _itemSlots.SetLock(uid, component.DiskSlot, true);
}
else
{
@@ -194,6 +203,7 @@ public sealed class NukeSystem : EntitySystem
_transform.SetCoordinates(uid, xform, xform.Coordinates.SnapToGrid());
_transform.AnchorEntity(uid, xform);
+ _itemSlots.SetLock(uid, component.DiskSlot, false);
}
UpdateUserInterface(uid, component);
diff --git a/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs b/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs
index f7d8588716..ae8215ac6a 100644
--- a/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs
+++ b/Content.Server/Nutrition/EntitySystems/FoodSequenceSystem.cs
@@ -116,14 +116,8 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
return false;
//looking for a suitable FoodSequence prototype
- ProtoId elementProto = string.Empty;
- foreach (var pair in element.Comp.Entries)
- {
- if (pair.Key == start.Comp.Key)
- {
- elementProto = pair.Value;
- }
- }
+ if (!element.Comp.Entries.TryGetValue(start.Comp.Key, out var elementProto))
+ return false;
if (!_proto.TryIndex(elementProto, out var elementIndexed))
return false;
@@ -139,7 +133,7 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
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/Objectives/ObjectivesSystem.cs b/Content.Server/Objectives/ObjectivesSystem.cs
index 61df1414c0..73bb74d524 100644
--- a/Content.Server/Objectives/ObjectivesSystem.cs
+++ b/Content.Server/Objectives/ObjectivesSystem.cs
@@ -13,6 +13,7 @@ using System.Linq;
using System.Text;
using Content.Server.Objectives.Commands;
using Content.Shared.Prototypes;
+using Content.Shared.Roles.Jobs;
using Robust.Server.Player;
using Robust.Shared.Utility;
@@ -25,6 +26,7 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
+ [Dependency] private readonly SharedJobSystem _job = default!;
private IEnumerable? _objectives;
@@ -257,7 +259,12 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
_player.TryGetPlayerData(mind.Comp.OriginalOwnerUserId.Value, out var sessionData))
{
var username = sessionData.UserName;
- return Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
+
+ var nameWithJobMaybe = name;
+ if (_job.MindTryGetJobName(mind, out var jobName))
+ nameWithJobMaybe += ", " + jobName;
+
+ return Loc.GetString("objectives-player-user-named", ("user", username), ("name", nameWithJobMaybe));
}
return Loc.GetString("objectives-player-named", ("name", name));
diff --git a/Content.Server/Objectives/Systems/StealConditionSystem.cs b/Content.Server/Objectives/Systems/StealConditionSystem.cs
index 2c9244cf7d..be34a80fe3 100644
--- a/Content.Server/Objectives/Systems/StealConditionSystem.cs
+++ b/Content.Server/Objectives/Systems/StealConditionSystem.cs
@@ -72,14 +72,15 @@ public sealed class StealConditionSystem : EntitySystem
private void OnAfterAssign(Entity condition, ref ObjectiveAfterAssignEvent args)
{
var group = _proto.Index(condition.Comp.StealGroup);
+ string localizedName = Loc.GetString(group.Name);
var title =condition.Comp.OwnerText == null
- ? Loc.GetString(condition.Comp.ObjectiveNoOwnerText, ("itemName", group.Name))
- : Loc.GetString(condition.Comp.ObjectiveText, ("owner", Loc.GetString(condition.Comp.OwnerText)), ("itemName", group.Name));
+ ? Loc.GetString(condition.Comp.ObjectiveNoOwnerText, ("itemName", localizedName))
+ : Loc.GetString(condition.Comp.ObjectiveText, ("owner", Loc.GetString(condition.Comp.OwnerText)), ("itemName", localizedName));
var description = condition.Comp.CollectionSize > 1
- ? Loc.GetString(condition.Comp.DescriptionMultiplyText, ("itemName", group.Name), ("count", condition.Comp.CollectionSize))
- : Loc.GetString(condition.Comp.DescriptionText, ("itemName", group.Name));
+ ? Loc.GetString(condition.Comp.DescriptionMultiplyText, ("itemName", localizedName), ("count", condition.Comp.CollectionSize))
+ : Loc.GetString(condition.Comp.DescriptionText, ("itemName", localizedName));
_metaData.SetEntityName(condition.Owner, title, args.Meta);
_metaData.SetEntityDescription(condition.Owner, description, args.Meta);
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 @@ namespace Content.Server.PDA
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 @@ namespace Content.Server.PDA
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 @@ namespace Content.Server.PDA
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,536 +12,418 @@ using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent;
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()
+ 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 OnPlayerAttached(Entity entity, ref PlayerAttachedEvent args)
+ {
+ SetMoveInput(entity, MoveButtons.None);
+ }
+
+ private void OnPlayerDetached(Entity entity, ref PlayerDetachedEvent args)
+ {
+ SetMoveInput(entity, MoveButtons.None);
+ }
+
+ protected override bool CanSound()
+ {
+ return true;
+ }
+
+ public override void UpdateBeforeSolve(bool prediction, float frameTime)
+ {
+ base.UpdateBeforeSolve(prediction, frameTime);
+
+ var inputQueryEnumerator = AllEntityQuery();
+
+ while (inputQueryEnumerator.MoveNext(out var uid, out var mover))
{
- base.Initialize();
- SubscribeLocalEvent(OnRelayPlayerAttached);
- SubscribeLocalEvent(OnRelayPlayerDetached);
- SubscribeLocalEvent(OnPlayerAttached);
- SubscribeLocalEvent(OnPlayerDetached);
- }
+ var physicsUid = uid;
- private void OnRelayPlayerAttached(Entity entity, ref PlayerAttachedEvent args)
- {
- if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
- SetMoveInput((entity.Comp.RelayEntity, inputMover), MoveButtons.None);
- }
+ if (RelayQuery.HasComponent(uid))
+ continue;
- 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 OnPlayerAttached(Entity entity, ref PlayerAttachedEvent args)
- {
- SetMoveInput(entity, MoveButtons.None);
- }
-
- private void OnPlayerDetached(Entity entity, ref PlayerDetachedEvent args)
- {
- SetMoveInput(entity, MoveButtons.None);
- }
-
- protected override bool CanSound()
- {
- return true;
- }
-
- public override void UpdateBeforeSolve(bool prediction, float frameTime)
- {
- base.UpdateBeforeSolve(prediction, frameTime);
-
- var inputQueryEnumerator = AllEntityQuery();
-
- while (inputQueryEnumerator.MoveNext(out var uid, out var mover))
+ if (!XformQuery.TryGetComponent(uid, out var xform))
{
- var physicsUid = uid;
+ continue;
+ }
- if (RelayQuery.HasComponent(uid))
- continue;
+ PhysicsComponent? body;
+ var xformMover = xform;
- if (!XformQuery.TryGetComponent(uid, out var xform))
+ 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;
-
- if (mover.ToParent && RelayQuery.HasComponent(xform.ParentUid))
- {
- if (!PhysicsQuery.TryGetComponent(xform.ParentUid, out body) ||
- !XformQuery.TryGetComponent(xform.ParentUid, out xformMover))
- {
- continue;
- }
-
- physicsUid = xform.ParentUid;
- }
- else if (!PhysicsQuery.TryGetComponent(uid, out body))
- {
- continue;
- }
-
- HandleMobMovement(uid,
- mover,
- physicsUid,
- body,
- xformMover,
- frameTime);
+ physicsUid = xform.ParentUid;
+ }
+ else if (!PhysicsQuery.TryGetComponent(uid, out body))
+ {
+ continue;
}
- HandleShuttleMovement(frameTime);
+ HandleMobMovement(uid,
+ mover,
+ physicsUid,
+ body,
+ xformMover,
+ frameTime);
}
- public (Vector2 Strafe, float Rotation, float Brakes) GetPilotVelocityInput(PilotComponent component)
+ HandleShuttleMovement(frameTime);
+ }
+
+ public (Vector2 Strafe, float Rotation, float Brakes) GetPilotVelocityInput(PilotComponent component)
+ {
+ if (!Timing.InSimulation)
{
- 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);
- }
-
- 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;
- }
-
- ApplyTick(component, remainingFraction);
-
- // Logger.Info($"{curDir}{walk}{sprint}");
+ // 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);
-
- if (subTick >= pilot.LastInputSubTick)
- {
- var fraction = (subTick - pilot.LastInputSubTick) / (float) ushort.MaxValue;
-
- ApplyTick(pilot, fraction);
- pilot.LastInputSubTick = subTick;
- }
-
- var buttons = pilot.HeldButtons;
-
- if (state)
- {
- buttons |= button;
- }
- else
- {
- buttons &= ~button;
- }
-
- pilot.HeldButtons = buttons;
+ ApplyTick(pilot, fraction);
+ pilot.LastInputSubTick = subTick;
}
- private static void ApplyTick(PilotComponent component, float fraction)
+ var buttons = pilot.HeldButtons;
+
+ if (state)
{
- var x = 0;
- var y = 0;
- var rot = 0;
- int brake;
-
- if ((component.HeldButtons & ShuttleButtons.StrafeLeft) != 0x0)
- {
- x -= 1;
- }
-
- if ((component.HeldButtons & ShuttleButtons.StrafeRight) != 0x0)
- {
- x += 1;
- }
-
- component.CurTickStrafeMovement.X += x * fraction;
-
- if ((component.HeldButtons & ShuttleButtons.StrafeUp) != 0x0)
- {
- y += 1;
- }
-
- if ((component.HeldButtons & ShuttleButtons.StrafeDown) != 0x0)
- {
- y -= 1;
- }
-
- component.CurTickStrafeMovement.Y += y * fraction;
-
- if ((component.HeldButtons & ShuttleButtons.RotateLeft) != 0x0)
- {
- rot -= 1;
- }
-
- if ((component.HeldButtons & ShuttleButtons.RotateRight) != 0x0)
- {
- rot += 1;
- }
-
- component.CurTickRotationMovement += rot * fraction;
-
- if ((component.HeldButtons & ShuttleButtons.Brake) != 0x0)
- {
- brake = 1;
- }
- else
- {
- brake = 0;
- }
-
- component.CurTickBraking += brake * fraction;
+ buttons |= button;
+ }
+ else
+ {
+ buttons &= ~button;
}
- ///
- /// Helper function to extrapolate max velocity for a given Vector2 (really, its angle) and shuttle.
- ///
- private Vector2 ObtainMaxVel(Vector2 vel, ShuttleComponent shuttle)
+ pilot.HeldButtons = buttons;
+ }
+
+ private static void ApplyTick(PilotComponent component, float fraction)
+ {
+ var x = 0;
+ var y = 0;
+ var rot = 0;
+ int brake;
+
+ if ((component.HeldButtons & ShuttleButtons.StrafeLeft) != 0x0)
{
- 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);
+ x -= 1;
}
- private void HandleShuttleMovement(float frameTime)
+ if ((component.HeldButtons & ShuttleButtons.StrafeRight) != 0x0)
{
- var newPilots = new Dictionary)>();
+ x += 1;
+ }
- // 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))
+ component.CurTickStrafeMovement.X += x * fraction;
+
+ if ((component.HeldButtons & ShuttleButtons.StrafeUp) != 0x0)
+ {
+ y += 1;
+ }
+
+ if ((component.HeldButtons & ShuttleButtons.StrafeDown) != 0x0)
+ {
+ y -= 1;
+ }
+
+ component.CurTickStrafeMovement.Y += y * fraction;
+
+ if ((component.HeldButtons & ShuttleButtons.RotateLeft) != 0x0)
+ {
+ rot -= 1;
+ }
+
+ if ((component.HeldButtons & ShuttleButtons.RotateRight) != 0x0)
+ {
+ rot += 1;
+ }
+
+ component.CurTickRotationMovement += rot * fraction;
+
+ if ((component.HeldButtons & ShuttleButtons.Brake) != 0x0)
+ {
+ brake = 1;
+ }
+ else
+ {
+ brake = 0;
+ }
+
+ 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;
+
+ // TODO: This is terrible. Just make a new mover and also make it remote piloting + device networks
+ if (TryComp(consoleEnt, out var cargoConsole))
{
- var consoleEnt = pilot.Console;
-
- // TODO: This is terrible. Just make a new mover and also make it remote piloting + device networks
- if (TryComp(consoleEnt, out var cargoConsole))
- {
- consoleEnt = cargoConsole.Entity;
- }
-
- 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))
- {
- pilots = (shuttleComponent, new List<(EntityUid, PilotComponent, InputMoverComponent, TransformComponent)>());
- newPilots[gridId.Value] = pilots;
- }
-
- pilots.Item2.Add((uid, pilot, mover, xform));
+ consoleEnt = cargoConsole.Entity;
}
- // Reset inputs for non-piloted shuttles.
- foreach (var (shuttleUid, (shuttle, _)) in _shuttlePilots)
- {
- if (newPilots.ContainsKey(shuttleUid) || CanPilot(shuttleUid))
- continue;
+ if (!TryComp(consoleEnt, out TransformComponent? xform)) continue;
- _thruster.DisableLinearThrusters(shuttle);
+ 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))
+ {
+ pilots = (shuttleComponent, new List<(EntityUid, PilotComponent, InputMoverComponent, TransformComponent)>());
+ newPilots[gridId.Value] = pilots;
}
- _shuttlePilots = newPilots;
+ pilots.Item2.Add((uid, pilot, mover, xform));
+ }
- // 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)
+ // Reset inputs for non-piloted shuttles.
+ foreach (var (shuttleUid, (shuttle, _)) in _shuttlePilots)
+ {
+ if (newPilots.ContainsKey(shuttleUid) || CanPilot(shuttleUid))
+ continue;
+
+ _thruster.DisableLinearThrusters(shuttle);
+ }
+
+ _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)
+ {
+ 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 (Paused(shuttleUid) || CanPilot(shuttleUid) || !TryComp(shuttleUid, out var body))
- continue;
+ var (strafe, rotation, brakes) = GetPilotVelocityInput(pilot);
- 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 (brakes > 0f)
{
- var (strafe, rotation, brakes) = GetPilotVelocityInput(pilot);
-
- if (brakes > 0f)
- {
- brakeInput += brakes;
- }
-
- if (strafe.Length() > 0f)
- {
- var offsetRotation = consoleXform.LocalRotation;
- linearInput += offsetRotation.RotateVec(strafe);
- }
-
- if (rotation != 0f)
- {
- angularInput += rotation;
- }
+ brakeInput += brakes;
}
- var count = pilots.Count;
- linearInput /= count;
- angularInput /= count;
- brakeInput /= count;
-
- // Handle shuttle movement
- if (brakeInput > 0f)
+ if (strafe.Length() > 0f)
{
- if (body.LinearVelocity.Length() > 0f)
- {
- // Minimum brake velocity for a direction to show its thrust appearance.
- const float appearanceThreshold = 0.1f;
-
- // 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 < 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);
-
- if (shuttleVelocity.X > appearanceThreshold)
- _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.West);
-
- var index = (int) Math.Log2((int) DirectionFlag.West);
- force.X -= shuttle.LinearThrust[index];
- }
-
- if (shuttleVelocity.Y < 0f)
- {
- _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.South);
-
- if (shuttleVelocity.Y < -appearanceThreshold)
- _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.North);
-
- 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.South);
-
- var index = (int) Math.Log2((int) DirectionFlag.South);
- force.Y -= shuttle.LinearThrust[index];
- }
-
- var impulse = force * brakeInput * ShuttleComponent.BrakeCoefficient;
- impulse = shuttleNorthAngle.RotateVec(impulse);
- var forceMul = frameTime * body.InvMass;
- var maxVelocity = (-body.LinearVelocity).Length() / forceMul;
-
- // Don't overshoot
- if (impulse.Length() > maxVelocity)
- impulse = impulse.Normalized() * maxVelocity;
-
- PhysicsSystem.ApplyForce(shuttleUid, impulse, body: body);
- }
- else
- {
- _thruster.DisableLinearThrusters(shuttle);
- }
-
- 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
- {
- _thruster.SetAngularThrust(shuttle, false);
- }
+ var offsetRotation = consoleXform.LocalRotation;
+ linearInput += offsetRotation.RotateVec(strafe);
}
- if (linearInput.Length().Equals(0f))
+ if (rotation != 0f)
{
- PhysicsSystem.SetSleepingAllowed(shuttleUid, body, true);
-
- if (brakeInput.Equals(0f))
- _thruster.DisableLinearThrusters(shuttle);
+ angularInput += rotation;
}
- else
+ }
+
+ var count = pilots.Count;
+ linearInput /= count;
+ angularInput /= count;
+ brakeInput /= count;
+
+ // Handle shuttle movement
+ if (brakeInput > 0f)
+ {
+ if (body.LinearVelocity.Length() > 0f)
{
- PhysicsSystem.SetSleepingAllowed(shuttleUid, body, false);
- var angle = linearInput.ToWorldAngle();
- var linearDir = angle.GetDir();
- var dockFlag = linearDir.AsFlag();
- var totalForce = Vector2.Zero;
+ // Minimum brake velocity for a direction to show its thrust appearance.
+ const float appearanceThreshold = 0.1f;
- // Won't just do cardinal directions.
- foreach (DirectionFlag dir in Enum.GetValues(typeof(DirectionFlag)))
+ // 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 < 0f)
{
- // 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;
- }
+ _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.West);
- if ((dir & dockFlag) == 0x0)
- {
- _thruster.DisableLinearThrustDirection(shuttle, dir);
- continue;
- }
+ if (shuttleVelocity.X < -appearanceThreshold)
+ _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.East);
- var force = Vector2.Zero;
- var index = (int) Math.Log2((int) dir);
- var thrust = shuttle.LinearThrust[index];
+ var index = (int) Math.Log2((int) DirectionFlag.East);
+ force.X += shuttle.LinearThrust[index];
+ }
+ else if (shuttleVelocity.X > 0f)
+ {
+ _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.East);
- 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 (shuttleVelocity.X > appearanceThreshold)
+ _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.West);
- _thruster.EnableLinearThrustDirection(shuttle, dir);
- var impulse = force * linearInput.Length();
- totalForce += impulse;
+ var index = (int) Math.Log2((int) DirectionFlag.West);
+ force.X -= shuttle.LinearThrust[index];
}
+ if (shuttleVelocity.Y < 0f)
+ {
+ _thruster.DisableLinearThrustDirection(shuttle, DirectionFlag.South);
+
+ if (shuttleVelocity.Y < -appearanceThreshold)
+ _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.North);
+
+ 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.South);
+
+ var index = (int) Math.Log2((int) DirectionFlag.South);
+ force.Y -= shuttle.LinearThrust[index];
+ }
+
+ var impulse = force * brakeInput * ShuttleComponent.BrakeCoefficient;
+ impulse = shuttleNorthAngle.RotateVec(impulse);
var forceMul = frameTime * body.InvMass;
+ var maxVelocity = (-body.LinearVelocity).Length() / forceMul;
- 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;
+ // Don't overshoot
+ if (impulse.Length() > maxVelocity)
+ impulse = impulse.Normalized() * maxVelocity;
- var finalForce = Vector2Dot(totalForce, properAccel.Normalized()) * properAccel.Normalized();
-
- if (localVel.Length() >= maxVelocity.Length() && Vector2.Dot(totalForce, localVel) > 0f)
- finalForce = Vector2.Zero; // burn would be faster if used as such
-
- if (finalForce.Length() > properAccel.Length())
- finalForce = properAccel; // don't overshoot
-
- //Log.Info($"shuttle: maxVelocity {maxVelocity} totalForce {totalForce} finalForce {finalForce} forceMul {forceMul} properAccel {properAccel}");
-
- finalForce = shuttleNorthAngle.RotateVec(finalForce);
-
- if (finalForce.Length() > 0f)
- PhysicsSystem.ApplyForce(shuttleUid, finalForce, body: body);
- }
-
- if (MathHelper.CloseTo(angularInput, 0f))
- {
- PhysicsSystem.SetSleepingAllowed(shuttleUid, body, true);
-
- if (brakeInput <= 0f)
- _thruster.SetAngularThrust(shuttle, false);
+ PhysicsSystem.ApplyForce(shuttleUid, impulse, body: body);
}
else
{
- PhysicsSystem.SetSleepingAllowed(shuttleUid, body, false);
- var torque = shuttle.AngularThrust * -angularInput;
+ _thruster.DisableLinearThrusters(shuttle);
+ }
- // 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.
+ if (body.AngularVelocity != 0f)
+ {
+ var torque = shuttle.AngularThrust * brakeInput * (body.AngularVelocity > 0f ? -1f : 1f) * ShuttleComponent.BrakeCoefficient;
var torqueMul = body.InvI * frameTime;
- torque = Math.Clamp(torque,
- (-ShuttleComponent.MaxAngularVelocity - body.AngularVelocity) / torqueMul,
- (ShuttleComponent.MaxAngularVelocity - body.AngularVelocity) / torqueMul);
+ if (body.AngularVelocity > 0f)
+ {
+ torque = MathF.Max(-body.AngularVelocity / torqueMul, torque);
+ }
+ else
+ {
+ torque = MathF.Min(-body.AngularVelocity / torqueMul, torque);
+ }
if (!torque.Equals(0f))
{
@@ -549,23 +431,140 @@ namespace Content.Server.Physics.Controllers
_thruster.SetAngularThrust(shuttle, true);
}
}
+ else
+ {
+ _thruster.SetAngularThrust(shuttle, false);
+ }
+ }
+
+ 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)))
+ {
+ // 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);
+ 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;
+
+ 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 finalForce = Vector2Dot(totalForce, properAccel.Normalized()) * properAccel.Normalized();
+
+ if (localVel.Length() >= maxVelocity.Length() && Vector2.Dot(totalForce, localVel) > 0f)
+ finalForce = Vector2.Zero; // burn would be faster if used as such
+
+ if (finalForce.Length() > properAccel.Length())
+ finalForce = properAccel; // don't overshoot
+
+ //Log.Info($"shuttle: maxVelocity {maxVelocity} totalForce {totalForce} finalForce {finalForce} forceMul {forceMul} properAccel {properAccel}");
+
+ finalForce = shuttleNorthAngle.RotateVec(finalForce);
+
+ if (finalForce.Length() > 0f)
+ PhysicsSystem.ApplyForce(shuttleUid, finalForce, body: body);
+ }
+
+ 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;
+
+ // 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;
+
+ 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);
- }
-
}
+
+ // .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 @@ public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequir
return;
}
- if (TryComp(ent.Owner, out var panel) && panel.Open)
- return;
-
args.Cancel();
}
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenFill.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenFill.cs
index 5a0d77c615..77c615d378 100644
--- a/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenFill.cs
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.DunGenFill.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using System.Threading.Tasks;
+using Content.Shared.Maps;
using Content.Shared.Procedural;
using Content.Shared.Procedural.DungeonGenerators;
@@ -10,7 +11,7 @@ public sealed partial class DungeonJob
///
///
///
- private async Task GenerateFillDunGen(DungeonData data, HashSet reservedTiles)
+ private async Task GenerateFillDunGen(FillGridDunGen fill, DungeonData data, HashSet reservedTiles)
{
if (!data.Entities.TryGetValue(DungeonDataKey.Fill, out var fillEnt))
{
@@ -28,6 +29,9 @@ public sealed partial class DungeonJob
if (reservedTiles.Contains(tile))
continue;
+ if (fill.AllowedTiles != null && !fill.AllowedTiles.Contains(((ContentTileDefinition) _tileDefManager[tileRef.Value.Tile.TypeId]).ID))
+ continue;
+
if (!_anchorable.TileFree(_grid, tile, DungeonSystem.CollisionLayer, DungeonSystem.CollisionMask))
continue;
diff --git a/Content.Server/Procedural/DungeonJob/DungeonJob.cs b/Content.Server/Procedural/DungeonJob/DungeonJob.cs
index cdf1f00205..cdb5eb0805 100644
--- a/Content.Server/Procedural/DungeonJob/DungeonJob.cs
+++ b/Content.Server/Procedural/DungeonJob/DungeonJob.cs
@@ -230,7 +230,7 @@ public sealed partial class DungeonJob : Job>
dungeons.AddRange(await GenerateExteriorDungen(position, exterior, reservedTiles, random));
break;
case FillGridDunGen fill:
- dungeons.Add(await GenerateFillDunGen(data, reservedTiles));
+ dungeons.Add(await GenerateFillDunGen(fill, data, reservedTiles));
break;
case JunctionDunGen junc:
await PostGen(junc, data, dungeons[^1], reservedTiles, random);
diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs
index fa4f6db240..4cfceaf297 100644
--- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs
+++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs
@@ -170,7 +170,7 @@ public sealed partial class RevenantSystem : EntitySystem
}
}
- ChangeEssenceAmount(uid, abilityCost, component, false);
+ ChangeEssenceAmount(uid, -abilityCost, component, false);
_statusEffects.TryAddStatusEffect(uid, "Corporeal", TimeSpan.FromSeconds(debuffs.Y), false);
_stun.TryStun(uid, TimeSpan.FromSeconds(debuffs.X), false);
diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs
index 42783f163b..82bdb78816 100644
--- a/Content.Server/RoundEnd/RoundEndSystem.cs
+++ b/Content.Server/RoundEnd/RoundEndSystem.cs
@@ -194,7 +194,7 @@ namespace Content.Server.RoundEnd
ExpectedCountdownEnd = _gameTiming.CurTime + countdownTime;
// TODO full game saves
- Timer.Spawn(countdownTime, _shuttle.CallEmergencyShuttle, _countdownTokenSource.Token);
+ Timer.Spawn(countdownTime, _shuttle.DockEmergencyShuttle, _countdownTokenSource.Token);
ActivateCooldown();
RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
diff --git a/Content.Server/Shuttles/Commands/DockEmergencyShuttleCommand.cs b/Content.Server/Shuttles/Commands/DockEmergencyShuttleCommand.cs
index 8febe51f5a..f219602bcb 100644
--- a/Content.Server/Shuttles/Commands/DockEmergencyShuttleCommand.cs
+++ b/Content.Server/Shuttles/Commands/DockEmergencyShuttleCommand.cs
@@ -19,6 +19,6 @@ public sealed class DockEmergencyShuttleCommand : IConsoleCommand
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var system = _sysManager.GetEntitySystem();
- system.CallEmergencyShuttle();
+ system.DockEmergencyShuttle();
}
}
diff --git a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
index 9b94d6024e..46d2cd69b9 100644
--- a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
+++ b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs
@@ -22,6 +22,7 @@ using Content.Shared.DeviceNetwork;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Parallax.Biomes;
+using Content.Shared.Preferences;
using Content.Shared.Salvage;
using Content.Shared.Shuttles.Components;
using Content.Shared.Tiles;
@@ -91,7 +92,7 @@ public sealed class ArrivalsSystem : EntitySystem
{
base.Initialize();
- SubscribeLocalEvent(HandlePlayerSpawning, before: new []{ typeof(ContainerSpawnPointSystem), typeof(SpawnPointSystem)});
+ SubscribeLocalEvent(HandlePlayerSpawning, before: new []{ typeof(SpawnPointSystem)}, after: new [] { typeof(ContainerSpawnPointSystem)});
SubscribeLocalEvent(OnStationPostInit);
@@ -334,6 +335,8 @@ public sealed class ArrivalsSystem : EntitySystem
if (ev.SpawnResult != null)
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)
return;
diff --git a/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs b/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs
index 597d74dcc7..aabfaa31dd 100644
--- a/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs
+++ b/Content.Server/Shuttles/Systems/DockingSystem.Shuttle.cs
@@ -17,7 +17,7 @@ public sealed partial class DockingSystem
private const int DockRoundingDigits = 2;
- public Angle GetAngle(EntityUid uid, TransformComponent xform, EntityUid targetUid, TransformComponent targetXform, EntityQuery xformQuery)
+ public Angle GetAngle(EntityUid uid, TransformComponent xform, EntityUid targetUid, TransformComponent targetXform)
{
var (shuttlePos, shuttleRot) = _transform.GetWorldPositionRotation(xform);
var (targetPos, targetRot) = _transform.GetWorldPositionRotation(targetXform);
@@ -288,9 +288,7 @@ public sealed partial class DockingSystem
// Prioritise by priority docks, then by maximum connected ports, then by most similar angle.
validDockConfigs = validDockConfigs
- .OrderByDescending(x => x.Docks.Any(docks =>
- TryComp(docks.DockBUid, out var priority) &&
- priority.Tag?.Equals(priorityTag) == true))
+ .OrderByDescending(x => IsConfigPriority(x, priorityTag))
.ThenByDescending(x => x.Docks.Count)
.ThenBy(x => Math.Abs(Angle.ShortestDistance(x.Angle.Reduced(), targetGridAngle).Theta)).ToList();
@@ -301,6 +299,13 @@ public sealed partial class DockingSystem
return location;
}
+ public bool IsConfigPriority(DockingConfig config, string? priorityTag)
+ {
+ return config.Docks.Any(docks =>
+ TryComp(docks.DockBUid, out var priority)
+ && priority.Tag?.Equals(priorityTag) == true);
+ }
+
///
/// Checks whether the shuttle can warp to the specified position.
///
diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
index 4c13a2cc82..6c4bdc0814 100644
--- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
+++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using System.Numerics;
using System.Threading;
using Content.Server.Access.Systems;
@@ -255,18 +256,19 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
}
///
- /// Attempts to dock the emergency shuttle to the station.
+ /// Attempts to dock a station's emergency shuttle.
///
- public void CallEmergencyShuttle(EntityUid stationUid, StationEmergencyShuttleComponent? stationShuttle = null)
+ ///
+ public ShuttleDockResult? DockSingleEmergencyShuttle(EntityUid stationUid, StationEmergencyShuttleComponent? stationShuttle = null)
{
if (!Resolve(stationUid, ref stationShuttle))
- return;
+ return null;
if (!TryComp(stationShuttle.EmergencyShuttle, out TransformComponent? xform) ||
!TryComp(stationShuttle.EmergencyShuttle, out var shuttle))
{
Log.Error($"Attempted to call an emergency shuttle for an uninitialized station? Station: {ToPrettyString(stationUid)}. Shuttle: {ToPrettyString(stationShuttle.EmergencyShuttle)}");
- return;
+ return null;
}
var targetGrid = _station.GetLargestGrid(Comp(stationUid));
@@ -274,60 +276,126 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
// UHH GOOD LUCK
if (targetGrid == null)
{
- _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid)} unable to dock with station {ToPrettyString(stationUid)}");
- _chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-good-luck"), playDefaultSound: false);
+ _logger.Add(
+ LogType.EmergencyShuttle,
+ LogImpact.High,
+ $"Emergency shuttle {ToPrettyString(stationUid)} unable to dock with station {ToPrettyString(stationUid)}");
+
+ return new ShuttleDockResult
+ {
+ Station = (stationUid, stationShuttle),
+ ResultType = ShuttleDockResultType.GoodLuck,
+ };
+ }
+
+ ShuttleDockResultType resultType;
+ if (_shuttle.TryFTLDock(stationShuttle.EmergencyShuttle.Value, shuttle, targetGrid.Value, out var config, DockTag))
+ {
+ _logger.Add(
+ LogType.EmergencyShuttle,
+ LogImpact.High,
+ $"Emergency shuttle {ToPrettyString(stationUid)} docked with stations");
+
+ resultType = _dock.IsConfigPriority(config, DockTag)
+ ? ShuttleDockResultType.PriorityDock
+ : ShuttleDockResultType.OtherDock;
+ }
+ else
+ {
+ _logger.Add(
+ LogType.EmergencyShuttle,
+ LogImpact.High,
+ $"Emergency shuttle {ToPrettyString(stationUid)} unable to find a valid docking port for {ToPrettyString(stationUid)}");
+
+ resultType = ShuttleDockResultType.NoDock;
+ }
+
+ return new ShuttleDockResult
+ {
+ Station = (stationUid, stationShuttle),
+ DockingConfig = config,
+ ResultType = resultType,
+ TargetGrid = targetGrid,
+ };
+ }
+
+ ///
+ /// Do post-shuttle-dock setup. Announce to the crew and set up shuttle timers.
+ ///
+ public void AnnounceShuttleDock(ShuttleDockResult result, bool extended)
+ {
+ var shuttle = result.Station.Comp.EmergencyShuttle;
+
+ DebugTools.Assert(shuttle != null);
+
+ if (result.ResultType == ShuttleDockResultType.GoodLuck)
+ {
+ _chatSystem.DispatchStationAnnouncement(
+ result.Station,
+ Loc.GetString("emergency-shuttle-good-luck"),
+ playDefaultSound: false);
+
// TODO: Need filter extensions or something don't blame me.
_audio.PlayGlobal("/Audio/Misc/notice1.ogg", Filter.Broadcast(), true);
return;
}
- var xformQuery = GetEntityQuery();
+ DebugTools.Assert(result.TargetGrid != null);
- if (_shuttle.TryFTLDock(stationShuttle.EmergencyShuttle.Value, shuttle, targetGrid.Value, DockTag))
+ // Send station announcement.
+
+ var targetXform = Transform(result.TargetGrid.Value);
+ var angle = _dock.GetAngle(
+ shuttle.Value,
+ Transform(shuttle.Value),
+ result.TargetGrid.Value,
+ targetXform);
+
+ var direction = ContentLocalizationManager.FormatDirection(angle.GetDir());
+ var location = FormattedMessage.RemoveMarkupPermissive(
+ _navMap.GetNearestBeaconString((shuttle.Value, Transform(shuttle.Value))));
+
+ var extendedText = extended ? Loc.GetString("emergency-shuttle-extended") : "";
+ var locKey = result.ResultType == ShuttleDockResultType.NoDock
+ ? "emergency-shuttle-nearby"
+ : "emergency-shuttle-docked";
+
+ _chatSystem.DispatchStationAnnouncement(
+ result.Station,
+ Loc.GetString(
+ locKey,
+ ("time", $"{_consoleAccumulator:0}"),
+ ("direction", direction),
+ ("location", location),
+ ("extended", extendedText)),
+ playDefaultSound: false);
+
+ // Trigger shuttle timers on the shuttle.
+
+ var time = TimeSpan.FromSeconds(_consoleAccumulator);
+ if (TryComp(shuttle, out var netComp))
{
- if (TryComp(targetGrid.Value, out TransformComponent? targetXform))
+ var payload = new NetworkPayload
{
- var angle = _dock.GetAngle(stationShuttle.EmergencyShuttle.Value, xform, targetGrid.Value, targetXform, xformQuery);
- var direction = ContentLocalizationManager.FormatDirection(angle.GetDir());
- var location = FormattedMessage.RemoveMarkupPermissive(_navMap.GetNearestBeaconString((stationShuttle.EmergencyShuttle.Value, xform)));
- _chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-docked", ("time", $"{_consoleAccumulator:0}"), ("direction", direction), ("location", location)), playDefaultSound: false);
- }
-
- // shuttle timers
- var time = TimeSpan.FromSeconds(_consoleAccumulator);
- if (TryComp(stationShuttle.EmergencyShuttle.Value, out var netComp))
- {
- var payload = new NetworkPayload
- {
- [ShuttleTimerMasks.ShuttleMap] = stationShuttle.EmergencyShuttle.Value,
- [ShuttleTimerMasks.SourceMap] = targetXform?.MapUid,
- [ShuttleTimerMasks.DestMap] = _roundEnd.GetCentcomm(),
- [ShuttleTimerMasks.ShuttleTime] = time,
- [ShuttleTimerMasks.SourceTime] = time,
- [ShuttleTimerMasks.DestTime] = time + TimeSpan.FromSeconds(TransitTime),
- [ShuttleTimerMasks.Docked] = true
- };
- _deviceNetworkSystem.QueuePacket(stationShuttle.EmergencyShuttle.Value, null, payload, netComp.TransmitFrequency);
- }
-
- _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid)} docked with stations");
- // TODO: Need filter extensions or something don't blame me.
- _audio.PlayGlobal("/Audio/Announcements/shuttle_dock.ogg", Filter.Broadcast(), true);
+ [ShuttleTimerMasks.ShuttleMap] = shuttle,
+ [ShuttleTimerMasks.SourceMap] = targetXform.MapUid,
+ [ShuttleTimerMasks.DestMap] = _roundEnd.GetCentcomm(),
+ [ShuttleTimerMasks.ShuttleTime] = time,
+ [ShuttleTimerMasks.SourceTime] = time,
+ [ShuttleTimerMasks.DestTime] = time + TimeSpan.FromSeconds(TransitTime),
+ [ShuttleTimerMasks.Docked] = true,
+ };
+ _deviceNetworkSystem.QueuePacket(shuttle.Value, null, payload, netComp.TransmitFrequency);
}
- else
- {
- if (TryComp(targetGrid.Value, out var targetXform))
- {
- var angle = _dock.GetAngle(stationShuttle.EmergencyShuttle.Value, xform, targetGrid.Value, targetXform, xformQuery);
- var direction = ContentLocalizationManager.FormatDirection(angle.GetDir());
- var location = FormattedMessage.RemoveMarkupPermissive(_navMap.GetNearestBeaconString((stationShuttle.EmergencyShuttle.Value, xform)));
- _chatSystem.DispatchStationAnnouncement(stationUid, Loc.GetString("emergency-shuttle-nearby", ("time", $"{_consoleAccumulator:0}"), ("direction", direction), ("location", location)), playDefaultSound: false);
- }
- _logger.Add(LogType.EmergencyShuttle, LogImpact.High, $"Emergency shuttle {ToPrettyString(stationUid)} unable to find a valid docking port for {ToPrettyString(stationUid)}");
- // TODO: Need filter extensions or something don't blame me.
- _audio.PlayGlobal("/Audio/Misc/notice1.ogg", Filter.Broadcast(), true);
- }
+ // Play announcement audio.
+
+ var audioFile = result.ResultType == ShuttleDockResultType.NoDock
+ ? "/Audio/Misc/notice1.ogg"
+ : "/Audio/Announcements/shuttle_dock.ogg";
+
+ // TODO: Need filter extensions or something don't blame me.
+ _audio.PlayGlobal(audioFile, Filter.Broadcast(), true);
}
private void OnStationInit(EntityUid uid, StationCentcommComponent component, MapInitEvent args)
@@ -353,9 +421,12 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
}
///
- /// Spawns the emergency shuttle for each station and starts the countdown until controls unlock.
+ /// Teleports the emergency shuttle to its station and starts the countdown until it launches.
///
- public void CallEmergencyShuttle()
+ ///
+ /// If the emergency shuttle is disabled, this immediately ends the round.
+ ///
+ public void DockEmergencyShuttle()
{
if (EmergencyShuttleArrived)
return;
@@ -371,9 +442,34 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
var query = AllEntityQuery();
+ var dockResults = new List();
+
while (query.MoveNext(out var uid, out var comp))
{
- CallEmergencyShuttle(uid, comp);
+ if (DockSingleEmergencyShuttle(uid, comp) is { } dockResult)
+ dockResults.Add(dockResult);
+ }
+
+ // Make the shuttle wait longer if it couldn't dock in the normal spot.
+ // We have to handle the possibility of there being multiple stations, so since the shuttle timer is global,
+ // use the WORST value we have.
+ var worstResult = dockResults.Max(x => x.ResultType);
+ var multiplier = worstResult switch
+ {
+ ShuttleDockResultType.OtherDock => _configManager.GetCVar(
+ CCVars.EmergencyShuttleDockTimeMultiplierOtherDock),
+ ShuttleDockResultType.NoDock => _configManager.GetCVar(
+ CCVars.EmergencyShuttleDockTimeMultiplierNoDock),
+ // GoodLuck doesn't get a multiplier.
+ // Quite frankly at that point the round is probably so fucked that you'd rather it be over ASAP.
+ _ => 1,
+ };
+
+ _consoleAccumulator *= multiplier;
+
+ foreach (var shuttleDockResult in dockResults)
+ {
+ AnnounceShuttleDock(shuttleDockResult, multiplier > 1);
}
_commsConsole.UpdateCommsConsoleInterface();
@@ -579,4 +675,66 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
return _transformSystem.GetWorldMatrix(shuttleXform).TransformBox(grid.LocalAABB).Contains(_transformSystem.GetWorldPosition(xform));
}
+
+ ///
+ /// A result of a shuttle dock operation done by .
+ ///
+ ///
+ public sealed class ShuttleDockResult
+ {
+ ///
+ /// The station for which the emergency shuttle got docked.
+ ///
+ public Entity Station;
+
+ ///
+ /// The target grid of the station that the shuttle tried to dock to.
+ ///
+ ///
+ /// Not present if is .
+ ///
+ public EntityUid? TargetGrid;
+
+ ///
+ /// Enum code describing the dock result.
+ ///
+ public ShuttleDockResultType ResultType;
+
+ ///
+ /// The docking config used to actually dock to the station.
+ ///
+ ///
+ /// Only present if is
+ /// or .
+ ///
+ public DockingConfig? DockingConfig;
+ }
+
+ ///
+ /// Emergency shuttle dock result codes used by .
+ ///
+ public enum ShuttleDockResultType : byte
+ {
+ // This enum is ordered from "best" to "worst". This is used to sort the results.
+
+ ///
+ /// The shuttle was docked at a priority dock, which is the intended destination.
+ ///
+ PriorityDock,
+
+ ///
+ /// The shuttle docked at another dock on the station then the intended priority dock.
+ ///
+ OtherDock,
+
+ ///
+ /// The shuttle couldn't find any suitable dock on the station at all, it did not dock.
+ ///
+ NoDock,
+
+ ///
+ /// No station grid was found at all, shuttle did not get moved.
+ ///
+ GoodLuck,
+ }
}
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
index 62a328aa0f..fa55991a9f 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
@@ -72,11 +72,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