Merge pull request #441 from crystallpunk-14/ed-17-09-2024-upstream2
Ed 17 09 2024 upstream
This commit is contained in:
2
.github/workflows/update-credits.yml
vendored
2
.github/workflows/update-credits.yml
vendored
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
45
Content.Client/Guidebook/GuidebookDataSystem.cs
Normal file
45
Content.Client/Guidebook/GuidebookDataSystem.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Client.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// Client system for storing and retrieving values extracted from entity prototypes
|
||||
/// for display in the guidebook (<see cref="RichText.ProtodataTag"/>).
|
||||
/// Requests data from the server on <see cref="Initialize"/>.
|
||||
/// Can also be pushed new data when the server reloads prototypes.
|
||||
/// </summary>
|
||||
public sealed class GuidebookDataSystem : EntitySystem
|
||||
{
|
||||
private GuidebookData? _data;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<UpdateGuidebookDataEvent>(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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to retrieve a value using the given identifiers.
|
||||
/// See <see cref="GuidebookData.TryGetValue"/> for more information.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
49
Content.Client/Guidebook/Richtext/ProtodataTag.cs
Normal file
49
Content.Client/Guidebook/Richtext/ProtodataTag.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Globalization;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Guidebook.RichText;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="Shared.Guidebook.GuidebookDataAttribute"/>.
|
||||
/// </summary>
|
||||
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<GuidebookDataSystem>();
|
||||
|
||||
// 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";
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,10 @@
|
||||
</Control>
|
||||
<Control HorizontalExpand="True"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Name="ButtonSaveDraft" SetHeight="32" SetWidth="85"
|
||||
StyleClasses="OpenRight" Text="{Loc news-write-ui-save-text}"/>
|
||||
<Button Name="ButtonPreview" SetHeight="32" SetWidth="85"
|
||||
StyleClasses="OpenRight" Text="{Loc news-write-ui-preview-text}"/>
|
||||
StyleClasses="OpenBoth" Text="{Loc news-write-ui-preview-text}"/>
|
||||
<Button Name="ButtonPublish" SetHeight="32" SetWidth="85" Text="{Loc news-write-ui-publish-text}" Access="Public"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Content.Client.MassMedia.Ui;
|
||||
public sealed partial class ArticleEditorPanel : Control
|
||||
{
|
||||
public event Action? PublishButtonPressed;
|
||||
public event Action<string, string>? 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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<int>? 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<PdaSystem>();
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -67,14 +67,17 @@
|
||||
Description="{Loc 'comp-pda-ui-ringtone-button-description'}"/>
|
||||
<pda:PdaSettingsButton Name="ActivateMusicButton"
|
||||
Access="Public"
|
||||
Visible="False"
|
||||
Text="{Loc 'pda-bound-user-interface-music-button'}"
|
||||
Description="{Loc 'pda-bound-user-interface-music-button-description'}"/>
|
||||
<pda:PdaSettingsButton Name="ShowUplinkButton"
|
||||
Access="Public"
|
||||
Visible="False"
|
||||
Text="{Loc 'pda-bound-user-interface-show-uplink-title'}"
|
||||
Description="{Loc 'pda-bound-user-interface-show-uplink-description'}"/>
|
||||
<pda:PdaSettingsButton Name="LockUplinkButton"
|
||||
Access="Public"
|
||||
Visible="False"
|
||||
Text="{Loc 'pda-bound-user-interface-lock-uplink-title'}"
|
||||
Description="{Loc 'pda-bound-user-interface-lock-uplink-description'}"/>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -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<RelayInputMoverComponent, LocalPlayerAttachedEvent>(OnRelayPlayerAttached);
|
||||
SubscribeLocalEvent<RelayInputMoverComponent, LocalPlayerDetachedEvent>(OnRelayPlayerDetached);
|
||||
SubscribeLocalEvent<InputMoverComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<InputMoverComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
|
||||
|
||||
public override void Initialize()
|
||||
SubscribeLocalEvent<InputMoverComponent, UpdateIsPredictedEvent>(OnUpdatePredicted);
|
||||
SubscribeLocalEvent<MovementRelayTargetComponent, UpdateIsPredictedEvent>(OnUpdateRelayTargetPredicted);
|
||||
SubscribeLocalEvent<PullableComponent, UpdateIsPredictedEvent>(OnUpdatePullablePredicted);
|
||||
}
|
||||
|
||||
private void OnUpdatePredicted(Entity<InputMoverComponent> 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<MovementRelayTargetComponent> entity, ref UpdateIsPredictedEvent args)
|
||||
{
|
||||
if (entity.Comp.Source == _playerManager.LocalEntity)
|
||||
args.IsPredicted = true;
|
||||
}
|
||||
|
||||
private void OnUpdatePullablePredicted(Entity<PullableComponent> 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<RelayInputMoverComponent> 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<RelayInputMoverComponent> 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<InputMoverComponent> entity, ref LocalPlayerAttachedEvent args)
|
||||
{
|
||||
SetMoveInput(entity, MoveButtons.None);
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(Entity<InputMoverComponent> 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<RelayInputMoverComponent, LocalPlayerAttachedEvent>(OnRelayPlayerAttached);
|
||||
SubscribeLocalEvent<RelayInputMoverComponent, LocalPlayerDetachedEvent>(OnRelayPlayerDetached);
|
||||
SubscribeLocalEvent<InputMoverComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<InputMoverComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
|
||||
|
||||
SubscribeLocalEvent<InputMoverComponent, UpdateIsPredictedEvent>(OnUpdatePredicted);
|
||||
SubscribeLocalEvent<MovementRelayTargetComponent, UpdateIsPredictedEvent>(OnUpdateRelayTargetPredicted);
|
||||
SubscribeLocalEvent<PullableComponent, UpdateIsPredictedEvent>(OnUpdatePullablePredicted);
|
||||
return;
|
||||
}
|
||||
|
||||
private void OnUpdatePredicted(Entity<InputMoverComponent> 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<MovementRelayTargetComponent> entity, ref UpdateIsPredictedEvent args)
|
||||
{
|
||||
if (entity.Comp.Source == _playerManager.LocalEntity)
|
||||
args.IsPredicted = true;
|
||||
}
|
||||
|
||||
private void OnUpdatePullablePredicted(Entity<PullableComponent> 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<RelayInputMoverComponent> 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<RelayInputMoverComponent> 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<InputMoverComponent> entity, ref LocalPlayerAttachedEvent args)
|
||||
{
|
||||
SetMoveInput(entity, MoveButtons.None);
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(Entity<InputMoverComponent> 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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ namespace Content.Client.Power.APC
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = this.CreateWindow<ApcMenu>();
|
||||
_menu.SetEntity(Owner);
|
||||
_menu.OnBreaker += BreakerPressed;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,6 @@ public sealed class ActivatableUIRequiresPowerSystem : SharedActivatableUIRequir
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<WiresPanelComponent>(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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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<ReplayLoadingFailed>();
|
||||
state.SetData(exception, cancelAction, retryAction);
|
||||
}
|
||||
|
||||
public void ReturnToDefaultState()
|
||||
|
||||
36
Content.Client/Replay/UI/Loading/ReplayLoadingFailed.cs
Normal file
36
Content.Client/Replay/UI/Loading/ReplayLoadingFailed.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// State used to display an error message if a replay failed to load.
|
||||
/// </summary>
|
||||
/// <seealso cref="ReplayLoadingFailedControl"/>
|
||||
/// <seealso cref="ContentReplayPlaybackManager"/>
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:pllax="clr-namespace:Content.Client.Parallax">
|
||||
<pllax:ParallaxControl />
|
||||
<Control HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<PanelContainer StyleClasses="AngleRect" />
|
||||
<BoxContainer Orientation="Vertical" SetSize="800 600" Margin="2">
|
||||
<ScrollContainer Name="what" VerticalExpand="True" HScrollEnabled="False" Margin="0 0 0 2" ReturnMeasure="True">
|
||||
<RichTextLabel Name="ReasonLabel" VerticalAlignment="Top" />
|
||||
</ScrollContainer>
|
||||
<Button Name="RetryButton" StyleClasses="Caution" Text="{Loc 'replay-loading-retry'}" Visible="False" />
|
||||
<Button Name="CancelButton" Text="{Loc 'replay-loading-cancel'}" Visible="False" />
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
</Control>
|
||||
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
SeparationOverride="4">
|
||||
<EntityPrototypeView
|
||||
Name="ItemPrototype"
|
||||
Margin="4 4"
|
||||
Margin="4 0 0 0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
MinSize="32 32"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<BoxContainer Name="MainContainer" Orientation="Vertical">
|
||||
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'vending-machine-component-search-filter'}" HorizontalExpand="True" Margin ="4 4"/>
|
||||
<co:SearchListContainer Name="VendingContents" VerticalExpand="True" Margin="4 0"/>
|
||||
<co:SearchListContainer Name="VendingContents" VerticalExpand="True" Margin="4 4"/>
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
@@ -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!;
|
||||
|
||||
/// <summary>
|
||||
@@ -31,8 +34,6 @@ namespace Content.Client.Verbs
|
||||
/// </summary>
|
||||
public const float EntityMenuLookupSize = 0.25f;
|
||||
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// These flags determine what entities the user can see on the context menu.
|
||||
/// </summary>
|
||||
@@ -40,6 +41,8 @@ namespace Content.Client.Verbs
|
||||
|
||||
public Action<VerbsResponseEvent>? OnVerbsResponse;
|
||||
|
||||
private List<EntityUid> _entities = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -76,49 +79,50 @@ namespace Content.Client.Verbs
|
||||
visibility = ev.Visibility;
|
||||
|
||||
// Get entities
|
||||
List<EntityUid> 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<SpriteComponent>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<IPrototypeManager>();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var recipe in proto.EnumeratePrototypes<LatheRecipePrototype>())
|
||||
{
|
||||
if (recipe.Result == null)
|
||||
Assert.That(recipe.ResultReagents, Is.Not.Null, $"Recipe '{recipe.ID}' has no result or result reagents.");
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ public sealed class AdminSystem : EntitySystem
|
||||
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
|
||||
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
|
||||
SubscribeLocalEvent<ActorComponent, EntityRenamedEvent>(OnPlayerRenamed);
|
||||
}
|
||||
|
||||
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
|
||||
@@ -124,6 +125,11 @@ public sealed class AdminSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerRenamed(Entity<ActorComponent> ent, ref EntityRenamedEvent args)
|
||||
{
|
||||
UpdatePlayerList(ent.Comp.PlayerSession);
|
||||
}
|
||||
|
||||
public void UpdatePlayerList(ICommonSession player)
|
||||
{
|
||||
_playerList[player.UserId] = GetPlayerInfo(player.Data, player);
|
||||
|
||||
@@ -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<DeviceLinkSinkComponent>(Transform(tech).Coordinates, range);
|
||||
if (devices.Count < 1)
|
||||
return;
|
||||
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var device = _random.Pick(devices);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<GasTankComponent, GasAnalyzerScanEvent>(OnAnalyzed);
|
||||
SubscribeLocalEvent<GasTankComponent, PriceCalculationEvent>(OnGasTankPrice);
|
||||
SubscribeLocalEvent<GasTankComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb);
|
||||
Subs.CVar(_cfg, CCVars.AtmosTankFragment, UpdateMaxRange, true);
|
||||
}
|
||||
|
||||
private void UpdateMaxRange(float value)
|
||||
{
|
||||
_maxExplosionRange = value;
|
||||
}
|
||||
|
||||
private void OnGasShutdown(Entity<GasTankComponent> 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);
|
||||
|
||||
|
||||
@@ -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<GasPressurePumpComponent, AtmosDeviceDisabledEvent>(OnPumpLeaveAtmosphere);
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, ActivateInWorldEvent>(OnPumpActivate);
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
// Bound UI subscriptions
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, GasPressurePumpChangeOutputPressureMessage>(OnOutputPressureChangeMessage);
|
||||
SubscribeLocalEvent<GasPressurePumpComponent, GasPressurePumpToggleStatusMessage>(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<ApcPowerReceiverComponent>(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<ApcPowerReceiverComponent>(uid, out var power) && power.Powered);
|
||||
_appearance.SetData(uid, PumpVisuals.Enabled, pumpOn, appearance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GasVolumePumpComponent, AtmosDeviceDisabledEvent>(OnVolumePumpLeaveAtmosphere);
|
||||
SubscribeLocalEvent<GasVolumePumpComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<GasVolumePumpComponent, ActivateInWorldEvent>(OnPumpActivate);
|
||||
SubscribeLocalEvent<GasVolumePumpComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
// Bound UI subscriptions
|
||||
SubscribeLocalEvent<GasVolumePumpComponent, GasVolumePumpChangeTransferRateMessage>(OnTransferRateChangeMessage);
|
||||
SubscribeLocalEvent<GasVolumePumpComponent, GasVolumePumpToggleStatusMessage>(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<ApcPowerReceiverComponent>(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<ApcPowerReceiverComponent>(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);
|
||||
|
||||
@@ -111,6 +111,13 @@ namespace Content.Server.Body.Components
|
||||
[DataField]
|
||||
public SoundSpecifier BloodHealedSound = new SoundPathSpecifier("/Audio/Effects/lightburn.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float BloodHealedSoundThreshold = -0.1f;
|
||||
|
||||
// TODO probably damage bleed thresholds.
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -13,12 +13,12 @@ public sealed partial class ProduceComponent : SharedProduceComponent
|
||||
/// <summary>
|
||||
/// Seed data used to create a <see cref="SeedComponent"/> when this produce has its seeds extracted.
|
||||
/// </summary>
|
||||
[DataField("seed")]
|
||||
[DataField]
|
||||
public SeedData? Seed;
|
||||
|
||||
/// <summary>
|
||||
/// Seed data used to create a <see cref="SeedComponent"/> when this produce has its seeds extracted.
|
||||
/// </summary>
|
||||
[DataField("seedId", customTypeSerializer: typeof(PrototypeIdSerializer<SeedPrototype>))]
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<SeedPrototype>))]
|
||||
public string? SeedId;
|
||||
}
|
||||
|
||||
@@ -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<EntityPrototype>))]
|
||||
public List<string> ProductPrototypes = new();
|
||||
|
||||
[DataField("chemicals")] public Dictionary<string, SeedChemQuantity> Chemicals = new();
|
||||
[DataField] public Dictionary<string, SeedChemQuantity> Chemicals = new();
|
||||
|
||||
[DataField("consumeGasses")] public Dictionary<Gas, float> ConsumeGasses = new();
|
||||
[DataField] public Dictionary<Gas, float> ConsumeGasses = new();
|
||||
|
||||
[DataField("exudeGasses")] public Dictionary<Gas, float> ExudeGasses = new();
|
||||
[DataField] public Dictionary<Gas, float> 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;
|
||||
|
||||
/// <summary>
|
||||
/// If true, cannot be harvested for seeds. Balances hybrids and
|
||||
/// mutations.
|
||||
/// </summary>
|
||||
[DataField("seedless")] public bool Seedless = false;
|
||||
[DataField] public bool Seedless = false;
|
||||
|
||||
/// <summary>
|
||||
/// If false, rapidly decrease health while growing. Used to kill off
|
||||
/// plants with "bad" mutations.
|
||||
/// </summary>
|
||||
[DataField("viable")] public bool Viable = true;
|
||||
|
||||
/// <summary>
|
||||
/// If true, fruit slips players.
|
||||
/// </summary>
|
||||
[DataField("slip")] public bool Slip = false;
|
||||
|
||||
/// <summary>
|
||||
/// If true, fruits are sentient.
|
||||
/// </summary>
|
||||
[DataField("sentient")] public bool Sentient = false;
|
||||
[DataField] public bool Viable = true;
|
||||
|
||||
/// <summary>
|
||||
/// If true, a sharp tool is required to harvest this plant.
|
||||
/// </summary>
|
||||
[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";
|
||||
|
||||
/// <summary>
|
||||
/// Screams random sound, could be strict sound SoundPathSpecifier or collection SoundCollectionSpecifier
|
||||
/// base class is SoundSpecifier
|
||||
/// Screams random sound from collection SoundCollectionSpecifier
|
||||
/// </summary>
|
||||
[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<EntityPrototype>))] public string KudzuPrototype = "WeakKudzu";
|
||||
|
||||
public float BioluminescentRadius = 2f;
|
||||
|
||||
[DataField("kudzuPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] 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
|
||||
|
||||
/// <summary>
|
||||
/// The mutation effects that have been applied to this plant.
|
||||
/// </summary>
|
||||
[DataField] public List<RandomPlantMutation> Mutations { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// The seed prototypes this seed may mutate into when prompted to.
|
||||
/// </summary>
|
||||
[DataField("mutationPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer<SeedPrototype>))]
|
||||
[DataField(customTypeSerializer: typeof(PrototypeIdListSerializer<SeedPrototype>))]
|
||||
public List<string> 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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<SlipperyComponent>(entity);
|
||||
Dirty(entity, slippery);
|
||||
EnsureComp<StepTriggerComponent>(entity);
|
||||
// Need a fixture with a slip layer in order to actually do the slipping
|
||||
var fixtures = EnsureComp<FixturesComponent>(entity);
|
||||
var body = EnsureComp<PhysicsComponent>(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<CollisionWakeComponent>(entity);
|
||||
_colWakeSystem.SetEnabled(entity, false, collisionWake);
|
||||
}
|
||||
}
|
||||
|
||||
return products;
|
||||
|
||||
@@ -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<WeightedRandomFillSolutionPrototype>("RandomPickBotanyReagent");
|
||||
_randomMutations = _prototypeManager.Index<RandomPlantMutationListPrototype>("RandomPlantMutations");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void MutateSeed(ref SeedData seed, float severity)
|
||||
/// <param name="seed"></param>
|
||||
/// <param name="severity"></param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks all defined mutations against a seed to see which of them are applied.
|
||||
/// </summary>
|
||||
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<Gas, float> 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<Gas>().ToList());
|
||||
if (gasses.ContainsKey(gas))
|
||||
{
|
||||
gasses[gas] += amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
gasses.Add(gas, amount);
|
||||
}
|
||||
}
|
||||
|
||||
private void MutateChemicals(ref Dictionary<string, SeedChemQuantity> 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>{
|
||||
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<string, SeedChemQuantity> val, Dictionary<string, SeedChemQuantity> other)
|
||||
{
|
||||
// Go through chemicals from the pollen in swab
|
||||
|
||||
@@ -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<GhostRoleComponent>(uid);
|
||||
EnsureComp<GhostTakeoverAvailableComponent>(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<PointLightComponent>(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<PointLightComponent>(uid);
|
||||
}
|
||||
|
||||
if (!TryComp<AppearanceComponent>(uid, out var app))
|
||||
return;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Content.Server.Dragon;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class DragonRuleComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
@@ -10,7 +10,7 @@ public sealed partial class ActivateArtifact : EntityEffect
|
||||
public override void Effect(EntityEffectBaseArgs args)
|
||||
{
|
||||
var artifact = args.EntityManager.EntitySysManager.GetEntitySystem<ArtifactSystem>();
|
||||
artifact.TryActivateArtifact(args.TargetEntity);
|
||||
artifact.TryActivateArtifact(args.TargetEntity, logMissing: false);
|
||||
}
|
||||
|
||||
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) =>
|
||||
|
||||
48
Content.Server/EntityEffects/Effects/Glow.cs
Normal file
48
Content.Server/EntityEffects/Effects/Glow.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Content.Shared.EntityEffects;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.EntityEffects.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// Makes a mob glow.
|
||||
/// </summary>
|
||||
public sealed partial class Glow : EntityEffect
|
||||
{
|
||||
[DataField]
|
||||
public float Radius = 2f;
|
||||
|
||||
[DataField]
|
||||
public Color Color = Color.Black;
|
||||
|
||||
private static readonly List<Color> 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<IRobustRandom>();
|
||||
Color = random.Pick(Colors);
|
||||
}
|
||||
|
||||
var lightSystem = args.EntityManager.System<SharedPointLightSystem>();
|
||||
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";
|
||||
}
|
||||
}
|
||||
142
Content.Server/EntityEffects/Effects/PlantChangeStat.cs
Normal file
142
Content.Server/EntityEffects/Effects/PlantChangeStat.cs
Normal file
@@ -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<PlantHolderComponent>(args.TargetEntity);
|
||||
if (plantHolder == null || plantHolder.Seed == null)
|
||||
return;
|
||||
|
||||
var member = plantHolder.Seed.GetType().GetField(TargetValue);
|
||||
var mutationSys = args.EntityManager.System<MutationSystem>();
|
||||
|
||||
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<IRobustRandom>();
|
||||
return random.Prob(odds);
|
||||
}
|
||||
|
||||
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
55
Content.Server/EntityEffects/Effects/PlantMutateChemicals.cs
Normal file
55
Content.Server/EntityEffects/Effects/PlantMutateChemicals.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// changes the chemicals available in a plant's produce
|
||||
/// </summary>
|
||||
public sealed partial class PlantMutateChemicals : EntityEffect
|
||||
{
|
||||
public override void Effect(EntityEffectBaseArgs args)
|
||||
{
|
||||
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
|
||||
|
||||
if (plantholder.Seed == null)
|
||||
return;
|
||||
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
var chemicals = plantholder.Seed.Chemicals;
|
||||
var randomChems = prototypeManager.Index<WeightedRandomFillSolutionPrototype>("RandomPickBotanyReagent").Fills;
|
||||
|
||||
// Add a random amount of a random chemical to this set of chemicals
|
||||
if (randomChems != null)
|
||||
{
|
||||
var pick = random.Pick<RandomFillSolution>(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";
|
||||
}
|
||||
}
|
||||
87
Content.Server/EntityEffects/Effects/PlantMutateGases.cs
Normal file
87
Content.Server/EntityEffects/Effects/PlantMutateGases.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// changes the gases that a plant or produce create.
|
||||
/// </summary>
|
||||
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<PlantHolderComponent>(args.TargetEntity);
|
||||
|
||||
if (plantholder.Seed == null)
|
||||
return;
|
||||
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
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<Gas>().ToList());
|
||||
if (gasses.ContainsKey(gas))
|
||||
{
|
||||
gasses[gas] += amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
gasses.Add(gas, amount);
|
||||
}
|
||||
}
|
||||
|
||||
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
{
|
||||
return "TODO";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// changes the gases that a plant or produce consumes.
|
||||
/// </summary>
|
||||
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<PlantHolderComponent>(args.TargetEntity);
|
||||
|
||||
if (plantholder.Seed == null)
|
||||
return;
|
||||
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
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<Gas>().ToList());
|
||||
if (gasses.ContainsKey(gas))
|
||||
{
|
||||
gasses[gas] += amount;
|
||||
}
|
||||
else
|
||||
{
|
||||
gasses.Add(gas, amount);
|
||||
}
|
||||
}
|
||||
|
||||
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
{
|
||||
return "TODO";
|
||||
}
|
||||
}
|
||||
30
Content.Server/EntityEffects/Effects/PlantMutateHarvest.cs
Normal file
30
Content.Server/EntityEffects/Effects/PlantMutateHarvest.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Upgrades a plant's harvest type.
|
||||
/// </summary>
|
||||
public sealed partial class PlantMutateHarvest : EntityEffect
|
||||
{
|
||||
public override void Effect(EntityEffectBaseArgs args)
|
||||
{
|
||||
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(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";
|
||||
}
|
||||
}
|
||||
43
Content.Server/EntityEffects/Effects/PlantSpeciesChange.cs
Normal file
43
Content.Server/EntityEffects/Effects/PlantSpeciesChange.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Changes a plant into one of the species its able to mutate into.
|
||||
/// </summary>
|
||||
public sealed partial class PlantSpeciesChange : EntityEffect
|
||||
{
|
||||
public override void Effect(EntityEffectBaseArgs args)
|
||||
{
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
|
||||
|
||||
if (plantholder.Seed == null)
|
||||
return;
|
||||
|
||||
if (plantholder.Seed.MutationPrototypes.Count == 0)
|
||||
return;
|
||||
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
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";
|
||||
}
|
||||
}
|
||||
38
Content.Server/EntityEffects/Effects/Slipify.cs
Normal file
38
Content.Server/EntityEffects/Effects/Slipify.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Makes a mob slippery.
|
||||
/// </summary>
|
||||
public sealed partial class Slipify : EntityEffect
|
||||
{
|
||||
public override void Effect(EntityEffectBaseArgs args)
|
||||
{
|
||||
var fixtureSystem = args.EntityManager.System<FixtureSystem>();
|
||||
var colWakeSystem = args.EntityManager.System<CollisionWakeSystem>();
|
||||
var slippery = args.EntityManager.EnsureComponent<SlipperyComponent>(args.TargetEntity);
|
||||
args.EntityManager.Dirty(args.TargetEntity, slippery);
|
||||
args.EntityManager.EnsureComponent<StepTriggerComponent>(args.TargetEntity);
|
||||
// Need a fixture with a slip layer in order to actually do the slipping
|
||||
var fixtures = args.EntityManager.EnsureComponent<FixturesComponent>(args.TargetEntity);
|
||||
var body = args.EntityManager.EnsureComponent<PhysicsComponent>(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<CollisionWakeComponent>(args.TargetEntity);
|
||||
colWakeSystem.SetEnabled(args.TargetEntity, false, collisionWake);
|
||||
}
|
||||
|
||||
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -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<FingerprintComponent>(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;
|
||||
|
||||
@@ -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}"))
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class DragonRuleComponent : Component;
|
||||
52
Content.Server/GameTicking/Rules/DragonRuleSystem.cs
Normal file
52
Content.Server/GameTicking/Rules/DragonRuleSystem.cs
Normal file
@@ -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<DragonRuleComponent>
|
||||
{
|
||||
[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<DragonRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagEntitySelected);
|
||||
}
|
||||
|
||||
private void AfterAntagEntitySelected(Entity<DragonRuleComponent> 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<StationDataComponent>(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;
|
||||
}
|
||||
}
|
||||
@@ -36,8 +36,10 @@ public sealed class GravityGeneratorSystem : EntitySystem
|
||||
private void OnActivated(Entity<GravityGeneratorComponent> ent, ref ChargedMachineActivatedEvent args)
|
||||
{
|
||||
ent.Comp.GravityActive = true;
|
||||
if (TryComp<TransformComponent>(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<GravityGeneratorComponent> ent, ref ChargedMachineDeactivatedEvent args)
|
||||
{
|
||||
ent.Comp.GravityActive = false;
|
||||
if (TryComp<TransformComponent>(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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
111
Content.Server/Guidebook/GuidebookDataSystem.cs
Normal file
111
Content.Server/Guidebook/GuidebookDataSystem.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Reflection;
|
||||
using Content.Shared.Guidebook;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Guidebook;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class GuidebookDataSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
|
||||
private readonly Dictionary<string, List<MemberInfo>> _tagged = [];
|
||||
private GuidebookData _cachedData = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<RequestGuidebookDataEvent>(OnRequestRules);
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(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<EntityPrototype>())
|
||||
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<GuidebookDataAttribute>())
|
||||
{
|
||||
// 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<EntityPrototype>();
|
||||
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)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the cached data, then regathers it.
|
||||
/// </summary>
|
||||
private void RebuildDataCache()
|
||||
{
|
||||
_cachedData.Clear();
|
||||
GatherData(ref _cachedData);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ public sealed class IdentitySystem : SharedIdentitySystem
|
||||
SubscribeLocalEvent<IdentityComponent, DidUnequipEvent>((uid, _, _) => QueueIdentityUpdate(uid));
|
||||
SubscribeLocalEvent<IdentityComponent, DidUnequipHandEvent>((uid, _, _) => QueueIdentityUpdate(uid));
|
||||
SubscribeLocalEvent<IdentityComponent, WearerMaskToggledEvent>((uid, _, _) => QueueIdentityUpdate(uid));
|
||||
SubscribeLocalEvent<IdentityComponent, EntityRenamedEvent>((uid, _, _) => QueueIdentityUpdate(uid));
|
||||
SubscribeLocalEvent<IdentityComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,4 +22,16 @@ public sealed partial class NewsWriterComponent : Component
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier ConfirmSound = new SoundPathSpecifier("/Audio/Machines/scan_finish.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// This stores the working title of the current article
|
||||
/// </summary>
|
||||
[DataField, ViewVariables]
|
||||
public string DraftTitle = "";
|
||||
|
||||
/// <summary>
|
||||
/// This stores the working content of the current article
|
||||
/// </summary>
|
||||
[DataField, ViewVariables]
|
||||
public string DraftContent = "";
|
||||
}
|
||||
|
||||
@@ -51,6 +51,8 @@ public sealed class NewsSystem : SharedNewsSystem
|
||||
subs.Event<NewsWriterDeleteMessage>(OnWriteUiDeleteMessage);
|
||||
subs.Event<NewsWriterArticlesRequestMessage>(OnRequestArticlesUiMessage);
|
||||
subs.Event<NewsWriterPublishMessage>(OnWriteUiPublishMessage);
|
||||
subs.Event<NewsWriterSaveDraftMessage>(OnNewsWriterDraftUpdatedMessage);
|
||||
subs.Event<NewsWriterRequestDraftMessage>(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<NewsWriterComponent> ent, ref NewsWriterSaveDraftMessage args)
|
||||
{
|
||||
ent.Comp.DraftTitle = args.DraftTitle;
|
||||
ent.Comp.DraftContent = args.DraftContent;
|
||||
}
|
||||
|
||||
private void OnRequestArticleDraftMessage(Entity<NewsWriterComponent> ent, ref NewsWriterRequestDraftMessage msg)
|
||||
{
|
||||
UpdateWriterUi(ent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 <Username|EntityUid> <New character name>";
|
||||
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<MetaDataComponent>(entityUid.Value);
|
||||
var oldName = metadata.EntityName;
|
||||
_entManager.System<MetaDataSystem>().SetEntityName(entityUid.Value, name, metadata);
|
||||
|
||||
var minds = _entManager.System<SharedMindSystem>();
|
||||
|
||||
if (minds.TryGetMind(entityUid.Value, out var mindId, out var mind))
|
||||
{
|
||||
// Mind
|
||||
mind.CharacterName = name;
|
||||
_entManager.Dirty(mindId, mind);
|
||||
}
|
||||
|
||||
// Id Cards
|
||||
if (_entManager.TrySystem<IdCardSystem>(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<StationRecordsSystem>(out var recordsSystem)
|
||||
&& _entManager.TryGetComponent(idCard, out StationRecordKeyStorageComponent? keyStorage)
|
||||
&& keyStorage.Key is {} key)
|
||||
{
|
||||
if (recordsSystem.TryGetRecord<GeneralStationRecord>(key, out var generalRecord))
|
||||
{
|
||||
generalRecord.Name = name;
|
||||
}
|
||||
|
||||
recordsSystem.Synchronize(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PDAs
|
||||
if (_entManager.TrySystem<PdaSystem>(out var pdaSystem))
|
||||
{
|
||||
var query = _entManager.EntityQueryEnumerator<PdaComponent>();
|
||||
while (query.MoveNext(out var uid, out var pda))
|
||||
{
|
||||
if (pda.OwnerName == oldName)
|
||||
{
|
||||
pdaSystem.SetOwner(uid, pda, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Admin Overlay
|
||||
if (_entManager.TrySystem<AdminSystem>(out var adminSystem)
|
||||
&& _entManager.TryGetComponent<ActorComponent>(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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -116,14 +116,8 @@ public sealed class FoodSequenceSystem : SharedFoodSequenceSystem
|
||||
return false;
|
||||
|
||||
//looking for a suitable FoodSequence prototype
|
||||
ProtoId<FoodSequenceElementPrototype> 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))
|
||||
|
||||
@@ -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<string>? _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));
|
||||
|
||||
@@ -72,14 +72,15 @@ public sealed class StealConditionSystem : EntitySystem
|
||||
private void OnAfterAssign(Entity<StealConditionComponent> 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);
|
||||
|
||||
@@ -55,9 +55,23 @@ namespace Content.Server.PDA
|
||||
SubscribeLocalEvent<PdaComponent, CartridgeLoaderNotificationSentEvent>(OnNotification);
|
||||
|
||||
SubscribeLocalEvent<StationRenamedEvent>(OnStationRenamed);
|
||||
SubscribeLocalEvent<EntityRenamedEvent>(OnEntityRenamed);
|
||||
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
|
||||
}
|
||||
|
||||
private void OnEntityRenamed(ref EntityRenamedEvent ev)
|
||||
{
|
||||
var query = EntityQueryEnumerator<PdaComponent>();
|
||||
|
||||
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<PdaComponent>();
|
||||
var query = AllEntityQuery<PdaComponent>();
|
||||
while (query.MoveNext(out var ent, out var comp))
|
||||
{
|
||||
UpdatePdaUi(ent, comp);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<WiresPanelComponent>(ent.Owner, out var panel) && panel.Open)
|
||||
return;
|
||||
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// <see cref="FillGridDunGen"/>
|
||||
/// </summary>
|
||||
private async Task<Dungeon> GenerateFillDunGen(DungeonData data, HashSet<Vector2i> reservedTiles)
|
||||
private async Task<Dungeon> GenerateFillDunGen(FillGridDunGen fill, DungeonData data, HashSet<Vector2i> 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;
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ public sealed partial class DungeonJob : Job<List<Dungeon>>
|
||||
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);
|
||||
|
||||
@@ -170,7 +170,7 @@ public sealed partial class RevenantSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
ChangeEssenceAmount(uid, abilityCost, component, false);
|
||||
ChangeEssenceAmount(uid, -abilityCost, component, false);
|
||||
|
||||
_statusEffects.TryAddStatusEffect<CorporealComponent>(uid, "Corporeal", TimeSpan.FromSeconds(debuffs.Y), false);
|
||||
_stun.TryStun(uid, TimeSpan.FromSeconds(debuffs.X), false);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -19,6 +19,6 @@ public sealed class DockEmergencyShuttleCommand : IConsoleCommand
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var system = _sysManager.GetEntitySystem<EmergencyShuttleSystem>();
|
||||
system.CallEmergencyShuttle();
|
||||
system.DockEmergencyShuttle();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PlayerSpawningEvent>(HandlePlayerSpawning, before: new []{ typeof(ContainerSpawnPointSystem), typeof(SpawnPointSystem)});
|
||||
SubscribeLocalEvent<PlayerSpawningEvent>(HandlePlayerSpawning, before: new []{ typeof(SpawnPointSystem)}, after: new [] { typeof(ContainerSpawnPointSystem)});
|
||||
|
||||
SubscribeLocalEvent<StationArrivalsComponent, StationPostInitEvent>(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;
|
||||
|
||||
@@ -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<TransformComponent> 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<PriorityDockComponent>(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<PriorityDockComponent>(docks.DockBUid, out var priority)
|
||||
&& priority.Tag?.Equals(priorityTag) == true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the shuttle can warp to the specified position.
|
||||
/// </summary>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to dock the emergency shuttle to the station.
|
||||
/// Attempts to dock a station's emergency shuttle.
|
||||
/// </summary>
|
||||
public void CallEmergencyShuttle(EntityUid stationUid, StationEmergencyShuttleComponent? stationShuttle = null)
|
||||
/// <seealso cref="DockEmergencyShuttle"/>
|
||||
public ShuttleDockResult? DockSingleEmergencyShuttle(EntityUid stationUid, StationEmergencyShuttleComponent? stationShuttle = null)
|
||||
{
|
||||
if (!Resolve(stationUid, ref stationShuttle))
|
||||
return;
|
||||
return null;
|
||||
|
||||
if (!TryComp(stationShuttle.EmergencyShuttle, out TransformComponent? xform) ||
|
||||
!TryComp<ShuttleComponent>(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<StationDataComponent>(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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do post-shuttle-dock setup. Announce to the crew and set up shuttle timers.
|
||||
/// </summary>
|
||||
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<TransformComponent>();
|
||||
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<DeviceNetworkComponent>(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<DeviceNetworkComponent>(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<TransformComponent>(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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public void CallEmergencyShuttle()
|
||||
/// <remarks>
|
||||
/// If the emergency shuttle is disabled, this immediately ends the round.
|
||||
/// </remarks>
|
||||
public void DockEmergencyShuttle()
|
||||
{
|
||||
if (EmergencyShuttleArrived)
|
||||
return;
|
||||
@@ -371,9 +442,34 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
|
||||
|
||||
var query = AllEntityQuery<StationEmergencyShuttleComponent>();
|
||||
|
||||
var dockResults = new List<ShuttleDockResult>();
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A result of a shuttle dock operation done by <see cref="EmergencyShuttleSystem.DockSingleEmergencyShuttle"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="ShuttleDockResultType"/>
|
||||
public sealed class ShuttleDockResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The station for which the emergency shuttle got docked.
|
||||
/// </summary>
|
||||
public Entity<StationEmergencyShuttleComponent> Station;
|
||||
|
||||
/// <summary>
|
||||
/// The target grid of the station that the shuttle tried to dock to.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not present if <see cref="ResultType"/> is <see cref="ShuttleDockResultType.GoodLuck"/>.
|
||||
/// </remarks>
|
||||
public EntityUid? TargetGrid;
|
||||
|
||||
/// <summary>
|
||||
/// Enum code describing the dock result.
|
||||
/// </summary>
|
||||
public ShuttleDockResultType ResultType;
|
||||
|
||||
/// <summary>
|
||||
/// The docking config used to actually dock to the station.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only present if <see cref="ResultType"/> is <see cref="ShuttleDockResultType.PriorityDock"/>
|
||||
/// or <see cref="ShuttleDockResultType.NoDock"/>.
|
||||
/// </remarks>
|
||||
public DockingConfig? DockingConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Emergency shuttle dock result codes used by <see cref="ShuttleDockResult"/>.
|
||||
/// </summary>
|
||||
public enum ShuttleDockResultType : byte
|
||||
{
|
||||
// This enum is ordered from "best" to "worst". This is used to sort the results.
|
||||
|
||||
/// <summary>
|
||||
/// The shuttle was docked at a priority dock, which is the intended destination.
|
||||
/// </summary>
|
||||
PriorityDock,
|
||||
|
||||
/// <summary>
|
||||
/// The shuttle docked at another dock on the station then the intended priority dock.
|
||||
/// </summary>
|
||||
OtherDock,
|
||||
|
||||
/// <summary>
|
||||
/// The shuttle couldn't find any suitable dock on the station at all, it did not dock.
|
||||
/// </summary>
|
||||
NoDock,
|
||||
|
||||
/// <summary>
|
||||
/// No station grid was found at all, shuttle did not get moved.
|
||||
/// </summary>
|
||||
GoodLuck,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +72,11 @@ public sealed partial class ShuttleSystem
|
||||
|
||||
private readonly HashSet<EntityUid> _lookupEnts = new();
|
||||
private readonly HashSet<EntityUid> _immuneEnts = new();
|
||||
private readonly HashSet<Entity<NoFTLComponent>> _noFtls = new();
|
||||
|
||||
private EntityQuery<BodyComponent> _bodyQuery;
|
||||
private EntityQuery<BuckleComponent> _buckleQuery;
|
||||
private EntityQuery<FTLBeaconComponent> _beaconQuery;
|
||||
private EntityQuery<GhostComponent> _ghostQuery;
|
||||
private EntityQuery<FTLSmashImmuneComponent> _immuneQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private EntityQuery<StatusEffectsComponent> _statusQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
@@ -88,8 +88,7 @@ public sealed partial class ShuttleSystem
|
||||
|
||||
_bodyQuery = GetEntityQuery<BodyComponent>();
|
||||
_buckleQuery = GetEntityQuery<BuckleComponent>();
|
||||
_beaconQuery = GetEntityQuery<FTLBeaconComponent>();
|
||||
_ghostQuery = GetEntityQuery<GhostComponent>();
|
||||
_immuneQuery = GetEntityQuery<FTLSmashImmuneComponent>();
|
||||
_physicsQuery = GetEntityQuery<PhysicsComponent>();
|
||||
_statusQuery = GetEntityQuery<StatusEffectsComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
@@ -104,7 +103,7 @@ public sealed partial class ShuttleSystem
|
||||
|
||||
private void OnFtlShutdown(Entity<FTLComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
Del(ent.Comp.VisualizerEntity);
|
||||
QueueDel(ent.Comp.VisualizerEntity);
|
||||
ent.Comp.VisualizerEntity = null;
|
||||
}
|
||||
|
||||
@@ -423,7 +422,12 @@ public sealed partial class ShuttleSystem
|
||||
// Offset the start by buffer range just to avoid overlap.
|
||||
var ftlStart = new EntityCoordinates(ftlMap, new Vector2(_index + width / 2f, 0f) - shuttleCenter);
|
||||
|
||||
// Store the matrix for the grid prior to movement. This means any entities we need to leave behind we can make sure their positions are updated.
|
||||
// Setting the entity to map directly may run grid traversal (at least at time of writing this).
|
||||
var oldMapUid = xform.MapUid;
|
||||
var oldGridMatrix = _transform.GetWorldMatrix(xform);
|
||||
_transform.SetCoordinates(entity.Owner, ftlStart);
|
||||
LeaveNoFTLBehind((entity.Owner, xform), oldGridMatrix, oldMapUid);
|
||||
|
||||
// Reset rotation so they always face the same direction.
|
||||
xform.LocalRotation = Angle.Zero;
|
||||
@@ -495,6 +499,9 @@ public sealed partial class ShuttleSystem
|
||||
|
||||
MapId mapId;
|
||||
|
||||
QueueDel(entity.Comp1.VisualizerEntity);
|
||||
entity.Comp1.VisualizerEntity = null;
|
||||
|
||||
if (!Exists(entity.Comp1.TargetCoordinates.EntityId))
|
||||
{
|
||||
// Uhh good luck
|
||||
@@ -647,6 +654,31 @@ public sealed partial class ShuttleSystem
|
||||
}
|
||||
}
|
||||
|
||||
private void LeaveNoFTLBehind(Entity<TransformComponent> grid, Matrix3x2 oldGridMatrix, EntityUid? oldMapUid)
|
||||
{
|
||||
if (oldMapUid == null)
|
||||
return;
|
||||
|
||||
_noFtls.Clear();
|
||||
var oldGridRotation = oldGridMatrix.Rotation();
|
||||
_lookup.GetGridEntities(grid.Owner, _noFtls);
|
||||
|
||||
foreach (var childUid in _noFtls)
|
||||
{
|
||||
if (!_xformQuery.TryComp(childUid, out var childXform))
|
||||
continue;
|
||||
|
||||
// If we're not parented directly to the grid the matrix may be wrong.
|
||||
var relative = _physics.GetRelativePhysicsTransform(childUid.Owner, (grid.Owner, grid.Comp));
|
||||
|
||||
_transform.SetCoordinates(
|
||||
childUid,
|
||||
childXform,
|
||||
new EntityCoordinates(oldMapUid.Value,
|
||||
Vector2.Transform(relative.Position, oldGridMatrix)), rotation: relative.Quaternion2D.Angle + oldGridRotation);
|
||||
}
|
||||
}
|
||||
|
||||
private void KnockOverKids(TransformComponent xform, ref ValueList<EntityUid> toKnock)
|
||||
{
|
||||
// Not recursive because probably not necessary? If we need it to be that's why this method is separate.
|
||||
@@ -688,8 +720,28 @@ public sealed partial class ShuttleSystem
|
||||
/// Tries to dock with the target grid, otherwise falls back to proximity.
|
||||
/// This bypasses FTL travel time.
|
||||
/// </summary>
|
||||
public bool TryFTLDock(EntityUid shuttleUid, ShuttleComponent component, EntityUid targetUid, string? priorityTag = null)
|
||||
public bool TryFTLDock(
|
||||
EntityUid shuttleUid,
|
||||
ShuttleComponent component,
|
||||
EntityUid targetUid,
|
||||
string? priorityTag = null)
|
||||
{
|
||||
return TryFTLDock(shuttleUid, component, targetUid, out _, priorityTag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to dock with the target grid, otherwise falls back to proximity.
|
||||
/// This bypasses FTL travel time.
|
||||
/// </summary>
|
||||
public bool TryFTLDock(
|
||||
EntityUid shuttleUid,
|
||||
ShuttleComponent component,
|
||||
EntityUid targetUid,
|
||||
[NotNullWhen(true)] out DockingConfig? config,
|
||||
string? priorityTag = null)
|
||||
{
|
||||
config = null;
|
||||
|
||||
if (!_xformQuery.TryGetComponent(shuttleUid, out var shuttleXform) ||
|
||||
!_xformQuery.TryGetComponent(targetUid, out var targetXform) ||
|
||||
targetXform.MapUid == null ||
|
||||
@@ -698,7 +750,7 @@ public sealed partial class ShuttleSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
var config = _dockSystem.GetDockingConfig(shuttleUid, targetUid, priorityTag);
|
||||
config = _dockSystem.GetDockingConfig(shuttleUid, targetUid, priorityTag);
|
||||
|
||||
if (config != null)
|
||||
{
|
||||
@@ -923,8 +975,11 @@ public sealed partial class ShuttleSystem
|
||||
if (!Resolve(uid, ref manager, ref grid, ref xform) || xform.MapUid == null)
|
||||
return;
|
||||
|
||||
if (!TryComp(xform.MapUid, out BroadphaseComponent? lookup))
|
||||
return;
|
||||
|
||||
// Flatten anything not parented to a grid.
|
||||
var transform = _physics.GetPhysicsTransform(uid, xform);
|
||||
var transform = _physics.GetRelativePhysicsTransform((uid, xform), xform.MapUid.Value);
|
||||
var aabbs = new List<Box2>(manager.Fixtures.Count);
|
||||
var tileSet = new List<(Vector2i, Tile)>();
|
||||
|
||||
@@ -945,7 +1000,8 @@ public sealed partial class ShuttleSystem
|
||||
_biomes.ReserveTiles(xform.MapUid.Value, aabb, tileSet);
|
||||
_lookupEnts.Clear();
|
||||
_immuneEnts.Clear();
|
||||
_lookup.GetEntitiesIntersecting(xform.MapUid.Value, aabb, _lookupEnts, LookupFlags.Uncontained);
|
||||
// TODO: Ideally we'd query first BEFORE moving grid but needs adjustments above.
|
||||
_lookup.GetLocalEntitiesIntersecting(xform.MapUid.Value, fixture.Shape, transform, _lookupEnts, flags: LookupFlags.Uncontained, lookup: lookup);
|
||||
|
||||
foreach (var ent in _lookupEnts)
|
||||
{
|
||||
@@ -954,7 +1010,13 @@ public sealed partial class ShuttleSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_ghostQuery.HasComponent(ent) || _beaconQuery.HasComponent(ent))
|
||||
// If it's on our grid ignore it.
|
||||
if (!_xformQuery.TryComp(ent, out var childXform) || childXform.GridUid == uid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_immuneQuery.HasComponent(ent))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -968,9 +1030,6 @@ public sealed partial class ShuttleSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
if (HasComp<FTLBeaconComponent>(ent))
|
||||
continue;
|
||||
|
||||
QueueDel(ent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Spawners.EntitySystems;
|
||||
@@ -11,6 +14,7 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
|
||||
@@ -26,6 +30,13 @@ public sealed class ContainerSpawnPointSystem : EntitySystem
|
||||
if (args.SpawnResult != null)
|
||||
return;
|
||||
|
||||
// If it's just a spawn pref check if it's for cryo (silly).
|
||||
if (args.HumanoidCharacterProfile?.SpawnPriority != SpawnPriorityPreference.Cryosleep &&
|
||||
(!_proto.TryIndex(args.Job?.Prototype, out var jobProto) || jobProto.JobEntity == null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var query = EntityQueryEnumerator<ContainerSpawnPointComponent, ContainerManagerComponent, TransformComponent>();
|
||||
var possibleContainers = new List<Entity<ContainerSpawnPointComponent, ContainerManagerComponent, TransformComponent>>();
|
||||
|
||||
|
||||
@@ -7,29 +7,11 @@ namespace Content.Server.Speech.EntitySystems;
|
||||
public sealed partial class SkeletonAccentSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
|
||||
|
||||
[GeneratedRegex(@"(?<!\w)[^aeiou]one", RegexOptions.IgnoreCase, "en-US")]
|
||||
private static partial Regex BoneRegex();
|
||||
|
||||
private static readonly Dictionary<string, string> DirectReplacements = new()
|
||||
{
|
||||
{ "fuck you", "I've got a BONE to pick with you" },
|
||||
{ "fucked", "boned"},
|
||||
{ "fuck", "RATTLE RATTLE" },
|
||||
{ "fck", "RATTLE RATTLE" },
|
||||
{ "shit", "RATTLE RATTLE" }, // Capitalize RATTLE RATTLE regardless of original message case.
|
||||
{ "definitely", "make no bones about it" },
|
||||
{ "absolutely", "make no bones about it" },
|
||||
{ "afraid", "rattled"},
|
||||
{ "scared", "rattled"},
|
||||
{ "spooked", "rattled"},
|
||||
{ "shocked", "rattled"},
|
||||
{ "killed", "skeletonized"},
|
||||
{ "humorous", "humerus"},
|
||||
{ "to be a", "tibia"},
|
||||
{ "under", "ulna"}
|
||||
};
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -50,11 +32,8 @@ public sealed partial class SkeletonAccentSystem : EntitySystem
|
||||
// At the start of words, any non-vowel + "one" becomes "bone", e.g. tone -> bone ; lonely -> bonely; clone -> clone (remains unchanged).
|
||||
msg = BoneRegex().Replace(msg, "bone");
|
||||
|
||||
// Direct word/phrase replacements:
|
||||
foreach (var (first, replace) in DirectReplacements)
|
||||
{
|
||||
msg = Regex.Replace(msg, $@"(?<!\w){first}(?!\w)", replace, RegexOptions.IgnoreCase);
|
||||
}
|
||||
// apply word replacements
|
||||
msg = _replacement.ApplyReplacements(msg, "skeleton");
|
||||
|
||||
// Suffix:
|
||||
if (_random.Prob(component.ackChance))
|
||||
|
||||
@@ -250,7 +250,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
|
||||
_accessSystem.SetAccessToJob(cardId, jobPrototype, extendedAccess);
|
||||
|
||||
if (pdaComponent != null)
|
||||
_pdaSystem.SetOwner(idUid.Value, pdaComponent, characterName);
|
||||
_pdaSystem.SetOwner(idUid.Value, pdaComponent, entity, characterName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.StationEvents.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(BureaucraticErrorRule))]
|
||||
public sealed partial class BureaucraticErrorRuleComponent : Component
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The jobs that are ignored by this rule and won't have their slots changed.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<JobPrototype>> IgnoredJobs = new();
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Roles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
@@ -23,6 +23,9 @@ public sealed class BureaucraticErrorRule : StationEventSystem<BureaucraticError
|
||||
|
||||
var jobList = _stationJobs.GetJobs(chosenStation.Value).Keys.ToList();
|
||||
|
||||
foreach(var job in component.IgnoredJobs)
|
||||
jobList.Remove(job);
|
||||
|
||||
if (jobList.Count == 0)
|
||||
return;
|
||||
|
||||
|
||||
@@ -235,6 +235,8 @@ public sealed class IonStormRule : StationEventSystem<IonStormRuleComponent>
|
||||
|
||||
if (plural) feeling = feelingPlural;
|
||||
|
||||
var subjects = RobustRandom.Prob(0.5f) ? objectsThreats : Loc.GetString("ion-storm-people");
|
||||
|
||||
// message logic!!!
|
||||
return RobustRandom.Next(0, 36) switch
|
||||
{
|
||||
@@ -266,7 +268,7 @@ public sealed class IonStormRule : StationEventSystem<IonStormRuleComponent>
|
||||
26 => Loc.GetString("ion-storm-law-crew-must-go", ("who", crewAll), ("area", area)),
|
||||
27 => Loc.GetString("ion-storm-law-crew-only-1", ("who", crew1), ("part", part)),
|
||||
28 => Loc.GetString("ion-storm-law-crew-only-2", ("who", crew1), ("other", crew2), ("part", part)),
|
||||
29 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", RobustRandom.Prob(0.5f) ? objectsThreats : "PEOPLE"), ("part", part)),
|
||||
29 => Loc.GetString("ion-storm-law-crew-only-subjects", ("adjective", adjective), ("subjects", subjects), ("part", part)),
|
||||
30 => Loc.GetString("ion-storm-law-crew-must-do", ("must", must), ("part", part)),
|
||||
31 => Loc.GetString("ion-storm-law-crew-must-have", ("adjective", adjective), ("objects", objects), ("part", part)),
|
||||
32 => Loc.GetString("ion-storm-law-crew-must-eat", ("who", who), ("adjective", adjective), ("food", food), ("part", part)),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Content.Server.Access.Systems;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Preferences;
|
||||
@@ -35,12 +38,14 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly StationRecordKeyStorageSystem _keyStorage = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IdCardSystem _idCard = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawn);
|
||||
SubscribeLocalEvent<EntityRenamedEvent>(OnRename);
|
||||
}
|
||||
|
||||
private void OnPlayerSpawn(PlayerSpawnCompleteEvent args)
|
||||
@@ -51,6 +56,30 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem
|
||||
CreateGeneralRecord(args.Station, args.Mob, args.Profile, args.JobId, stationRecords);
|
||||
}
|
||||
|
||||
private void OnRename(ref EntityRenamedEvent ev)
|
||||
{
|
||||
// When a player gets renamed their card gets changed to match.
|
||||
// Unfortunately this means that an event is called for it as well, and since TryFindIdCard will succeed if the
|
||||
// given entity is a card and the card itself is the key the record will be mistakenly renamed to the card's name
|
||||
// if we don't return early.
|
||||
if (HasComp<IdCardComponent>(ev.Uid))
|
||||
return;
|
||||
|
||||
if (_idCard.TryFindIdCard(ev.Uid, out var idCard))
|
||||
{
|
||||
if (TryComp(idCard, out StationRecordKeyStorageComponent? keyStorage)
|
||||
&& keyStorage.Key is {} key)
|
||||
{
|
||||
if (TryGetRecord<GeneralStationRecord>(key, out var generalRecord))
|
||||
{
|
||||
generalRecord.Name = ev.NewName;
|
||||
}
|
||||
|
||||
Synchronize(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateGeneralRecord(EntityUid station, EntityUid player, HumanoidCharacterProfile profile,
|
||||
string? jobId, StationRecordsComponent records)
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
{
|
||||
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraDeactivateEvent>(OnSurveillanceCameraDeactivate);
|
||||
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
|
||||
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, ComponentStartup>(OnComponentStartup);
|
||||
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, AfterActivatableUIOpenEvent>(OnToggleInterface);
|
||||
@@ -196,6 +197,12 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, SurveillanceCameraMonitorComponent component, ComponentShutdown args)
|
||||
{
|
||||
RemoveActiveCamera(uid, component);
|
||||
}
|
||||
|
||||
|
||||
private void OnToggleInterface(EntityUid uid, SurveillanceCameraMonitorComponent component,
|
||||
AfterActivatableUIOpenEvent args)
|
||||
{
|
||||
|
||||
@@ -321,6 +321,13 @@ public sealed class SurveillanceCameraSystem : EntitySystem
|
||||
{
|
||||
AddActiveViewer(camera, player, monitor, component);
|
||||
}
|
||||
|
||||
// Add monitor without viewers
|
||||
if (players.Count == 0 && monitor != null)
|
||||
{
|
||||
component.ActiveMonitors.Add(monitor.Value);
|
||||
UpdateVisuals(camera, component);
|
||||
}
|
||||
}
|
||||
|
||||
// Switch the set of active viewers from one camera to another.
|
||||
@@ -349,13 +356,12 @@ public sealed class SurveillanceCameraSystem : EntitySystem
|
||||
|
||||
public void RemoveActiveViewer(EntityUid camera, EntityUid player, EntityUid? monitor = null, SurveillanceCameraComponent? component = null, ActorComponent? actor = null)
|
||||
{
|
||||
if (!Resolve(camera, ref component)
|
||||
|| !Resolve(player, ref actor))
|
||||
{
|
||||
if (!Resolve(camera, ref component))
|
||||
return;
|
||||
}
|
||||
|
||||
_viewSubscriberSystem.RemoveViewSubscriber(camera, actor.PlayerSession);
|
||||
if (Resolve(player, ref actor))
|
||||
_viewSubscriberSystem.RemoveViewSubscriber(camera, actor.PlayerSession);
|
||||
|
||||
component.ActiveViewers.Remove(player);
|
||||
|
||||
if (monitor != null)
|
||||
@@ -377,6 +383,13 @@ public sealed class SurveillanceCameraSystem : EntitySystem
|
||||
{
|
||||
RemoveActiveViewer(camera, player, monitor, component);
|
||||
}
|
||||
|
||||
// Even if not removing any viewers, remove the monitor
|
||||
if (players.Count == 0 && monitor != null)
|
||||
{
|
||||
component.ActiveMonitors.Remove(monitor.Value);
|
||||
UpdateVisuals(camera, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateVisuals(EntityUid uid, SurveillanceCameraComponent? component = null, AppearanceComponent? appearance = null)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Traits;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -24,6 +25,14 @@ public sealed class TraitSystem : EntitySystem
|
||||
// When the player is spawned in, add all trait components selected during character creation
|
||||
private void OnPlayerSpawnComplete(PlayerSpawnCompleteEvent args)
|
||||
{
|
||||
// Check if player's job allows to apply traits
|
||||
if (args.JobId == null ||
|
||||
!_prototypeManager.TryIndex<JobPrototype>(args.JobId ?? string.Empty, out var protoJob) ||
|
||||
!protoJob.ApplyTraits)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var traitId in args.Profile.TraitPreferences)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<TraitPrototype>(traitId, out var traitPrototype))
|
||||
|
||||
@@ -140,10 +140,11 @@ public sealed partial class ArtifactSystem : EntitySystem
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="component"></param>
|
||||
/// <param name="logMissing">Set this to false if you don't know if the entity is an artifact.</param>
|
||||
/// <returns></returns>
|
||||
public bool TryActivateArtifact(EntityUid uid, EntityUid? user = null, ArtifactComponent? component = null)
|
||||
public bool TryActivateArtifact(EntityUid uid, EntityUid? user = null, ArtifactComponent? component = null, bool logMissing = true)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
if (!Resolve(uid, ref component, logMissing))
|
||||
return false;
|
||||
|
||||
// check if artifact is under suppression field
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class ChemicalPuddleArtifactSystem : EntitySystem
|
||||
/// The key for the node data entry containing
|
||||
/// the chemicals that the puddle is made of.
|
||||
/// </summary>
|
||||
public const string NodeDataChemicalList = "nodeDataSpawnAmount";
|
||||
public const string NodeDataChemicalList = "nodeDataChemicalList";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
|
||||
@@ -25,6 +25,19 @@ public abstract class SharedIdCardSystem : EntitySystem
|
||||
|
||||
SubscribeLocalEvent<IdCardComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
|
||||
SubscribeLocalEvent<EntityRenamedEvent>(OnRename);
|
||||
}
|
||||
|
||||
private void OnRename(ref EntityRenamedEvent ev)
|
||||
{
|
||||
// When a player gets renamed their id card is renamed as well to match.
|
||||
// Unfortunately since TryFindIdCard will succeed if the entity is also a card this means that the card will
|
||||
// keep renaming itself unless we return early.
|
||||
if (HasComp<IdCardComponent>(ev.Uid))
|
||||
return;
|
||||
|
||||
if (TryFindIdCard(ev.Uid, out var idCard))
|
||||
TryChangeFullName(idCard, ev.NewName, idCard);
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args)
|
||||
|
||||
@@ -45,6 +45,12 @@ public sealed partial class PerishableComponent : Component
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public int Stage;
|
||||
|
||||
/// <summary>
|
||||
/// If true, rot will always progress.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool ForceRotProgression;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -115,6 +115,10 @@ public abstract class SharedRottingSystem : EntitySystem
|
||||
if (!Resolve(uid, ref perishable, false))
|
||||
return false;
|
||||
|
||||
// Overrides all the other checks.
|
||||
if (perishable.ForceRotProgression)
|
||||
return true;
|
||||
|
||||
// only dead things or inanimate objects can rot
|
||||
if (TryComp<MobStateComponent>(uid, out var mobState) && !_mobState.IsDead(uid, mobState))
|
||||
return false;
|
||||
|
||||
@@ -242,8 +242,9 @@ public abstract partial class SharedBuckleSystem
|
||||
if (_whitelistSystem.IsWhitelistFail(strapComp.Whitelist, buckleUid) ||
|
||||
_whitelistSystem.IsBlacklistPass(strapComp.Blacklist, buckleUid))
|
||||
{
|
||||
if (_netManager.IsServer && popup && user != null)
|
||||
_popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), user.Value, user.Value, PopupType.Medium);
|
||||
if (popup)
|
||||
_popup.PopupClient(Loc.GetString("buckle-component-cannot-fit-message"), user, PopupType.Medium);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -261,23 +262,24 @@ public abstract partial class SharedBuckleSystem
|
||||
|
||||
if (user != null && !HasComp<HandsComponent>(user))
|
||||
{
|
||||
// PopupPredicted when
|
||||
if (_netManager.IsServer && popup)
|
||||
_popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user.Value, user.Value);
|
||||
if (popup)
|
||||
_popup.PopupClient(Loc.GetString("buckle-component-no-hands-message"), user);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buckleComp.Buckled)
|
||||
{
|
||||
if (_netManager.IsClient || popup || user == null)
|
||||
return false;
|
||||
|
||||
var message = Loc.GetString(buckleUid == user
|
||||
if (popup)
|
||||
{
|
||||
var message = Loc.GetString(buckleUid == user
|
||||
? "buckle-component-already-buckled-message"
|
||||
: "buckle-component-other-already-buckled-message",
|
||||
("owner", Identity.Entity(buckleUid, EntityManager)));
|
||||
|
||||
_popup.PopupEntity(message, user.Value, user.Value);
|
||||
_popup.PopupClient(message, user);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -291,29 +293,30 @@ public abstract partial class SharedBuckleSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_netManager.IsClient || popup || user == null)
|
||||
return false;
|
||||
|
||||
var message = Loc.GetString(buckleUid == user
|
||||
if (popup)
|
||||
{
|
||||
var message = Loc.GetString(buckleUid == user
|
||||
? "buckle-component-cannot-buckle-message"
|
||||
: "buckle-component-other-cannot-buckle-message",
|
||||
("owner", Identity.Entity(buckleUid, EntityManager)));
|
||||
|
||||
_popup.PopupEntity(message, user.Value, user.Value);
|
||||
_popup.PopupClient(message, user);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!StrapHasSpace(strapUid, buckleComp, strapComp))
|
||||
{
|
||||
if (_netManager.IsClient || popup || user == null)
|
||||
return false;
|
||||
|
||||
var message = Loc.GetString(buckleUid == user
|
||||
? "buckle-component-cannot-fit-message"
|
||||
: "buckle-component-other-cannot-fit-message",
|
||||
if (popup)
|
||||
{
|
||||
var message = Loc.GetString(buckleUid == user
|
||||
? "buckle-component-cannot-buckle-message"
|
||||
: "buckle-component-other-cannot-buckle-message",
|
||||
("owner", Identity.Entity(buckleUid, EntityManager)));
|
||||
|
||||
_popup.PopupEntity(message, user.Value, user.Value);
|
||||
_popup.PopupClient(message, user);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ using Content.Shared.Rotation;
|
||||
using Content.Shared.Standing;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -18,7 +17,6 @@ namespace Content.Shared.Buckle;
|
||||
|
||||
public abstract partial class SharedBuckleSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
|
||||
|
||||
@@ -1256,6 +1256,13 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<float> AtmosHeatScale =
|
||||
CVarDef.Create("atmos.heat_scale", 8f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum explosion radius for explosions caused by bursting a gas tank ("max caps").
|
||||
/// Setting this to zero disables the explosion but still allows the tank to burst and leak.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> AtmosTankFragment =
|
||||
CVarDef.Create("atmos.max_explosion_range", 26f, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* MIDI instruments
|
||||
*/
|
||||
@@ -1572,6 +1579,18 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<float> EmergencyShuttleDockTime =
|
||||
CVarDef.Create("shuttle.emergency_dock_time", 180f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// If the emergency shuttle can't dock at a priority port, the dock time will be multiplied with this value.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> EmergencyShuttleDockTimeMultiplierOtherDock =
|
||||
CVarDef.Create("shuttle.emergency_dock_time_multiplier_other_dock", 1.6667f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// If the emergency shuttle can't dock at all, the dock time will be multiplied with this value.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> EmergencyShuttleDockTimeMultiplierNoDock =
|
||||
CVarDef.Create("shuttle.emergency_dock_time_multiplier_no_dock", 2f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// How long after the console is authorized for the shuttle to early launch.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Chemistry.Components;
|
||||
@@ -88,6 +90,14 @@ public sealed partial class InjectorComponent : Component
|
||||
[DataField]
|
||||
public InjectorToggleMode ToggleState = InjectorToggleMode.Draw;
|
||||
|
||||
/// <summary>
|
||||
/// Reagents that are allowed to be within this injector.
|
||||
/// If a solution has both allowed and non-allowed reagents, only allowed reagents will be drawn into this injector.
|
||||
/// A null ReagentWhitelist indicates all reagents are allowed.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<ReagentPrototype>>? ReagentWhitelist = null;
|
||||
|
||||
#region Arguments for injection doafter
|
||||
|
||||
/// <inheritdoc cref=DoAfterArgs.NeedHand>
|
||||
|
||||
@@ -612,7 +612,7 @@ namespace Content.Shared.Chemistry.Components
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Splits a solution without the specified reagent prototypes.
|
||||
/// Splits a solution with only the specified reagent prototypes.
|
||||
/// </summary>
|
||||
public Solution SplitSolutionWithOnly(FixedPoint2 toTake, params string[] includedPrototypes)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Damage.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for an effect that nullifies <see cref="SlowOnDamageComponent"/> and adds an alert.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SlowOnDamageSystem))]
|
||||
public sealed partial class IgnoreSlowOnDamageComponent : Component;
|
||||
@@ -22,6 +22,10 @@ namespace Content.Shared.Damage
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ClothingGotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<ClothingSlowOnDamageModifierComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
|
||||
|
||||
SubscribeLocalEvent<IgnoreSlowOnDamageComponent, ComponentStartup>(OnIgnoreStartup);
|
||||
SubscribeLocalEvent<IgnoreSlowOnDamageComponent, ComponentShutdown>(OnIgnoreShutdown);
|
||||
SubscribeLocalEvent<IgnoreSlowOnDamageComponent, ModifySlowOnDamageSpeedEvent>(OnIgnoreModifySpeed);
|
||||
}
|
||||
|
||||
private void OnRefreshMovespeed(EntityUid uid, SlowOnDamageComponent component, RefreshMovementSpeedModifiersEvent args)
|
||||
@@ -84,6 +88,21 @@ namespace Content.Shared.Damage
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(args.Wearer);
|
||||
}
|
||||
|
||||
private void OnIgnoreStartup(Entity<IgnoreSlowOnDamageComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent);
|
||||
}
|
||||
|
||||
private void OnIgnoreShutdown(Entity<IgnoreSlowOnDamageComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(ent);
|
||||
}
|
||||
|
||||
private void OnIgnoreModifySpeed(Entity<IgnoreSlowOnDamageComponent> ent, ref ModifySlowOnDamageSpeedEvent args)
|
||||
{
|
||||
args.Speed = 1f;
|
||||
}
|
||||
}
|
||||
|
||||
[ByRefEvent]
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed partial class AirlockComponent : Component
|
||||
/// <summary>
|
||||
/// Whether the airlock should auto close. This value is reset every time the airlock closes.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool AutoClose = true;
|
||||
|
||||
/// <summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user