Merge remote-tracking branch 'upstream/master' into ed-17-09-2024-upstream2

# Conflicts:
#	Content.Server/GameTicking/GameTicker.Spawning.cs
This commit is contained in:
Ed
2024-09-17 11:54:36 +03:00
561 changed files with 55148 additions and 10009 deletions

View File

@@ -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

View File

@@ -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;
}

View 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);
}
}

View 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";
}
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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>

View File

@@ -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 };
}
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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()

View 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();
}
}

View File

@@ -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>

View File

@@ -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();
};
}
}
}

View File

@@ -4,7 +4,7 @@
SeparationOverride="4">
<EntityPrototypeView
Name="ItemPrototype"
Margin="4 4"
Margin="4 0 0 0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MinSize="32 32"

View File

@@ -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" />

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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.

View File

@@ -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]

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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,

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Dragon;
[RegisterComponent]
public sealed partial class DragonRuleComponent : Component
{
}

View File

@@ -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) =>

View 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";
}
}

View 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();
}
}

View 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";
}
}

View 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";
}
}

View 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";
}
}

View 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";
}
}

View 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();
}
}

View File

@@ -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;

View File

@@ -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}"))

View File

@@ -0,0 +1,4 @@
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent]
public sealed partial class DragonRuleComponent : Component;

View 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;
}
}

View File

@@ -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);
}

View File

@@ -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);

View 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);
}
}

View File

@@ -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);
}

View File

@@ -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 = "";
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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))

View File

@@ -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));

View File

@@ -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);

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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,
}
}

View File

@@ -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);
}
}

View File

@@ -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>>();

View File

@@ -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))

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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)),

View File

@@ -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)
{

View File

@@ -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)
{

View File

@@ -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)

View File

@@ -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))

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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!;

View File

@@ -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>

View File

@@ -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>

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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]

View File

@@ -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