diff --git a/Content.Client/DeviceNetwork/JammerSystem.cs b/Content.Client/DeviceNetwork/JammerSystem.cs new file mode 100644 index 0000000000..c7dbf8c8fe --- /dev/null +++ b/Content.Client/DeviceNetwork/JammerSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.Radio.EntitySystems; + +namespace Content.Client.DeviceNetwork; + +public sealed class JammerSystem : SharedJammerSystem +{ + +} diff --git a/Content.Client/DoAfter/DoAfterOverlay.cs b/Content.Client/DoAfter/DoAfterOverlay.cs index 2e23dd44ca..45981159f0 100644 --- a/Content.Client/DoAfter/DoAfterOverlay.cs +++ b/Content.Client/DoAfter/DoAfterOverlay.cs @@ -21,7 +21,7 @@ public sealed class DoAfterOverlay : Overlay private readonly ProgressColorSystem _progressColor; private readonly Texture _barTexture; - private readonly ShaderInstance _shader; + private readonly ShaderInstance _unshadedShader; /// /// Flash time for cancelled DoAfters @@ -45,7 +45,7 @@ public sealed class DoAfterOverlay : Overlay var sprite = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/progress_bar.rsi"), "icon"); _barTexture = _entManager.EntitySysManager.GetEntitySystem().Frame0(sprite); - _shader = protoManager.Index("unshaded").Instance(); + _unshadedShader = protoManager.Index("unshaded").Instance(); } protected override void Draw(in OverlayDrawArgs args) @@ -58,7 +58,6 @@ public sealed class DoAfterOverlay : Overlay const float scale = 1f; var scaleMatrix = Matrix3.CreateScale(new Vector2(scale, scale)); var rotationMatrix = Matrix3.CreateRotation(-rotation); - handle.UseShader(_shader); var curTime = _timing.CurTime; @@ -79,6 +78,13 @@ public sealed class DoAfterOverlay : Overlay if (!bounds.Contains(worldPosition)) continue; + // shades the do-after bar if the do-after bar belongs to other players + // does not shade do-afters belonging to the local player + if (uid != localEnt) + handle.UseShader(null); + else + handle.UseShader(_unshadedShader); + // If the entity is paused, we will draw the do-after as it was when the entity got paused. var meta = metaQuery.GetComponent(uid); var time = meta.EntityPaused diff --git a/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs b/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs new file mode 100644 index 0000000000..480da6ad8d --- /dev/null +++ b/Content.Client/Ghost/Commands/ToggleGhostVisibilityCommand.cs @@ -0,0 +1,26 @@ +using Robust.Shared.Console; + +namespace Content.Client.Ghost.Commands; + +public sealed class ToggleGhostVisibilityCommand : IConsoleCommand +{ + [Dependency] private readonly IEntitySystemManager _entSysMan = default!; + + public string Command => "toggleghostvisibility"; + public string Description => "Toggles ghost visibility on the client."; + public string Help => "toggleghostvisibility [bool]"; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + var ghostSystem = _entSysMan.GetEntitySystem(); + + if (args.Length != 0 && bool.TryParse(args[0], out var visibility)) + { + ghostSystem.ToggleGhostVisibility(visibility); + } + else + { + ghostSystem.ToggleGhostVisibility(); + } + } +} diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs index c42e7cd0e0..94872a58ef 100644 --- a/Content.Client/Ghost/GhostSystem.cs +++ b/Content.Client/Ghost/GhostSystem.cs @@ -3,7 +3,6 @@ using Content.Shared.Actions; using Content.Shared.Ghost; using Robust.Client.Console; using Robust.Client.GameObjects; -using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Player; @@ -177,9 +176,9 @@ namespace Content.Client.Ghost _console.RemoteExecuteCommand(null, "ghostroles"); } - public void ToggleGhostVisibility() + public void ToggleGhostVisibility(bool? visibility = null) { - GhostVisibility = !GhostVisibility; + GhostVisibility = visibility ?? !GhostVisibility; } } } diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 5bae35da5b..6eb5dd9ec9 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -108,8 +108,11 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem /// This should not be used if the entity is owned by the server. The server will otherwise /// override this with the appearance data it sends over. /// - public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null) + public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null) { + if (profile == null) + return; + if (!Resolve(uid, ref humanoid)) { return; diff --git a/Content.Client/Implants/UI/ImplanterStatusControl.cs b/Content.Client/Implants/UI/ImplanterStatusControl.cs index f3f0cdea7d..e2ffabd17d 100644 --- a/Content.Client/Implants/UI/ImplanterStatusControl.cs +++ b/Content.Client/Implants/UI/ImplanterStatusControl.cs @@ -1,5 +1,6 @@ using Content.Client.Message; using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; using Content.Shared.Implants.Components; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -17,7 +18,7 @@ public sealed class ImplanterStatusControl : Control _parent = parent; _label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } }; _label.MaxWidth = 350; - AddChild(_label); + AddChild(new ClipControl { Children = { _label } }); Update(); } @@ -42,17 +43,12 @@ public sealed class ImplanterStatusControl : Control _ => Loc.GetString("injector-invalid-injector-toggle-mode") }; - var (implantName, implantDescription) = _parent.ImplanterSlot.HasItem switch - { - false => (Loc.GetString("implanter-empty-text"), ""), - true => (_parent.ImplantData.Item1, _parent.ImplantData.Item2), - }; - + var implantName = _parent.ImplanterSlot.HasItem + ? _parent.ImplantData.Item1 + : Loc.GetString("implanter-empty-text"); _label.SetMarkup(Loc.GetString("implanter-label", ("implantName", implantName), - ("implantDescription", implantDescription), - ("modeString", modeStringLocalized), - ("lineBreak", "\n"))); + ("modeString", modeStringLocalized))); } } diff --git a/Content.Client/Popups/PopupSystem.cs b/Content.Client/Popups/PopupSystem.cs index 3faa392e58..1ef8dfba2d 100644 --- a/Content.Client/Popups/PopupSystem.cs +++ b/Content.Client/Popups/PopupSystem.cs @@ -5,7 +5,6 @@ using Content.Shared.Popups; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Player; -using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Shared.Configuration; using Robust.Shared.Map; @@ -163,6 +162,15 @@ namespace Content.Client.Popups PopupEntity(message, uid, type); } + public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small) + { + if (recipient == null) + return; + + if (_timing.IsFirstTimePredicted) + PopupCursor(message, recipient.Value, type); + } + public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) { if (recipient == null) @@ -172,6 +180,15 @@ namespace Content.Client.Popups PopupEntity(message, uid, recipient.Value, type); } + public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small) + { + if (recipient == null) + return; + + if (_timing.IsFirstTimePredicted) + PopupCoordinates(message, coordinates, recipient.Value, type); + } + public override void PopupEntity(string? message, EntityUid uid, PopupType type = PopupType.Small) { if (TryComp(uid, out TransformComponent? transform)) diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index 8708ed3019..8aa55ca864 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -136,6 +136,8 @@ namespace Content.Client.Stylesheets public const string StyleClassPowerStateGood = "PowerStateGood"; public const string StyleClassItemStatus = "ItemStatus"; + public const string StyleClassItemStatusNotHeld = "ItemStatusNotHeld"; + public static readonly Color ItemStatusNotHeldColor = Color.Gray; //Background public const string StyleClassBackgroundBaseDark = "PanelBackgroundBaseDark"; @@ -1234,6 +1236,11 @@ namespace Content.Client.Stylesheets new StyleProperty("font", notoSans10), }), + Element() + .Class(StyleClassItemStatusNotHeld) + .Prop("font", notoSansItalic10) + .Prop("font-color", ItemStatusNotHeldColor), + Element() .Class(StyleClassItemStatus) .Prop(nameof(RichTextLabel.LineHeightScale), 0.7f) diff --git a/Content.Client/UserInterface/Controls/ClipControl.cs b/Content.Client/UserInterface/Controls/ClipControl.cs new file mode 100644 index 0000000000..1fca3c0f47 --- /dev/null +++ b/Content.Client/UserInterface/Controls/ClipControl.cs @@ -0,0 +1,55 @@ +using System.Numerics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; + +namespace Content.Client.UserInterface.Controls; + +/// +/// Pretends to child controls that there's infinite space. +/// This can be used to make something like a clip instead of wrapping. +/// +public sealed class ClipControl : Control +{ + private bool _clipHorizontal = true; + private bool _clipVertical = true; + + public bool ClipHorizontal + { + get => _clipHorizontal; + set + { + _clipHorizontal = value; + InvalidateMeasure(); + } + } + + public bool ClipVertical + { + get => _clipVertical; + set + { + _clipVertical = value; + InvalidateMeasure(); + } + } + + protected override Vector2 MeasureOverride(Vector2 availableSize) + { + if (ClipHorizontal) + availableSize = availableSize with { X = float.PositiveInfinity }; + if (ClipVertical) + availableSize = availableSize with { Y = float.PositiveInfinity }; + + return base.MeasureOverride(availableSize); + } + + protected override Vector2 ArrangeOverride(Vector2 finalSize) + { + foreach (var child in Children) + { + child.Arrange(UIBox2.FromDimensions(Vector2.Zero, child.DesiredSize)); + } + + return finalSize; + } +} diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs index 09663ba82c..c79f0f80f9 100644 --- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs +++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs @@ -200,7 +200,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged + SetHeight="60"/> + SetHeight="60"/> (); hotbarController.Setup(HandContainer, StoragePanel); @@ -29,9 +29,15 @@ public sealed partial class HotbarGui : UIWidget StatusPanelRight.Update(entity); } - public void SetHighlightHand(HandLocation? hand) + public void SetHighlightHand(HandUILocation? hand) { - StatusPanelLeft.UpdateHighlight(hand is HandLocation.Left); - StatusPanelRight.UpdateHighlight(hand is HandLocation.Middle or HandLocation.Right); + StatusPanelLeft.UpdateHighlight(hand is HandUILocation.Left); + StatusPanelRight.UpdateHighlight(hand is HandUILocation.Right); + } + + public void UpdateStatusVisibility(bool left, bool right) + { + StatusPanelLeft.Visible = left; + StatusPanelRight.Visible = right; } } diff --git a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml index 81142d64d2..3b1257b44c 100644 --- a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml +++ b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml @@ -4,25 +4,26 @@ xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client" VerticalAlignment="Bottom" HorizontalAlignment="Center"> - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs index e1fe6ab246..95951fa1b0 100644 --- a/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs +++ b/Content.Client/UserInterface/Systems/Inventory/Controls/ItemStatusPanel.xaml.cs @@ -1,17 +1,13 @@ -using System.Numerics; using Content.Client.Items; -using Content.Client.Resources; using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; using Content.Shared.Inventory.VirtualItem; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Timing; using Robust.Shared.Utility; -using static Content.Client.IoC.StaticIoC; namespace Content.Client.UserInterface.Systems.Inventory.Controls; @@ -23,17 +19,15 @@ public sealed partial class ItemStatusPanel : Control [ViewVariables] private EntityUid? _entity; // Tracked so we can re-run SetSide() if the theme changes. - private HandLocation _side; + private HandUILocation _side; public ItemStatusPanel() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); - - SetSide(HandLocation.Middle); } - public void SetSide(HandLocation location) + public void SetSide(HandUILocation location) { // AN IMPORTANT REMINDER ABOUT THIS CODE: // In the UI, the RIGHT hand is on the LEFT on the screen. @@ -47,15 +41,14 @@ public sealed partial class ItemStatusPanel : Control switch (location) { - case HandLocation.Right: + case HandUILocation.Right: texture = Theme.ResolveTexture("item_status_right"); textureHighlight = Theme.ResolveTexture("item_status_right_highlight"); cutOut = StyleBox.Margin.Left; flat = StyleBox.Margin.Right; contentMargin = MarginFromThemeColor("_itemstatus_content_margin_right"); break; - case HandLocation.Middle: - case HandLocation.Left: + case HandUILocation.Left: texture = Theme.ResolveTexture("item_status_left"); textureHighlight = Theme.ResolveTexture("item_status_left_highlight"); cutOut = StyleBox.Margin.Right; @@ -104,11 +97,14 @@ public sealed partial class ItemStatusPanel : Control public void Update(EntityUid? entity) { + ItemNameLabel.Visible = entity != null; + NoItemLabel.Visible = entity == null; + if (entity == null) { + ItemNameLabel.Text = ""; ClearOldStatus(); _entity = null; - VisWrapper.Visible = false; return; } @@ -119,8 +115,6 @@ public sealed partial class ItemStatusPanel : Control UpdateItemName(); } - - VisWrapper.Visible = true; } public void UpdateHighlight(bool highlight) diff --git a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs index 492fad3872..8aea5d7ee6 100644 --- a/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs +++ b/Content.Client/Weapons/Ranged/ItemStatus/BulletRender.cs @@ -6,40 +6,10 @@ using Robust.Client.UserInterface; namespace Content.Client.Weapons.Ranged.ItemStatus; -/// -/// Renders one or more rows of bullets for item status. -/// -/// -/// This is a custom control to allow complex responsive layout logic. -/// -public sealed class BulletRender : Control +public abstract class BaseBulletRenderer : Control { - private static readonly Color ColorA = Color.FromHex("#b68f0e"); - private static readonly Color ColorB = Color.FromHex("#d7df60"); - private static readonly Color ColorGoneA = Color.FromHex("#000000"); - private static readonly Color ColorGoneB = Color.FromHex("#222222"); - - /// - /// Try to ensure there's at least this many bullets on one row. - /// - /// - /// For example, if there are two rows and the second row has only two bullets, - /// we "steal" some bullets from the row below it to make it look nicer. - /// - public const int MinCountPerRow = 7; - - public const int BulletHeight = 12; - public const int BulletSeparationNormal = 3; - public const int BulletSeparationTiny = 2; - public const int BulletWidthNormal = 5; - public const int BulletWidthTiny = 2; - public const int VerticalSeparation = 2; - - private readonly Texture _bulletTiny; - private readonly Texture _bulletNormal; - private int _capacity; - private BulletType _type = BulletType.Normal; + private LayoutParameters _params; public int Rows { get; set; } = 2; public int Count { get; set; } @@ -49,35 +19,31 @@ public sealed class BulletRender : Control get => _capacity; set { + if (_capacity == value) + return; + _capacity = value; InvalidateMeasure(); } } - public BulletType Type + protected LayoutParameters Parameters { - get => _type; + get => _params; set { - _type = value; + _params = value; InvalidateMeasure(); } } - public BulletRender() - { - var resC = IoCManager.Resolve(); - _bulletTiny = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/tiny.png"); - _bulletNormal = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/normal.png"); - } - protected override Vector2 MeasureOverride(Vector2 availableSize) { var countPerRow = Math.Min(Capacity, CountPerRow(availableSize.X)); var rows = Math.Min((int) MathF.Ceiling(Capacity / (float) countPerRow), Rows); - var height = BulletHeight * rows + (BulletSeparationNormal * rows - 1); + var height = _params.ItemHeight * rows + (_params.VerticalSeparation * rows - 1); var width = RowWidth(countPerRow); return new Vector2(width, height); @@ -91,13 +57,8 @@ public sealed class BulletRender : Control var countPerRow = CountPerRow(Size.X); - var (separation, _) = BulletParams(); - var texture = Type == BulletType.Normal ? _bulletNormal : _bulletTiny; - var pos = new Vector2(); - var altColor = false; - var spent = Capacity - Count; var bulletsDone = 0; @@ -105,7 +66,7 @@ public sealed class BulletRender : Control // Draw by rows, bottom to top. for (var row = 0; row < Rows; row++) { - altColor = false; + var altColor = false; var thisRowCount = Math.Min(countPerRow, Capacity - bulletsDone); if (thisRowCount <= 0) @@ -116,9 +77,10 @@ public sealed class BulletRender : Control // 1. The next row would have less than MinCountPerRow bullets. // 2. The next row is actually visible (we aren't the last row). // 3. MinCountPerRow is actually smaller than the count per row (avoid degenerate cases). + // 4. There's enough bullets that at least one will end up on the next row. var nextRowCount = Capacity - bulletsDone - thisRowCount; - if (nextRowCount < MinCountPerRow && row != Rows - 1 && MinCountPerRow < countPerRow) - thisRowCount -= MinCountPerRow - nextRowCount; + if (nextRowCount < _params.MinCountPerRow && row != Rows - 1 && _params.MinCountPerRow < countPerRow && nextRowCount > 0) + thisRowCount -= _params.MinCountPerRow - nextRowCount; // Account for row width to right-align. var rowWidth = RowWidth(thisRowCount); @@ -128,46 +90,130 @@ public sealed class BulletRender : Control for (var bullet = 0; bullet < thisRowCount; bullet++) { var absIdx = Capacity - bulletsDone - thisRowCount + bullet; - Color color; - if (absIdx >= spent) - color = altColor ? ColorA : ColorB; - else - color = altColor ? ColorGoneA : ColorGoneB; var renderPos = pos; - renderPos.Y = Size.Y - renderPos.Y - BulletHeight; - handle.DrawTexture(texture, renderPos, color); - pos.X += separation; + renderPos.Y = Size.Y - renderPos.Y - _params.ItemHeight; + + DrawItem(handle, renderPos, absIdx < spent, altColor); + + pos.X += _params.ItemSeparation; altColor ^= true; } bulletsDone += thisRowCount; pos.X = 0; - pos.Y += BulletHeight + VerticalSeparation; + pos.Y += _params.ItemHeight + _params.VerticalSeparation; } } + protected abstract void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor); + private int CountPerRow(float width) { - var (separation, bulletWidth) = BulletParams(); - return (int) ((width - bulletWidth + separation) / separation); - } - - private (int separation, int width) BulletParams() - { - return Type switch - { - BulletType.Normal => (BulletSeparationNormal, BulletWidthNormal), - BulletType.Tiny => (BulletSeparationTiny, BulletWidthTiny), - _ => throw new ArgumentOutOfRangeException() - }; + return (int) ((width - _params.ItemWidth + _params.ItemSeparation) / _params.ItemSeparation); } private int RowWidth(int count) { - var (separation, bulletWidth) = BulletParams(); + return (count - 1) * _params.ItemSeparation + _params.ItemWidth; + } - return (count - 1) * separation + bulletWidth; + protected struct LayoutParameters + { + public int ItemHeight; + public int ItemSeparation; + public int ItemWidth; + public int VerticalSeparation; + + /// + /// Try to ensure there's at least this many bullets on one row. + /// + /// + /// For example, if there are two rows and the second row has only two bullets, + /// we "steal" some bullets from the row below it to make it look nicer. + /// + public int MinCountPerRow; + } +} + +/// +/// Renders one or more rows of bullets for item status. +/// +/// +/// This is a custom control to allow complex responsive layout logic. +/// +public sealed class BulletRender : BaseBulletRenderer +{ + public const int MinCountPerRow = 7; + + public const int BulletHeight = 12; + public const int VerticalSeparation = 2; + + private static readonly LayoutParameters LayoutNormal = new LayoutParameters + { + ItemHeight = BulletHeight, + ItemSeparation = 3, + ItemWidth = 5, + VerticalSeparation = VerticalSeparation, + MinCountPerRow = MinCountPerRow + }; + + private static readonly LayoutParameters LayoutTiny = new LayoutParameters + { + ItemHeight = BulletHeight, + ItemSeparation = 2, + ItemWidth = 2, + VerticalSeparation = VerticalSeparation, + MinCountPerRow = MinCountPerRow + }; + + private static readonly Color ColorA = Color.FromHex("#b68f0e"); + private static readonly Color ColorB = Color.FromHex("#d7df60"); + private static readonly Color ColorGoneA = Color.FromHex("#000000"); + private static readonly Color ColorGoneB = Color.FromHex("#222222"); + + private readonly Texture _bulletTiny; + private readonly Texture _bulletNormal; + + private BulletType _type = BulletType.Normal; + + public BulletType Type + { + get => _type; + set + { + if (_type == value) + return; + + Parameters = _type switch + { + BulletType.Normal => LayoutNormal, + BulletType.Tiny => LayoutTiny, + _ => throw new ArgumentOutOfRangeException() + }; + + _type = value; + } + } + + public BulletRender() + { + var resC = IoCManager.Resolve(); + _bulletTiny = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/tiny.png"); + _bulletNormal = resC.GetTexture("/Textures/Interface/ItemStatus/Bullets/normal.png"); + Parameters = LayoutNormal; + } + + protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor) + { + Color color; + if (spent) + color = altColor ? ColorGoneA : ColorGoneB; + else + color = altColor ? ColorA : ColorB; + + var texture = _type == BulletType.Tiny ? _bulletTiny : _bulletNormal; + handle.DrawTexture(texture, renderPos, color); } public enum BulletType @@ -176,3 +222,31 @@ public sealed class BulletRender : Control Tiny } } + +public sealed class BatteryBulletRenderer : BaseBulletRenderer +{ + private static readonly Color ItemColor = Color.FromHex("#E00000"); + private static readonly Color ItemColorGone = Color.Black; + + private const int SizeH = 10; + private const int SizeV = 10; + private const int Separation = 4; + + public BatteryBulletRenderer() + { + Parameters = new LayoutParameters + { + ItemWidth = SizeH, + ItemHeight = SizeV, + ItemSeparation = SizeH + Separation, + MinCountPerRow = 3, + VerticalSeparation = Separation + }; + } + + protected override void DrawItem(DrawingHandleScreen handle, Vector2 renderPos, bool spent, bool altColor) + { + var color = spent ? ItemColorGone : ItemColor; + handle.DrawRect(UIBox2.FromDimensions(renderPos, new Vector2(SizeH, SizeV)), color); + } +} diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs index cc7405b047..84eaa9af1b 100644 --- a/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs +++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.AmmoCounter.cs @@ -116,7 +116,7 @@ public sealed partial class GunSystem public sealed class BoxesStatusControl : Control { - private readonly BoxContainer _bulletsList; + private readonly BatteryBulletRenderer _bullets; private readonly Label _ammoCount; public BoxesStatusControl() @@ -128,27 +128,18 @@ public sealed partial class GunSystem AddChild(new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal, - HorizontalExpand = true, Children = { - new Control + (_bullets = new BatteryBulletRenderer { - HorizontalExpand = true, - Children = - { - (_bulletsList = new BoxContainer - { - Orientation = BoxContainer.LayoutOrientation.Horizontal, - VerticalAlignment = VAlignment.Center, - SeparationOverride = 4 - }), - } - }, - new Control() { MinSize = new Vector2(5, 0) }, + Margin = new Thickness(0, 0, 5, 0), + HorizontalExpand = true + }), (_ammoCount = new Label { StyleClasses = { StyleNano.StyleClassItemStatus }, HorizontalAlignment = HAlignment.Right, + VerticalAlignment = VAlignment.Bottom }), } }); @@ -156,46 +147,12 @@ public sealed partial class GunSystem public void Update(int count, int max) { - _bulletsList.RemoveAllChildren(); - _ammoCount.Visible = true; _ammoCount.Text = $"x{count:00}"; - max = Math.Min(max, 8); - FillBulletRow(_bulletsList, count, max); - } - private static void FillBulletRow(Control container, int count, int capacity) - { - var colorGone = Color.FromHex("#000000"); - var color = Color.FromHex("#E00000"); - - // Draw the empty ones - for (var i = count; i < capacity; i++) - { - container.AddChild(new PanelContainer - { - PanelOverride = new StyleBoxFlat() - { - BackgroundColor = colorGone, - }, - MinSize = new Vector2(10, 15), - }); - } - - // Draw the full ones, but limit the count to the capacity - count = Math.Min(count, capacity); - for (var i = 0; i < count; i++) - { - container.AddChild(new PanelContainer - { - PanelOverride = new StyleBoxFlat() - { - BackgroundColor = color, - }, - MinSize = new Vector2(10, 15), - }); - } + _bullets.Capacity = max; + _bullets.Count = count; } } diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml index 15cc7a82ff..29f4a54847 100644 --- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml +++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml @@ -22,11 +22,6 @@ ToolTip="{Loc 'analysis-console-print-tooltip-info'}"> - - + + diff --git a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs index 2acf35da25..59cf34944c 100644 --- a/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs +++ b/Content.Client/Xenoarchaeology/Ui/AnalysisConsoleMenu.xaml.cs @@ -73,6 +73,10 @@ public sealed partial class AnalysisConsoleMenu : FancyWindow { ScanButton.Disabled = !state.CanScan; PrintButton.Disabled = !state.CanPrint; + if (state.IsTraversalDown) + DownBiasButton.Pressed = true; + else + UpBiasButton.Pressed = true; var disabled = !state.ServerConnected || !state.CanScan || state.PointAmount <= 0; diff --git a/Content.IntegrationTests/Pair/TestPair.Timing.cs b/Content.IntegrationTests/Pair/TestPair.Timing.cs index 3487ea6801..e0859660d4 100644 --- a/Content.IntegrationTests/Pair/TestPair.Timing.cs +++ b/Content.IntegrationTests/Pair/TestPair.Timing.cs @@ -1,5 +1,4 @@ #nullable enable -using Robust.Shared.Timing; namespace Content.IntegrationTests.Pair; @@ -19,6 +18,22 @@ public sealed partial class TestPair } } + /// + /// Convert a time interval to some number of ticks. + /// + public int SecondsToTicks(float seconds) + { + return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds); + } + + /// + /// Run the server & client in sync for some amount of time + /// + public async Task RunSeconds(float seconds) + { + await RunTicksSync(SecondsToTicks(seconds)); + } + /// /// Runs the server-client pair in sync, but also ensures they are both idle each tick. /// @@ -59,4 +74,4 @@ public sealed partial class TestPair delta = cTick - sTick; Assert.That(delta, Is.EqualTo(targetDelta)); } -} \ No newline at end of file +} diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs index 327ec627f5..d39c7284d0 100644 --- a/Content.IntegrationTests/PoolManager.Cvars.cs +++ b/Content.IntegrationTests/PoolManager.Cvars.cs @@ -32,6 +32,7 @@ public static partial class PoolManager (CCVars.GameLobbyEnabled.Name, "false"), (CCVars.ConfigPresetDevelopment.Name, "false"), (CCVars.AdminLogsEnabled.Name, "false"), + (CCVars.AutosaveEnabled.Name, "false"), (CVars.NetBufferSize.Name, "0") }; diff --git a/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs new file mode 100644 index 0000000000..5bada98a3a --- /dev/null +++ b/Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs @@ -0,0 +1,203 @@ +#nullable enable +using System.Linq; +using Content.Server.Body.Components; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Presets; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Mind; +using Content.Server.Pinpointer; +using Content.Server.Roles; +using Content.Server.Shuttles.Components; +using Content.Server.Station.Components; +using Content.Shared.CCVar; +using Content.Shared.Damage; +using Content.Shared.FixedPoint; +using Content.Shared.GameTicking; +using Content.Shared.Hands.Components; +using Content.Shared.Inventory; +using Content.Shared.NPC.Systems; +using Content.Shared.NukeOps; +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Map.Components; + +namespace Content.IntegrationTests.Tests.GameRules; + +[TestFixture] +public sealed class NukeOpsTest +{ + /// + /// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded. + /// + [Test] + public async Task TryStopNukeOpsFromConstantlyFailing() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings + { + Dirty = true, + DummyTicker = false, + Connected = true, + InLobby = true + }); + + var server = pair.Server; + var client = pair.Client; + var entMan = server.EntMan; + var mapSys = server.System(); + var ticker = server.System(); + var mindSys = server.System(); + var roleSys = server.System(); + var invSys = server.System(); + var factionSys = server.System(); + + Assert.That(server.CfgMan.GetCVar(CCVars.GridFill), Is.False); + server.CfgMan.SetCVar(CCVars.GridFill, true); + + // Initially in the lobby + Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); + Assert.That(client.AttachedEntity, Is.Null); + Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.NotReadyToPlay)); + + // There are no grids or maps + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + + // And no nukie related components + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + Assert.That(entMan.Count(), Is.Zero); + + // Ready up and start nukeops + await pair.WaitClientCommand("toggleready True"); + Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.ReadyToPlay)); + await pair.WaitCommand("forcepreset Nukeops"); + await pair.RunTicksSync(10); + + // Game should have started + Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.InRound)); + Assert.That(ticker.PlayerGameStatuses[client.User!.Value], Is.EqualTo(PlayerGameStatus.JoinedGame)); + Assert.That(client.EntMan.EntityExists(client.AttachedEntity)); + var player = pair.Player!.AttachedEntity!.Value; + Assert.That(entMan.EntityExists(player)); + + // Maps now exist + Assert.That(entMan.Count(), Is.GreaterThan(0)); + Assert.That(entMan.Count(), Is.GreaterThan(0)); + Assert.That(entMan.Count(), Is.EqualTo(2)); // The main station & nukie station + Assert.That(entMan.Count(), Is.GreaterThan(3)); // Each station has at least 1 grid, plus some shuttles + Assert.That(entMan.Count(), Is.EqualTo(1)); + + // And we now have nukie related components + Assert.That(entMan.Count(), Is.EqualTo(1)); + Assert.That(entMan.Count(), Is.EqualTo(1)); + Assert.That(entMan.Count(), Is.EqualTo(1)); + Assert.That(entMan.Count(), Is.EqualTo(1)); + + // The player entity should be the nukie commander + var mind = mindSys.GetMind(player)!.Value; + Assert.That(entMan.HasComponent(player)); + Assert.That(roleSys.MindIsAntagonist(mind)); + Assert.That(roleSys.MindHasRole(mind)); + Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True); + Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False); + + var roles = roleSys.MindGetAllRoles(mind); + var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander" && x.Component is NukeopsRoleComponent); + Assert.That(cmdRoles.Count(), Is.EqualTo(1)); + + // The game rule exists, and all the stations/shuttles/maps are properly initialized + var rule = entMan.AllComponents().Single().Component; + var mapRule = entMan.AllComponents().Single().Component; + foreach (var grid in mapRule.MapGrids) + { + Assert.That(entMan.EntityExists(grid)); + Assert.That(entMan.HasComponent(grid)); + Assert.That(entMan.HasComponent(grid)); + } + Assert.That(entMan.EntityExists(rule.TargetStation)); + + Assert.That(entMan.HasComponent(rule.TargetStation)); + + var nukieShuttlEnt = entMan.AllComponents().FirstOrDefault().Uid; + Assert.That(entMan.EntityExists(nukieShuttlEnt)); + + EntityUid? nukieStationEnt = null; + foreach (var grid in mapRule.MapGrids) + { + if (entMan.HasComponent(grid)) + { + nukieStationEnt = grid; + break; + } + } + + Assert.That(entMan.EntityExists(nukieStationEnt)); + var nukieStation = entMan.GetComponent(nukieStationEnt!.Value); + + Assert.That(entMan.EntityExists(nukieStation.Station)); + Assert.That(nukieStation.Station, Is.Not.EqualTo(rule.TargetStation)); + + Assert.That(server.MapMan.MapExists(mapRule.Map)); + var nukieMap = mapSys.GetMap(mapRule.Map!.Value); + + var targetStation = entMan.GetComponent(rule.TargetStation!.Value); + var targetGrid = targetStation.Grids.First(); + var targetMap = entMan.GetComponent(targetGrid).MapUid!.Value; + Assert.That(targetMap, Is.Not.EqualTo(nukieMap)); + + Assert.That(entMan.GetComponent(player).MapUid, Is.EqualTo(nukieMap)); + Assert.That(entMan.GetComponent(nukieStationEnt.Value).MapUid, Is.EqualTo(nukieMap)); + Assert.That(entMan.GetComponent(nukieShuttlEnt).MapUid, Is.EqualTo(nukieMap)); + + // The maps are all map-initialized, including the player + // Yes, this is necessary as this has repeatedly been broken somehow. + Assert.That(mapSys.IsInitialized(nukieMap)); + Assert.That(mapSys.IsInitialized(targetMap)); + Assert.That(mapSys.IsPaused(nukieMap), Is.False); + Assert.That(mapSys.IsPaused(targetMap), Is.False); + + EntityLifeStage LifeStage(EntityUid? uid) => entMan.GetComponent(uid!.Value).EntityLifeStage; + Assert.That(LifeStage(player), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(nukieMap), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(targetMap), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(nukieStationEnt.Value), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(nukieShuttlEnt), Is.GreaterThan(EntityLifeStage.Initialized)); + Assert.That(LifeStage(rule.TargetStation), Is.GreaterThan(EntityLifeStage.Initialized)); + + // Make sure the player has hands. We've had fucking disarmed nukies before. + Assert.That(entMan.HasComponent(player)); + Assert.That(entMan.GetComponent(player).Hands.Count, Is.GreaterThan(0)); + + // While we're at it, lets make sure they aren't naked. I don't know how many inventory slots all mobs will be + // likely to have in the future. But nukies should probably have at least 3 slots with something in them. + var enumerator = invSys.GetSlotEnumerator(player); + int total = 0; + while (enumerator.NextItem(out _)) + { + total++; + } + Assert.That(total, Is.GreaterThan(3)); + + // Finally lets check the nukie commander passed basic training and figured out how to breathe. + var totalSeconds = 30; + var totalTicks = (int) Math.Ceiling(totalSeconds / server.Timing.TickPeriod.TotalSeconds); + int increment = 5; + var resp = entMan.GetComponent(player); + var damage = entMan.GetComponent(player); + for (var tick = 0; tick < totalTicks; tick += increment) + { + await pair.RunTicksSync(increment); + Assert.That(resp.SuffocationCycles, Is.LessThanOrEqualTo(resp.SuffocationCycleThreshold)); + Assert.That(damage.TotalDamage, Is.EqualTo(FixedPoint2.Zero)); + } + + ticker.SetGamePreset((GamePresetPrototype?)null); + server.CfgMan.SetCVar(CCVars.GridFill, false); + await pair.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs index 1e3f9c9854..20a157e33e 100644 --- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs @@ -1,5 +1,6 @@ using Content.Server.GameTicking; using Content.Server.GameTicking.Commands; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Shared.CCVar; @@ -19,6 +20,9 @@ namespace Content.IntegrationTests.Tests.GameRules await using var pair = await PoolManager.GetServerClient(new PoolSettings { InLobby = true }); var server = pair.Server; + Assert.That(server.EntMan.Count(), Is.Zero); + Assert.That(server.EntMan.Count(), Is.Zero); + var entityManager = server.ResolveDependency(); var sGameTicker = server.ResolveDependency().GetEntitySystem(); var sGameTiming = server.ResolveDependency(); @@ -26,6 +30,9 @@ namespace Content.IntegrationTests.Tests.GameRules sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity); Assert.That(entityManager.TryGetComponent(ruleEntity, out var maxTime)); + Assert.That(server.EntMan.Count(), Is.EqualTo(1)); + Assert.That(server.EntMan.Count(), Is.EqualTo(1)); + await server.WaitAssertion(() => { Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.PreRoundLobby)); @@ -33,6 +40,9 @@ namespace Content.IntegrationTests.Tests.GameRules sGameTicker.StartRound(); }); + Assert.That(server.EntMan.Count(), Is.EqualTo(1)); + Assert.That(server.EntMan.Count(), Is.EqualTo(1)); + await server.WaitAssertion(() => { Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound)); diff --git a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs index 0f665a63de..5d7ae8efbf 100644 --- a/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs +++ b/Content.IntegrationTests/Tests/GameRules/SecretStartsTest.cs @@ -17,6 +17,7 @@ public sealed class SecretStartsTest var server = pair.Server; await server.WaitIdleAsync(); + var entMan = server.ResolveDependency(); var gameTicker = server.ResolveDependency().GetEntitySystem(); await server.WaitAssertion(() => @@ -32,10 +33,7 @@ public sealed class SecretStartsTest await server.WaitAssertion(() => { - foreach (var rule in gameTicker.GetAddedGameRules()) - { - Assert.That(gameTicker.GetActiveGameRules(), Does.Contain(rule)); - } + Assert.That(gameTicker.GetAddedGameRules().Count(), Is.GreaterThan(1), $"No additional rules started by secret rule."); // End all rules gameTicker.ClearGameRules(); diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index 480fd9cde6..d45290c866 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -767,14 +767,9 @@ public abstract partial class InteractionTest await Pair.RunTicksSync(ticks); } - protected int SecondsToTicks(float seconds) - { - return (int) Math.Ceiling(seconds / TickPeriod); - } - protected async Task RunSeconds(float seconds) { - await RunTicks(SecondsToTicks(seconds)); + await Pair.RunSeconds(seconds); } #endregion diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index a4ed31e998..42f64b344c 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -12,7 +12,6 @@ using Content.Shared.Body.Part; using Content.Shared.DoAfter; using Content.Shared.Hands.Components; using Content.Shared.Interaction; -using Content.Server.Item; using Content.Shared.Mind; using Content.Shared.Players; using Robust.Client.Input; diff --git a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs new file mode 100644 index 0000000000..30724b50a6 --- /dev/null +++ b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using System.Linq; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; +using Robust.Shared.Reflection; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.IntegrationTests.Tests.Linter; + +/// +/// Verify that the yaml linter successfully validates static fields +/// +[TestFixture] +public sealed class StaticFieldValidationTest +{ + [Test] + public async Task TestStaticFieldValidation() + { + await using var pair = await PoolManager.GetServerClient(); + var protoMan = pair.Server.ProtoMan; + + var protos = new Dictionary>(); + foreach (var kind in protoMan.EnumeratePrototypeKinds()) + { + var ids = protoMan.EnumeratePrototypes(kind).Select(x => x.ID).ToHashSet(); + protos.Add(kind, ids); + } + + Assert.That(protoMan.ValidateStaticFields(typeof(StringValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetValid), protos).Count, Is.Zero); + Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayValid), protos).Count, Is.Zero); + + Assert.That(protoMan.ValidateStaticFields(typeof(StringInvalid), protos).Count, Is.EqualTo(1)); + Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayInvalid), protos).Count, Is.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdInvalid), protos).Count, Is.EqualTo(1)); + Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayInvalid), protos).Count, Is.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestInvalid), protos).Count, Is.EqualTo(1)); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayInvalid), protos).Count, Is.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListInvalid), protos).Count, Is.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetInvalid), protos).Count, Is.EqualTo(2)); + Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayInvalid), protos).Count, Is.EqualTo(2)); + + await pair.CleanReturnAsync(); + } + + [TestPrototypes] + private const string TestPrototypes = @" +- type: entity + id: StaticFieldTestEnt + +- type: Tag + id: StaticFieldTestTag +"; + + [Reflect(false)] private sealed class StringValid + { + [ValidatePrototypeId] public static string Tag = "StaticFieldTestTag"; + } + + [Reflect(false)] private sealed class StringInvalid + { + [ValidatePrototypeId] public static string Tag = string.Empty; + } + + [Reflect(false)] private sealed class StringArrayValid + { + [ValidatePrototypeId] public static string[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"}; + } + + [Reflect(false)] private sealed class StringArrayInvalid + { + [ValidatePrototypeId] public static string[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty}; + } + + [Reflect(false)] private sealed class EntProtoIdValid + { + public static EntProtoId Tag = "StaticFieldTestEnt"; + } + + [Reflect(false)] private sealed class EntProtoIdInvalid + { + public static EntProtoId Tag = string.Empty; + } + + [Reflect(false)] private sealed class EntProtoIdArrayValid + { + public static EntProtoId[] Tag = {"StaticFieldTestEnt", "StaticFieldTestEnt"}; + } + + [Reflect(false)] private sealed class EntProtoIdArrayInvalid + { + public static EntProtoId[] Tag = {string.Empty, "StaticFieldTestEnt", string.Empty}; + } + + [Reflect(false)] private sealed class ProtoIdTestValid + { + public static ProtoId Tag = "StaticFieldTestTag"; + } + + [Reflect(false)] private sealed class ProtoIdTestInvalid + { + public static ProtoId Tag = string.Empty; + } + + [Reflect(false)] private sealed class ProtoIdArrayValid + { + public static ProtoId[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"}; + } + + [Reflect(false)] private sealed class ProtoIdArrayInvalid + { + public static ProtoId[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty}; + } + + [Reflect(false)] private sealed class ProtoIdListValid + { + public static List> Tag = new() {"StaticFieldTestTag", "StaticFieldTestTag"}; + } + + [Reflect(false)] private sealed class ProtoIdListInvalid + { + public static List> Tag = new() {string.Empty, "StaticFieldTestTag", string.Empty}; + } + + [Reflect(false)] private sealed class ProtoIdSetValid + { + public static HashSet> Tag = new() {"StaticFieldTestTag", "StaticFieldTestTag"}; + } + + [Reflect(false)] private sealed class ProtoIdSetInvalid + { + public static HashSet> Tag = new() {string.Empty, "StaticFieldTestTag", string.Empty, " "}; + } + + [Reflect(false)] private sealed class PrivateProtoIdArrayValid + { + private static ProtoId[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"}; + } + + [Reflect(false)] private sealed class PrivateProtoIdArrayInvalid + { + private static ProtoId[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty}; + } +} diff --git a/Content.IntegrationTests/Tests/Mapping/MappingTests.cs b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs new file mode 100644 index 0000000000..287e30eb8b --- /dev/null +++ b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs @@ -0,0 +1,102 @@ +using Robust.Server.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; + +namespace Content.IntegrationTests.Tests.Mapping; + +[TestFixture] +public sealed class MappingTests +{ + /// + /// Checks that the mapping command creates paused & uninitialized maps. + /// + [Test] + public async Task MappingTest() + { + await using var pair = await PoolManager.GetServerClient(new PoolSettings {Dirty = true, Connected = true, DummyTicker = false}); + + var server = pair.Server; + var entMan = server.EntMan; + var mapSys = server.System(); + + await pair.RunTicksSync(5); + var mapId = 1; + while (mapSys.MapExists(new(mapId))) + { + mapId++; + } + + await pair.WaitClientCommand($"mapping {mapId}"); + var map = mapSys.GetMap(new MapId(mapId)); + + var mapXform = server.Transform(map); + Assert.That(mapXform.MapUid, Is.EqualTo(map)); + Assert.That(mapXform.MapID, Is.EqualTo(new MapId(mapId))); + + var xform = server.Transform(pair.Player!.AttachedEntity!.Value); + + Assert.That(xform.MapUid, Is.EqualTo(map)); + Assert.That(mapSys.IsInitialized(map), Is.False); + Assert.That(mapSys.IsPaused(map), Is.True); + Assert.That(server.MetaData(map).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized)); + Assert.That(server.MetaData(map).EntityPaused, Is.True); + + // Spawn a new entity + EntityUid ent = default; + await server.WaitPost(() => + { + ent = entMan.Spawn(null, new MapCoordinates(default, new(mapId))); + }); + await pair.RunTicksSync(5); + Assert.That(server.MetaData(ent).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized)); + Assert.That(server.MetaData(ent).EntityPaused, Is.True); + + // Save the map + var file = $"{nameof(MappingTest)}.yml"; + await pair.WaitClientCommand($"savemap {mapId} {file}"); + + // Mapinitialize it + await pair.WaitClientCommand($"mapinit {mapId}"); + Assert.That(mapSys.IsInitialized(map), Is.True); + Assert.That(mapSys.IsPaused(map), Is.False); + Assert.That(server.MetaData(map).EntityLifeStage, Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(server.MetaData(map).EntityPaused, Is.False); + Assert.That(server.MetaData(ent).EntityLifeStage, Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(server.MetaData(ent).EntityPaused, Is.False); + + await server.WaitPost(() => entMan.DeleteEntity(map)); + + // Load the saved map + mapId++; + while (mapSys.MapExists(new(mapId))) + { + mapId++; + } + + await pair.WaitClientCommand($"mapping {mapId} {file}"); + map = mapSys.GetMap(new MapId(mapId)); + + // And it should all be paused and un-initialized + xform = server.Transform(pair.Player!.AttachedEntity!.Value); + Assert.That(xform.MapUid, Is.EqualTo(map)); + Assert.That(mapSys.IsInitialized(map), Is.False); + Assert.That(mapSys.IsPaused(map), Is.True); + Assert.That(server.MetaData(map).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized)); + Assert.That(server.MetaData(map).EntityPaused, Is.True); + + mapXform = server.Transform(map); + Assert.That(mapXform.MapUid, Is.EqualTo(map)); + Assert.That(mapXform.MapID, Is.EqualTo(new MapId(mapId))); + Assert.That(mapXform.ChildCount, Is.EqualTo(2)); + + mapXform.ChildEnumerator.MoveNext(out ent); + if (ent == pair.Player.AttachedEntity) + mapXform.ChildEnumerator.MoveNext(out ent); + + Assert.That(server.MetaData(ent).EntityLifeStage, Is.EqualTo(EntityLifeStage.Initialized)); + Assert.That(server.MetaData(ent).EntityPaused, Is.True); + + await server.WaitPost(() => entMan.DeleteEntity(map)); + await pair.CleanReturnAsync(); + } +} diff --git a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs index d527a346b2..f4e08b1dd6 100644 --- a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs +++ b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs @@ -28,10 +28,11 @@ public sealed class EvacShuttleTest // Dummy ticker tests should not have centcomm Assert.That(entMan.Count(), Is.Zero); - var shuttleEnabled = pair.Server.CfgMan.GetCVar(CCVars.EmergencyShuttleEnabled); - pair.Server.CfgMan.SetCVar(CCVars.GameMap, "Dev"); //CrystallPunk dont have Saltern - pair.Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false); + Assert.That(pair.Server.CfgMan.GetCVar(CCVars.GridFill), Is.False); pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, true); + pair.Server.CfgMan.SetCVar(CCVars.GameDummyTicker, false); + var gameMap = pair.Server.CfgMan.GetCVar(CCVars.GameMap); + pair.Server.CfgMan.SetCVar(CCVars.GameMap, "Dev"); await server.WaitPost(() => ticker.RestartRound()); await pair.RunTicksSync(25); @@ -71,6 +72,20 @@ public sealed class EvacShuttleTest Assert.That(shuttleXform.MapUid, Is.Not.Null); Assert.That(shuttleXform.MapUid, Is.EqualTo(centcommMap)); + // All of these should have been map-initialized. + var mapSys = entMan.System(); + Assert.That(mapSys.IsInitialized(centcommMap), Is.True); + Assert.That(mapSys.IsInitialized(salternXform.MapUid), Is.True); + Assert.That(mapSys.IsPaused(centcommMap), Is.False); + Assert.That(mapSys.IsPaused(salternXform.MapUid!.Value), Is.False); + + EntityLifeStage LifeStage(EntityUid uid) => entMan.GetComponent(uid).EntityLifeStage; + Assert.That(LifeStage(saltern), Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(LifeStage(shuttle), Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(LifeStage(centcomm), Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(LifeStage(centcommMap), Is.EqualTo(EntityLifeStage.MapInitialized)); + Assert.That(LifeStage(salternXform.MapUid.Value), Is.EqualTo(EntityLifeStage.MapInitialized)); + // Set up shuttle timing var evacSys = server.System(); evacSys.TransitTime = ShuttleSystem.DefaultTravelTime; // Absolute minimum transit time, so the test has to run for at least this long @@ -78,19 +93,15 @@ public sealed class EvacShuttleTest var dockTime = server.CfgMan.GetCVar(CCVars.EmergencyShuttleDockTime); server.CfgMan.SetCVar(CCVars.EmergencyShuttleDockTime, 2); - async Task RunSeconds(float seconds) - { - await pair.RunTicksSync((int) Math.Ceiling(seconds / server.Timing.TickPeriod.TotalSeconds)); - } // Call evac shuttle. await pair.WaitCommand("callshuttle 0:02"); - await RunSeconds(3); + await pair.RunSeconds(3); // Shuttle should have arrived on the station Assert.That(shuttleXform.MapUid, Is.EqualTo(salternXform.MapUid)); - await RunSeconds(2); + await pair.RunSeconds(2); // Shuttle should be FTLing back to centcomm Assert.That(entMan.Count(), Is.EqualTo(1)); @@ -101,14 +112,15 @@ public sealed class EvacShuttleTest Assert.That(shuttleXform.MapUid, Is.EqualTo(ftl.Owner)); // Shuttle should have arrived at centcomm - await RunSeconds(ShuttleSystem.DefaultTravelTime); + await pair.RunSeconds(ShuttleSystem.DefaultTravelTime); Assert.That(shuttleXform.MapUid, Is.EqualTo(centcommMap)); // Round should be ending now Assert.That(ticker.RunLevel, Is.EqualTo(GameRunLevel.PostRound)); server.CfgMan.SetCVar(CCVars.EmergencyShuttleDockTime, dockTime); - pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, shuttleEnabled); + pair.Server.CfgMan.SetCVar(CCVars.EmergencyShuttleEnabled, false); + pair.Server.CfgMan.SetCVar(CCVars.GameMap, gameMap); await pair.CleanReturnAsync(); } } diff --git a/Content.Server/Actions/ActionOnInteractSystem.cs b/Content.Server/Actions/ActionOnInteractSystem.cs index c9a5f4b5d0..eb35d41196 100644 --- a/Content.Server/Actions/ActionOnInteractSystem.cs +++ b/Content.Server/Actions/ActionOnInteractSystem.cs @@ -64,7 +64,7 @@ public sealed class ActionOnInteractSystem : EntitySystem var entOptions = GetValidActions(component.ActionEntities, args.CanReach); for (var i = entOptions.Count - 1; i >= 0; i--) { - var action = entOptions[i].Comp; + var action = entOptions[i]; if (!_actions.ValidateEntityTarget(args.User, args.Target.Value, action)) entOptions.RemoveAt(i); } @@ -88,7 +88,7 @@ public sealed class ActionOnInteractSystem : EntitySystem var options = GetValidActions(component.ActionEntities, args.CanReach); for (var i = options.Count - 1; i >= 0; i--) { - var action = options[i].Comp; + var action = options[i]; if (!_actions.ValidateWorldTarget(args.User, args.ClickLocation, action)) options.RemoveAt(i); } diff --git a/Content.Server/Administration/ServerApi.cs b/Content.Server/Administration/ServerApi.cs index 6f10ef9b47..04fd38598f 100644 --- a/Content.Server/Administration/ServerApi.cs +++ b/Content.Server/Administration/ServerApi.cs @@ -8,6 +8,7 @@ using System.Text.Json.Nodes; using System.Threading.Tasks; using Content.Server.Administration.Systems; using Content.Server.GameTicking; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Presets; using Content.Server.GameTicking.Rules.Components; using Content.Server.Maps; diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index eff97136d0..599485150a 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -1,23 +1,37 @@ -using Content.Server.GameTicking.Rules; +using Content.Server.Administration.Commands; +using Content.Server.Antag; +using Content.Server.GameTicking.Rules.Components; using Content.Server.Zombies; using Content.Shared.Administration; using Content.Shared.Database; -using Content.Shared.Humanoid; using Content.Shared.Mind.Components; +using Content.Shared.Roles; using Content.Shared.Verbs; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Server.Administration.Systems; public sealed partial class AdminVerbSystem { + [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly ZombieSystem _zombie = default!; - [Dependency] private readonly ThiefRuleSystem _thief = default!; - [Dependency] private readonly TraitorRuleSystem _traitorRule = default!; - [Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!; - [Dependency] private readonly PiratesRuleSystem _piratesRule = default!; - [Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!; + + [ValidatePrototypeId] + private const string DefaultTraitorRule = "Traitor"; + + [ValidatePrototypeId] + private const string DefaultNukeOpRule = "LoneOpsSpawn"; + + [ValidatePrototypeId] + private const string DefaultRevsRule = "Revolutionary"; + + [ValidatePrototypeId] + private const string DefaultThiefRule = "Thief"; + + [ValidatePrototypeId] + private const string PirateGearId = "PirateGear"; // All antag verbs have names so invokeverb works. private void AddAntagVerbs(GetVerbsEvent args) @@ -40,9 +54,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"), Act = () => { - // if its a monkey or mouse or something dont give uplink or objectives - var isHuman = HasComp(args.Target); - _traitorRule.MakeTraitorAdmin(args.Target, giveUplink: isHuman, giveObjectives: isHuman); + _antag.ForceMakeAntag(player, DefaultTraitorRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-traitor"), @@ -71,7 +83,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"), Act = () => { - _nukeopsRule.MakeLoneNukie(args.Target); + _antag.ForceMakeAntag(player, DefaultNukeOpRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-nuclear-operative"), @@ -85,14 +97,14 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"), Act = () => { - _piratesRule.MakePirate(args.Target); + // pirates just get an outfit because they don't really have logic associated with them + SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-pirate"), }; args.Verbs.Add(pirate); - //todo come here at some point dear lort. Verb headRev = new() { Text = Loc.GetString("admin-verb-text-make-head-rev"), @@ -100,7 +112,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"), Act = () => { - _revolutionaryRule.OnHeadRevAdmin(args.Target); + _antag.ForceMakeAntag(player, DefaultRevsRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-head-rev"), @@ -114,7 +126,7 @@ public sealed partial class AdminVerbSystem Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"), Act = () => { - _thief.AdminMakeThief(args.Target, false); //Midround add pacified is bad + _antag.ForceMakeAntag(player, DefaultThiefRule); }, Impact = LogImpact.High, Message = Loc.GetString("admin-verb-make-thief"), diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index 942882f7ae..042bac3956 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -152,7 +152,7 @@ public sealed partial class AdminVerbSystem Act = () => { // Fuck you. Burn Forever. - flammable.FireStacks = FlammableSystem.MaximumFireStacks; + flammable.FireStacks = flammable.MaximumFireStacks; _flammableSystem.Ignite(args.Target, args.User); var xform = Transform(args.Target); _popupSystem.PopupEntity(Loc.GetString("admin-smite-set-alight-self"), args.Target, diff --git a/Content.Server/Antag/AntagSelectionPlayerPool.cs b/Content.Server/Antag/AntagSelectionPlayerPool.cs new file mode 100644 index 0000000000..054292dcf9 --- /dev/null +++ b/Content.Server/Antag/AntagSelectionPlayerPool.cs @@ -0,0 +1,29 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.Server.Antag; + +public sealed class AntagSelectionPlayerPool(params List[] sessions) +{ + private readonly List> _orderedPools = sessions.ToList(); + + public bool TryPickAndTake(IRobustRandom random, [NotNullWhen(true)] out ICommonSession? session) + { + session = null; + + foreach (var pool in _orderedPools) + { + if (pool.Count == 0) + continue; + + session = random.PickAndTake(pool); + break; + } + + return session != null; + } + + public int Count => _orderedPools.Sum(p => p.Count); +} diff --git a/Content.Server/Antag/AntagSelectionSystem.API.cs b/Content.Server/Antag/AntagSelectionSystem.API.cs new file mode 100644 index 0000000000..f8ec5bcafc --- /dev/null +++ b/Content.Server/Antag/AntagSelectionSystem.API.cs @@ -0,0 +1,302 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Server.Antag.Components; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Objectives; +using Content.Shared.Chat; +using Content.Shared.Mind; +using JetBrains.Annotations; +using Robust.Shared.Audio; +using Robust.Shared.Player; + +namespace Content.Server.Antag; + +public sealed partial class AntagSelectionSystem +{ + /// + /// Tries to get the next non-filled definition based on the current amount of selected minds and other factors. + /// + public bool TryGetNextAvailableDefinition(Entity ent, + [NotNullWhen(true)] out AntagSelectionDefinition? definition) + { + definition = null; + + var totalTargetCount = GetTargetAntagCount(ent); + var mindCount = ent.Comp.SelectedMinds.Count; + if (mindCount >= totalTargetCount) + return false; + + foreach (var def in ent.Comp.Definitions) + { + var target = GetTargetAntagCount(ent, null, def); + + if (mindCount < target) + { + definition = def; + return true; + } + + mindCount -= target; + } + + return false; + } + + /// + /// Gets the number of antagonists that should be present for a given rule based on the provided pool. + /// A null pool will simply use the player count. + /// + public int GetTargetAntagCount(Entity ent, AntagSelectionPlayerPool? pool = null) + { + var count = 0; + foreach (var def in ent.Comp.Definitions) + { + count += GetTargetAntagCount(ent, pool, def); + } + + return count; + } + + /// + /// Gets the number of antagonists that should be present for a given antag definition based on the provided pool. + /// A null pool will simply use the player count. + /// + public int GetTargetAntagCount(Entity ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def) + { + var poolSize = pool?.Count ?? _playerManager.Sessions.Length; + // factor in other definitions' affect on the count. + var countOffset = 0; + foreach (var otherDef in ent.Comp.Definitions) + { + countOffset += Math.Clamp(poolSize / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio; + } + // make sure we don't double-count the current selection + countOffset -= Math.Clamp((poolSize + countOffset) / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio; + + return Math.Clamp((poolSize - countOffset) / def.PlayerRatio, def.Min, def.Max); + } + + /// + /// Returns identifiable information for all antagonists to be used in a round end summary. + /// + /// + /// A list containing, in order, the antag's mind, the session data, and the original name stored as a string. + /// + public List<(EntityUid, SessionData, string)> GetAntagIdentifiers(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return new List<(EntityUid, SessionData, string)>(); + + var output = new List<(EntityUid, SessionData, string)>(); + foreach (var (mind, name) in ent.Comp.SelectedMinds) + { + if (!TryComp(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null) + continue; + + if (!_playerManager.TryGetPlayerData(mindComp.OriginalOwnerUserId.Value, out var data)) + continue; + + output.Add((mind, data, name)); + } + return output; + } + + /// + /// Returns all the minds of antagonists. + /// + public List> GetAntagMinds(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return new(); + + var output = new List>(); + foreach (var (mind, _) in ent.Comp.SelectedMinds) + { + if (!TryComp(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null) + continue; + + output.Add((mind, mindComp)); + } + return output; + } + + /// + /// Helper specifically for + /// + public List GetAntagMindEntityUids(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return new(); + + return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList(); + } + + /// + /// Returns all the antagonists for this rule who are currently alive + /// + public IEnumerable GetAliveAntags(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + yield break; + + var minds = GetAntagMinds(ent); + foreach (var mind in minds) + { + if (_mind.IsCharacterDeadIc(mind)) + continue; + + if (mind.Comp.OriginalOwnedEntity != null) + yield return GetEntity(mind.Comp.OriginalOwnedEntity.Value); + } + } + + /// + /// Returns the number of alive antagonists for this rule. + /// + public int GetAliveAntagCount(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return 0; + + var numbah = 0; + var minds = GetAntagMinds(ent); + foreach (var mind in minds) + { + if (_mind.IsCharacterDeadIc(mind)) + continue; + + numbah++; + } + + return numbah; + } + + /// + /// Returns if there are any remaining antagonists alive for this rule. + /// + public bool AnyAliveAntags(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + return GetAliveAntags(ent).Any(); + } + + /// + /// Checks if all the antagonists for this rule are alive. + /// + public bool AllAntagsAlive(Entity ent) + { + if (!Resolve(ent, ref ent.Comp, false)) + return false; + + return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count; + } + + /// + /// Helper method to send the briefing text and sound to a player entity + /// + /// The entity chosen to be antag + /// The briefing text to send + /// The color the briefing should be, null for default + /// The sound to briefing/greeting sound to play + public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) + { + if (!_mind.TryGetMind(entity, out _, out var mindComponent)) + return; + + if (mindComponent.Session == null) + return; + + SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound); + } + + /// + /// Helper method to send the briefing text and sound to a list of sessions + /// + /// The sessions that will be sent the briefing + /// The briefing text to send + /// The color the briefing should be, null for default + /// The sound to briefing/greeting sound to play + [PublicAPI] + public void SendBriefing(List sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) + { + foreach (var session in sessions) + { + SendBriefing(session, briefing, briefingColor, briefingSound); + } + } + + /// + /// Helper method to send the briefing text and sound to a session + /// + /// The player chosen to be an antag + /// The briefing data + public void SendBriefing( + ICommonSession? session, + BriefingData? data) + { + if (session == null || data == null) + return; + + var text = data.Value.Text == null ? string.Empty : Loc.GetString(data.Value.Text); + SendBriefing(session, text, data.Value.Color, data.Value.Sound); + } + + /// + /// Helper method to send the briefing text and sound to a session + /// + /// The player chosen to be an antag + /// The briefing text to send + /// The color the briefing should be, null for default + /// The sound to briefing/greeting sound to play + public void SendBriefing( + ICommonSession? session, + string briefing, + Color? briefingColor, + SoundSpecifier? briefingSound) + { + if (session == null) + return; + + _audio.PlayGlobal(briefingSound, session); + if (!string.IsNullOrEmpty(briefing)) + { + var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing)); + _chat.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, + briefingColor); + } + } + + /// + /// This technically is a gamerule-ent-less way to make an entity an antag. + /// You should almost never be using this. + /// + public void ForceMakeAntag(ICommonSession? player, string defaultRule) where T : Component + { + var rule = ForceGetGameRuleEnt(defaultRule); + + if (!TryGetNextAvailableDefinition(rule, out var def)) + def = rule.Comp.Definitions.Last(); + MakeAntag(rule, player, def.Value); + } + + /// + /// Tries to grab one of the weird specific antag gamerule ents or starts a new one. + /// This is gross code but also most of this is pretty gross to begin with. + /// + public Entity ForceGetGameRuleEnt(string id) where T : Component + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var comp)) + { + return (uid, comp); + } + var ruleEnt = GameTicker.AddGameRule(id); + RemComp(ruleEnt); + var antag = Comp(ruleEnt); + antag.SelectionsComplete = true; // don't do normal selection. + GameTicker.StartGameRule(ruleEnt); + return (ruleEnt, antag); + } +} diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs index b11c562df5..eb68e077b1 100644 --- a/Content.Server/Antag/AntagSelectionSystem.cs +++ b/Content.Server/Antag/AntagSelectionSystem.cs @@ -1,347 +1,444 @@ +using System.Linq; +using Content.Server.Antag.Components; +using Content.Server.Chat.Managers; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.Ghost.Roles; +using Content.Server.Ghost.Roles.Components; using Content.Server.Mind; using Content.Server.Preferences.Managers; +using Content.Server.Roles; using Content.Server.Roles.Jobs; using Content.Server.Shuttles.Components; +using Content.Server.Station.Systems; using Content.Shared.Antag; +using Content.Shared.Ghost; using Content.Shared.Humanoid; using Content.Shared.Players; using Content.Shared.Preferences; -using Content.Shared.Roles; using Robust.Server.Audio; -using Robust.Shared.Audio; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using System.Linq; -using Content.Shared.Chat; +using Robust.Server.GameObjects; +using Robust.Server.Player; using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Player; +using Robust.Shared.Random; namespace Content.Server.Antag; -public sealed class AntagSelectionSystem : GameRuleSystem +public sealed partial class AntagSelectionSystem : GameRuleSystem { - [Dependency] private readonly IServerPreferencesManager _prefs = default!; - [Dependency] private readonly AudioSystem _audioSystem = default!; + [Dependency] private readonly IChatManager _chat = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IServerPreferencesManager _pref = default!; + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly GhostRoleSystem _ghostRole = default!; [Dependency] private readonly JobSystem _jobs = default!; - [Dependency] private readonly MindSystem _mindSystem = default!; - [Dependency] private readonly SharedRoleSystem _roleSystem = default!; + [Dependency] private readonly MapSystem _map = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly RoleSystem _role = default!; + [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; + [Dependency] private readonly TransformSystem _transform = default!; - #region Eligible Player Selection - /// - /// Get all players that are eligible for an antag role - /// - /// All sessions from which to select eligible players - /// The prototype to get eligible players for - /// Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included - /// Should players already selected as antags be eligible - /// Should we ignore if the player has enabled this specific role - /// A custom condition that each player is tested against, if it returns true the player is excluded from eligibility - /// List of all player entities that match the requirements - public List GetEligiblePlayers(IEnumerable playerSessions, - ProtoId antagPrototype, - bool includeAllJobs = false, - AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive, - bool ignorePreferences = false, - bool allowNonHumanoids = false, - Func? customExcludeCondition = null) + // arbitrary random number to give late joining some mild interest. + public const float LateJoinRandomChance = 0.5f; + + /// + public override void Initialize() { - var eligiblePlayers = new List(); + base.Initialize(); - foreach (var player in playerSessions) + SubscribeLocalEvent(OnTakeGhostRole); + + SubscribeLocalEvent(OnPlayerSpawning); + SubscribeLocalEvent(OnJobsAssigned); + SubscribeLocalEvent(OnSpawnComplete); + } + + private void OnTakeGhostRole(Entity ent, ref TakeGhostRoleEvent args) + { + if (args.TookRole) + return; + + if (ent.Comp.Rule is not { } rule || ent.Comp.Definition is not { } def) + return; + + if (!Exists(rule) || !TryComp(rule, out var select)) + return; + + MakeAntag((rule, select), args.Player, def, ignoreSpawner: true); + args.TookRole = true; + _ghostRole.UnregisterGhostRole((ent, Comp(ent))); + } + + private void OnPlayerSpawning(RulePlayerSpawningEvent args) + { + var pool = args.PlayerPool; + + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out var comp, out _)) { - if (IsPlayerEligible(player, antagPrototype, includeAllJobs, acceptableAntags, ignorePreferences, allowNonHumanoids, customExcludeCondition)) - eligiblePlayers.Add(player.AttachedEntity!.Value); - } - - return eligiblePlayers; - } - - /// - /// Get all sessions that are eligible for an antag role, can be run prior to sessions being attached to an entity - /// This does not exclude sessions that have already been chosen as antags - that must be handled manually - /// - /// All sessions from which to select eligible players - /// The prototype to get eligible players for - /// Should we ignore if the player has enabled this specific role - /// List of all player sessions that match the requirements - public List GetEligibleSessions(IEnumerable playerSessions, ProtoId antagPrototype, bool ignorePreferences = false) - { - var eligibleSessions = new List(); - - foreach (var session in playerSessions) - { - if (IsSessionEligible(session, antagPrototype, ignorePreferences)) - eligibleSessions.Add(session); - } - - return eligibleSessions; - } - - /// - /// Test eligibility of the player for a specific antag role - /// - /// The player session to test - /// The prototype to get eligible players for - /// Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included - /// Should players already selected as antags be eligible - /// Should we ignore if the player has enabled this specific role - /// A function, accepting an EntityUid and returning bool. Each player is tested against this, returning truw will exclude the player from eligibility - /// True if the player session matches the requirements, false otherwise - public bool IsPlayerEligible(ICommonSession session, - ProtoId antagPrototype, - bool includeAllJobs = false, - AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive, - bool ignorePreferences = false, - bool allowNonHumanoids = false, - Func? customExcludeCondition = null) - { - if (!IsSessionEligible(session, antagPrototype, ignorePreferences)) - return false; - - //Ensure the player has a mind - if (session.GetMind() is not { } playerMind) - return false; - - //Ensure the player has an attached entity - if (session.AttachedEntity is not { } playerEntity) - return false; - - //Ignore latejoined players, ie those on the arrivals station - if (HasComp(playerEntity)) - return false; - - //Exclude jobs that cannot be antag, unless explicitly allowed - if (!includeAllJobs && !_jobs.CanBeAntag(session)) - return false; - - //Check if the entity is already an antag - switch (acceptableAntags) - { - //If we dont want to select any antag roles - case AntagAcceptability.None: - { - if (_roleSystem.MindIsAntagonist(playerMind)) - return false; - break; - } - //If we dont want to select exclusive antag roles - case AntagAcceptability.NotExclusive: - { - if (_roleSystem.MindIsExclusiveAntagonist(playerMind)) - return false; - break; - } - } - - //Unless explictly allowed, ignore non humanoids (eg pets) - if (!allowNonHumanoids && !HasComp(playerEntity)) - return false; - - //If a custom condition was provided, test it and exclude the player if it returns true - if (customExcludeCondition != null && customExcludeCondition(playerEntity)) - return false; - - - return true; - } - - /// - /// Check if the session is eligible for a role, can be run prior to the session being attached to an entity - /// - /// Player session to check - /// Which antag prototype to check for - /// Ignore if the player has enabled this antag - /// True if the session matches the requirements, false otherwise - public bool IsSessionEligible(ICommonSession session, ProtoId antagPrototype, bool ignorePreferences = false) - { - //Exclude disconnected or zombie sessions - //No point giving antag roles to them - if (session.Status == SessionStatus.Disconnected || - session.Status == SessionStatus.Zombie) - return false; - - //Check the player has this antag preference selected - //Unless we are ignoring preferences, in which case add them anyway - var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(session.UserId).SelectedCharacter; - if (!pref.AntagPreferences.Contains(antagPrototype.Id) && !ignorePreferences) - return false; - - return true; - } - #endregion - - /// - /// Helper method to calculate the number of antags to select based upon the number of players - /// - /// How many players there are on the server - /// How many players should there be for an additional antag - /// Maximum number of antags allowed - /// The number of antags that should be chosen - public int CalculateAntagCount(int playerCount, int playersPerAntag, int maxAntags) - { - return Math.Clamp(playerCount / playersPerAntag, 1, maxAntags); - } - - #region Antag Selection - /// - /// Selects a set number of entities from several lists, prioritising the first list till its empty, then second list etc - /// - /// Array of lists, which are chosen from in order until the correct number of items are selected - /// How many items to select - /// Up to the specified count of elements from all provided lists - public List ChooseAntags(int count, params List[] eligiblePlayerLists) - { - var chosenPlayers = new List(); - foreach (var playerList in eligiblePlayerLists) - { - //Remove all chosen players from this list, to prevent duplicates - foreach (var chosenPlayer in chosenPlayers) - { - playerList.Remove(chosenPlayer); - } - - //If we have reached the desired number of players, skip - if (chosenPlayers.Count >= count) + if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn) continue; - //Pick and choose a random number of players from this list - chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList)); + if (comp.SelectionsComplete) + return; + + ChooseAntags((uid, comp), pool); + comp.SelectionsComplete = true; + + foreach (var session in comp.SelectedSessions) + { + args.PlayerPool.Remove(session); + GameTicker.PlayerJoinGame(session); + } } - return chosenPlayers; } - /// - /// Helper method to choose antags from a list - /// - /// List of eligible players - /// How many to choose - /// Up to the specified count of elements from the provided list - public List ChooseAntags(int count, List eligiblePlayers) + + private void OnJobsAssigned(RulePlayerJobsAssignedEvent args) { - var chosenPlayers = new List(); + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out var comp, out _)) + { + if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn) + continue; + + if (comp.SelectionsComplete) + continue; + + ChooseAntags((uid, comp)); + comp.SelectionsComplete = true; + } + } + + private void OnSpawnComplete(PlayerSpawnCompleteEvent args) + { + if (!args.LateJoin) + return; + + // TODO: this really doesn't handle multiple latejoin definitions well + // eventually this should probably store the players per definition with some kind of unique identifier. + // something to figure out later. + + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out var antag, out _)) + { + if (!RobustRandom.Prob(LateJoinRandomChance)) + continue; + + if (!antag.Definitions.Any(p => p.LateJoinAdditional)) + continue; + + if (!TryGetNextAvailableDefinition((uid, antag), out var def)) + continue; + + MakeAntag((uid, antag), args.Player, def.Value); + } + } + + protected override void Added(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) + { + base.Added(uid, component, gameRule, args); + + for (var i = 0; i < component.Definitions.Count; i++) + { + var def = component.Definitions[i]; + + if (def.MinRange != null) + { + def.Min = def.MinRange.Value.Next(RobustRandom); + } + + if (def.MaxRange != null) + { + def.Max = def.MaxRange.Value.Next(RobustRandom); + } + } + } + + protected override void Started(EntityUid uid, AntagSelectionComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + + if (component.SelectionsComplete) + return; + + if (GameTicker.RunLevel != GameRunLevel.InRound) + return; + + if (GameTicker.RunLevel == GameRunLevel.InRound && component.SelectionTime == AntagSelectionTime.PrePlayerSpawn) + return; + + ChooseAntags((uid, component)); + component.SelectionsComplete = true; + } + + /// + /// Chooses antagonists from the current selection of players + /// + public void ChooseAntags(Entity ent) + { + var sessions = _playerManager.Sessions.ToList(); + ChooseAntags(ent, sessions); + } + + /// + /// Chooses antagonists from the given selection of players + /// + public void ChooseAntags(Entity ent, List pool) + { + foreach (var def in ent.Comp.Definitions) + { + ChooseAntags(ent, pool, def); + } + } + + /// + /// Chooses antagonists from the given selection of players for the given antag definition. + /// + public void ChooseAntags(Entity ent, List pool, AntagSelectionDefinition def) + { + var playerPool = GetPlayerPool(ent, pool, def); + var count = GetTargetAntagCount(ent, playerPool, def); for (var i = 0; i < count; i++) { - if (eligiblePlayers.Count == 0) - break; - - chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers)); - } - - return chosenPlayers; - } - - /// - /// Selects a set number of sessions from several lists, prioritising the first list till its empty, then second list etc - /// - /// Array of lists, which are chosen from in order until the correct number of items are selected - /// How many items to select - /// Up to the specified count of elements from all provided lists - public List ChooseAntags(int count, params List[] eligiblePlayerLists) - { - var chosenPlayers = new List(); - foreach (var playerList in eligiblePlayerLists) - { - //Remove all chosen players from this list, to prevent duplicates - foreach (var chosenPlayer in chosenPlayers) + var session = (ICommonSession?) null; + if (def.PickPlayer) { - playerList.Remove(chosenPlayer); + if (!playerPool.TryPickAndTake(RobustRandom, out session)) + break; + + if (ent.Comp.SelectedSessions.Contains(session)) + continue; } - //If we have reached the desired number of players, skip - if (chosenPlayers.Count >= count) - continue; - - //Pick and choose a random number of players from this list - chosenPlayers.AddRange(ChooseAntags(count - chosenPlayers.Count, playerList)); + MakeAntag(ent, session, def); } - return chosenPlayers; } - /// - /// Helper method to choose sessions from a list - /// - /// List of eligible sessions - /// How many to choose - /// Up to the specified count of elements from the provided list - public List ChooseAntags(int count, List eligiblePlayers) - { - var chosenPlayers = new List(); - for (int i = 0; i < count; i++) + /// + /// Makes a given player into the specified antagonist. + /// + public void MakeAntag(Entity ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false) + { + var antagEnt = (EntityUid?) null; + var isSpawner = false; + + if (session != null) { - if (eligiblePlayers.Count == 0) - break; + ent.Comp.SelectedSessions.Add(session); - chosenPlayers.Add(RobustRandom.PickAndTake(eligiblePlayers)); + // we shouldn't be blocking the entity if they're just a ghost or smth. + if (!HasComp(session.AttachedEntity)) + antagEnt = session.AttachedEntity; } - - return chosenPlayers; - } - #endregion - - #region Briefings - /// - /// Helper method to send the briefing text and sound to a list of entities - /// - /// The players chosen to be antags - /// The briefing text to send - /// The color the briefing should be, null for default - /// The sound to briefing/greeting sound to play - public void SendBriefing(List entities, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) - { - foreach (var entity in entities) + else if (!ignoreSpawner && def.SpawnerPrototype != null) // don't add spawners if we have a player, dummy. { - SendBriefing(entity, briefing, briefingColor, briefingSound); + antagEnt = Spawn(def.SpawnerPrototype); + isSpawner = true; } - } - /// - /// Helper method to send the briefing text and sound to a player entity - /// - /// The entity chosen to be antag - /// The briefing text to send - /// The color the briefing should be, null for default - /// The sound to briefing/greeting sound to play - public void SendBriefing(EntityUid entity, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) - { - if (!_mindSystem.TryGetMind(entity, out _, out var mindComponent)) + if (!antagEnt.HasValue) + { + var getEntEv = new AntagSelectEntityEvent(session, ent); + RaiseLocalEvent(ent, ref getEntEv, true); + + if (!getEntEv.Handled) + { + throw new InvalidOperationException($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player."); + } + + antagEnt = getEntEv.Entity; + } + + if (antagEnt is not { } player) return; - if (mindComponent.Session == null) - return; + var getPosEv = new AntagSelectLocationEvent(session, ent); + RaiseLocalEvent(ent, ref getPosEv, true); + if (getPosEv.Handled) + { + var playerXform = Transform(player); + var pos = RobustRandom.Pick(getPosEv.Coordinates); + var mapEnt = _map.GetMap(pos.MapId); + _transform.SetMapCoordinates((player, playerXform), pos); + } - SendBriefing(mindComponent.Session, briefing, briefingColor, briefingSound); + if (isSpawner) + { + if (!TryComp(player, out var spawnerComp)) + { + Log.Error("Antag spawner with GhostRoleAntagSpawnerComponent."); + return; + } + + spawnerComp.Rule = ent; + spawnerComp.Definition = def; + return; + } + + EntityManager.AddComponents(player, def.Components); + _stationSpawning.EquipStartingGear(player, def.StartingGear); + + if (session != null) + { + var curMind = session.GetMind(); + if (curMind == null) + { + curMind = _mind.CreateMind(session.UserId, Name(antagEnt.Value)); + _mind.SetUserId(curMind.Value, session.UserId); + } + + EntityManager.AddComponents(curMind.Value, def.MindComponents); + _mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true); + ent.Comp.SelectedMinds.Add((curMind.Value, Name(player))); + } + + if (def.Briefing is { } briefing) + { + SendBriefing(session, briefing); + } + + var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def); + RaiseLocalEvent(ent, ref afterEv, true); } /// - /// Helper method to send the briefing text and sound to a list of sessions + /// Gets an ordered player pool based on player preferences and the antagonist definition. /// - /// - /// - /// - /// - - public void SendBriefing(List sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) + public AntagSelectionPlayerPool GetPlayerPool(Entity ent, List sessions, AntagSelectionDefinition def) { + var primaryList = new List(); + var secondaryList = new List(); + var fallbackList = new List(); + var rawList = new List(); foreach (var session in sessions) { - SendBriefing(session, briefing, briefingColor, briefingSound); - } - } - /// - /// Helper method to send the briefing text and sound to a session - /// - /// The player chosen to be an antag - /// The briefing text to send - /// The color the briefing should be, null for default - /// The sound to briefing/greeting sound to play + if (!IsSessionValid(ent, session, def) || + !IsEntityValid(session.AttachedEntity, def)) + { + rawList.Add(session); + continue; + } - public void SendBriefing(ICommonSession session, string briefing, Color? briefingColor, SoundSpecifier? briefingSound) - { - _audioSystem.PlayGlobal(briefingSound, session); - var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", briefing)); - ChatManager.ChatMessageToOne(ChatChannel.Server, briefing, wrappedMessage, default, false, session.Channel, briefingColor); + var pref = (HumanoidCharacterProfile) _pref.GetPreferences(session.UserId).SelectedCharacter; + if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.PrefRoles.Contains(p))) + { + primaryList.Add(session); + } + else if (def.PrefRoles.Count == 0 || pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p))) + { + secondaryList.Add(session); + } + else + { + fallbackList.Add(session); + } + } + + return new AntagSelectionPlayerPool(primaryList, secondaryList, fallbackList, rawList); + } + + /// + /// Checks if a given session is valid for an antagonist. + /// + public bool IsSessionValid(Entity ent, ICommonSession session, AntagSelectionDefinition def, EntityUid? mind = null) + { + mind ??= session.GetMind(); + + if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie) + return false; + + if (ent.Comp.SelectedSessions.Contains(session)) + return false; + + //todo: we need some way to check that we're not getting the same role twice. (double picking thieves or zombies through midrounds) + + switch (def.MultiAntagSetting) + { + case AntagAcceptability.None: + { + if (_role.MindIsAntagonist(mind)) + return false; + break; + } + case AntagAcceptability.NotExclusive: + { + if (_role.MindIsExclusiveAntagonist(mind)) + return false; + break; + } + } + + // todo: expand this to allow for more fine antag-selection logic for game rules. + if (!_jobs.CanBeAntag(session)) + return false; + + return true; + } + + /// + /// Checks if a given entity (mind/session not included) is valid for a given antagonist. + /// + private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def) + { + if (entity == null) + return false; + + if (HasComp(entity)) + return false; + + if (!def.AllowNonHumans && !HasComp(entity)) + return false; + + if (def.Whitelist != null) + { + if (!def.Whitelist.IsValid(entity.Value, EntityManager)) + return false; + } + + if (def.Blacklist != null) + { + if (def.Blacklist.IsValid(entity.Value, EntityManager)) + return false; + } + + return true; } - #endregion } + +/// +/// Event raised on a game rule entity in order to determine what the antagonist entity will be. +/// Only raised if the selected player's current entity is invalid. +/// +[ByRefEvent] +public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity GameRule) +{ + public readonly ICommonSession? Session = Session; + + public bool Handled => Entity != null; + + public EntityUid? Entity; +} + +/// +/// Event raised on a game rule entity to determine the location for the antagonist. +/// +[ByRefEvent] +public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity GameRule) +{ + public readonly ICommonSession? Session = Session; + + public bool Handled => Coordinates.Any(); + + public List Coordinates = new(); +} + +/// +/// Event raised on a game rule entity after the setup logic for an antag is complete. +/// Used for applying additional more complex setup logic. +/// +[ByRefEvent] +public readonly record struct AfterAntagEntitySelectedEvent(ICommonSession? Session, EntityUid EntityUid, Entity GameRule, AntagSelectionDefinition Def); diff --git a/Content.Server/Antag/Components/AntagSelectionComponent.cs b/Content.Server/Antag/Components/AntagSelectionComponent.cs new file mode 100644 index 0000000000..096be14049 --- /dev/null +++ b/Content.Server/Antag/Components/AntagSelectionComponent.cs @@ -0,0 +1,189 @@ +using Content.Server.Administration.Systems; +using Content.Server.Destructible.Thresholds; +using Content.Shared.Antag; +using Content.Shared.Roles; +using Content.Shared.Storage; +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.Antag.Components; + +[RegisterComponent, Access(typeof(AntagSelectionSystem), typeof(AdminVerbSystem))] +public sealed partial class AntagSelectionComponent : Component +{ + /// + /// Has the primary selection of antagonists finished yet? + /// + [DataField] + public bool SelectionsComplete; + + /// + /// The definitions for the antagonists + /// + [DataField] + public List Definitions = new(); + + /// + /// The minds and original names of the players selected to be antagonists. + /// + [DataField] + public List<(EntityUid, string)> SelectedMinds = new(); + + /// + /// When the antag selection will occur. + /// + [DataField] + public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn; + + /// + /// Cached sessions of players who are chosen. Used so we don't have to rebuild the pool multiple times in a tick. + /// Is not serialized. + /// + public HashSet SelectedSessions = new(); +} + +[DataDefinition] +public partial struct AntagSelectionDefinition() +{ + /// + /// A list of antagonist roles that are used for selecting which players will be antagonists. + /// + [DataField] + public List> PrefRoles = new(); + + /// + /// Fallback for . Useful if you need multiple role preferences for a team antagonist. + /// + [DataField] + public List> FallbackRoles = new(); + + /// + /// Should we allow people who already have an antagonist role? + /// + [DataField] + public AntagAcceptability MultiAntagSetting = AntagAcceptability.None; + + /// + /// The minimum number of this antag. + /// + [DataField] + public int Min = 1; + + /// + /// The maximum number of this antag. + /// + [DataField] + public int Max = 1; + + /// + /// A range used to randomly select + /// + [DataField] + public MinMax? MinRange; + + /// + /// A range used to randomly select + /// + [DataField] + public MinMax? MaxRange; + + /// + /// a player to antag ratio: used to determine the amount of antags that will be present. + /// + [DataField] + public int PlayerRatio = 10; + + /// + /// Whether or not players should be picked to inhabit this antag or not. + /// + [DataField] + public bool PickPlayer = true; + + /// + /// If true, players that latejoin into a round have a chance of being converted into antagonists. + /// + [DataField] + public bool LateJoinAdditional = false; + + //todo: find out how to do this with minimal boilerplate: filler department, maybe? + //public HashSet> JobBlacklist = new() + + /// + /// Mostly just here for legacy compatibility and reducing boilerplate + /// + [DataField] + public bool AllowNonHumans = false; + + /// + /// A whitelist for selecting which players can become this antag. + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// A blacklist for selecting which players can become this antag. + /// + [DataField] + public EntityWhitelist? Blacklist; + + /// + /// Components added to the player. + /// + [DataField] + public ComponentRegistry Components = new(); + + /// + /// Components added to the player's mind. + /// + [DataField] + public ComponentRegistry MindComponents = new(); + + /// + /// A set of starting gear that's equipped to the player. + /// + [DataField] + public ProtoId? StartingGear; + + /// + /// A briefing shown to the player. + /// + [DataField] + public BriefingData? Briefing; + + /// + /// A spawner used to defer the selection of this particular definition. + /// + /// + /// Not the cleanest way of doing this code but it's just an odd specific behavior. + /// Sue me. + /// + [DataField] + public EntProtoId? SpawnerPrototype; +} + +/// +/// Contains data used to generate a briefing. +/// +[DataDefinition] +public partial struct BriefingData +{ + /// + /// The text shown + /// + [DataField] + public LocId? Text; + + /// + /// The color of the text. + /// + [DataField] + public Color? Color; + + /// + /// The sound played. + /// + [DataField] + public SoundSpecifier? Sound; +} diff --git a/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs b/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs new file mode 100644 index 0000000000..fcaa4d4267 --- /dev/null +++ b/Content.Server/Antag/Components/GhostRoleAntagSpawnerComponent.cs @@ -0,0 +1,14 @@ +namespace Content.Server.Antag.Components; + +/// +/// Ghost role spawner that creates an antag for the associated gamerule. +/// +[RegisterComponent, Access(typeof(AntagSelectionSystem))] +public sealed partial class GhostRoleAntagSpawnerComponent : Component +{ + [DataField] + public EntityUid? Rule; + + [DataField] + public AntagSelectionDefinition? Definition; +} diff --git a/Content.Server/Antag/MobReplacementRuleSystem.cs b/Content.Server/Antag/MobReplacementRuleSystem.cs index 2446b976e1..18837b5a7c 100644 --- a/Content.Server/Antag/MobReplacementRuleSystem.cs +++ b/Content.Server/Antag/MobReplacementRuleSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Antag.Mimic; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Shared.VendingMachines; diff --git a/Content.Server/Atmos/Components/FlammableComponent.cs b/Content.Server/Atmos/Components/FlammableComponent.cs index 49da56563b..99ae5b1b5c 100644 --- a/Content.Server/Atmos/Components/FlammableComponent.cs +++ b/Content.Server/Atmos/Components/FlammableComponent.cs @@ -11,49 +11,65 @@ namespace Content.Server.Atmos.Components [ViewVariables(VVAccess.ReadWrite)] [DataField] - public bool OnFire { get; set; } + public bool OnFire; [ViewVariables(VVAccess.ReadWrite)] [DataField] - public float FireStacks { get; set; } + public float FireStacks; [ViewVariables(VVAccess.ReadWrite)] - [DataField("fireSpread")] + [DataField] + public float MaximumFireStacks = 10f; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float MinimumFireStacks = -10f; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public string FlammableFixtureID = "flammable"; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] + public float MinIgnitionTemperature = 373.15f; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField] public bool FireSpread { get; private set; } = false; [ViewVariables(VVAccess.ReadWrite)] - [DataField("canResistFire")] + [DataField] public bool CanResistFire { get; private set; } = false; - [DataField("damage", required: true)] + [DataField(required: true)] [ViewVariables(VVAccess.ReadWrite)] public DamageSpecifier Damage = new(); // Empty by default, we don't want any funny NREs. /// /// Used for the fixture created to handle passing firestacks when two flammable objects collide. /// - [DataField("flammableCollisionShape")] + [DataField] public IPhysShape FlammableCollisionShape = new PhysShapeCircle(0.35f); /// /// Should the component be set on fire by interactions with isHot entities /// [ViewVariables(VVAccess.ReadWrite)] - [DataField("alwaysCombustible")] + [DataField] public bool AlwaysCombustible = false; /// /// Can the component anyhow lose its FireStacks? /// [ViewVariables(VVAccess.ReadWrite)] - [DataField("canExtinguish")] + [DataField] public bool CanExtinguish = true; /// /// How many firestacks should be applied to component when being set on fire? /// [ViewVariables(VVAccess.ReadWrite)] - [DataField("firestacksOnIgnite")] + [DataField] public float FirestacksOnIgnite = 2.0f; /// diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs index 3ddaa10096..3448e5036f 100644 --- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs +++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs @@ -48,12 +48,10 @@ namespace Content.Server.Atmos.EntitySystems [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly IRobustRandom _random = default!; - public const float MinimumFireStacks = -10f; - public const float MaximumFireStacks = 20f; - private const float UpdateTime = 1f; + private EntityQuery _physicsQuery; - public const float MinIgnitionTemperature = 373.15f; - public const string FlammableFixtureID = "flammable"; + // This should probably be moved to the component, requires a rewrite, all fires tick at the same time + private const float UpdateTime = 1f; private float _timer; @@ -63,6 +61,8 @@ namespace Content.Server.Atmos.EntitySystems { UpdatesAfter.Add(typeof(AtmosphereSystem)); + _physicsQuery = GetEntityQuery(); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnInteractUsing); SubscribeLocalEvent(OnCollide); @@ -131,7 +131,7 @@ namespace Content.Server.Atmos.EntitySystems if (!TryComp(uid, out var body)) return; - _fixture.TryCreateFixture(uid, component.FlammableCollisionShape, FlammableFixtureID, hard: false, + _fixture.TryCreateFixture(uid, component.FlammableCollisionShape, component.FlammableFixtureID, hard: false, collisionMask: (int) CollisionGroup.FullTileLayer, body: body); } @@ -189,7 +189,7 @@ namespace Content.Server.Atmos.EntitySystems // Normal hard collisions, though this isn't generally possible since most flammable things are mobs // which don't collide with one another, shouldn't work here. - if (args.OtherFixtureId != FlammableFixtureID && args.OurFixtureId != FlammableFixtureID) + if (args.OtherFixtureId != flammable.FlammableFixtureID && args.OurFixtureId != flammable.FlammableFixtureID) return; if (!flammable.FireSpread) @@ -204,7 +204,17 @@ namespace Content.Server.Atmos.EntitySystems if (flammable.OnFire && otherFlammable.OnFire) { // Both are on fire -> equalize fire stacks. - var avg = (flammable.FireStacks + otherFlammable.FireStacks) / 2; + // Weight each thing's firestacks by its mass + var mass1 = 1f; + var mass2 = 1f; + if (_physicsQuery.TryComp(uid, out var physics) && _physicsQuery.TryComp(otherUid, out var otherPhys)) + { + mass1 = physics.Mass; + mass2 = otherPhys.Mass; + } + + var total = mass1 + mass2; + var avg = (flammable.FireStacks * mass1 + otherFlammable.FireStacks * mass2) / total; flammable.FireStacks = flammable.CanExtinguish ? avg : Math.Max(flammable.FireStacks, avg); otherFlammable.FireStacks = otherFlammable.CanExtinguish ? avg : Math.Max(otherFlammable.FireStacks, avg); UpdateAppearance(uid, flammable); @@ -213,25 +223,24 @@ namespace Content.Server.Atmos.EntitySystems } // Only one is on fire -> attempt to spread the fire. - if (flammable.OnFire) + var (srcUid, srcFlammable, destUid, destFlammable) = flammable.OnFire + ? (uid, flammable, otherUid, otherFlammable) + : (otherUid, otherFlammable, uid, flammable); + + // if the thing on fire has less mass, spread less firestacks and vice versa + var ratio = 0.5f; + if (_physicsQuery.TryComp(srcUid, out var srcPhysics) && _physicsQuery.TryComp(destUid, out var destPhys)) { - otherFlammable.FireStacks += flammable.FireStacks / 2; - Ignite(otherUid, uid, otherFlammable); - if (flammable.CanExtinguish) - { - flammable.FireStacks /= 2; - UpdateAppearance(uid, flammable); - } + ratio *= srcPhysics.Mass / destPhys.Mass; } - else + + var lost = srcFlammable.FireStacks * ratio; + destFlammable.FireStacks += lost; + Ignite(destUid, srcUid, destFlammable); + if (srcFlammable.CanExtinguish) { - flammable.FireStacks += otherFlammable.FireStacks / 2; - Ignite(uid, otherUid, flammable); - if (otherFlammable.CanExtinguish) - { - otherFlammable.FireStacks /= 2; - UpdateAppearance(otherUid, otherFlammable); - } + srcFlammable.FireStacks -= lost; + UpdateAppearance(srcUid, srcFlammable); } } @@ -242,7 +251,7 @@ namespace Content.Server.Atmos.EntitySystems private void OnTileFire(Entity ent, ref TileFireEvent args) { - var tempDelta = args.Temperature - MinIgnitionTemperature; + var tempDelta = args.Temperature - ent.Comp.MinIgnitionTemperature; _fireEvents.TryGetValue(ent, out var maxTemp); @@ -275,7 +284,7 @@ namespace Content.Server.Atmos.EntitySystems if (!Resolve(uid, ref flammable)) return; - flammable.FireStacks = MathF.Min(MathF.Max(MinimumFireStacks, flammable.FireStacks + relativeFireStacks), MaximumFireStacks); + flammable.FireStacks = MathF.Min(MathF.Max(flammable.MinimumFireStacks, flammable.FireStacks + relativeFireStacks), flammable.MaximumFireStacks); if (flammable.OnFire && flammable.FireStacks <= 0) Extinguish(uid, flammable); @@ -443,12 +452,10 @@ namespace Content.Server.Atmos.EntitySystems EnsureComp(uid); _ignitionSourceSystem.SetIgnited(uid); - var damageScale = MathF.Min( flammable.FireStacks, 5); - if (TryComp(uid, out TemperatureComponent? temp)) - _temperatureSystem.ChangeHeat(uid, 12500 * damageScale, false, temp); + _temperatureSystem.ChangeHeat(uid, 12500 * flammable.FireStacks, false, temp); - _damageableSystem.TryChangeDamage(uid, flammable.Damage * damageScale, interruptsDoAfters: false); + _damageableSystem.TryChangeDamage(uid, flammable.Damage * flammable.FireStacks, interruptsDoAfters: false); AdjustFireStacks(uid, flammable.FirestackFade * (flammable.Resisting ? 10f : 1f), flammable); diff --git a/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs b/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs index c30db08bbe..2e3b2c2115 100644 --- a/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs +++ b/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs @@ -1,4 +1,6 @@ +using Content.Server.Station.Components; using Content.Shared.Cargo; +using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Prototypes; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; @@ -38,3 +40,17 @@ public sealed partial class StationCargoOrderDatabaseComponent : Component [DataField] public EntProtoId PrinterOutput = "PaperCargoInvoice"; } + +/// +/// Event broadcast before a cargo order is fulfilled, allowing alternate systems to fulfill the order. +/// +[ByRefEvent] +public record struct FulfillCargoOrderEvent(Entity Station, CargoOrderData Order, Entity OrderConsole) +{ + public Entity OrderConsole = OrderConsole; + public Entity Station = Station; + public CargoOrderData Order = Order; + + public EntityUid? FulfillmentEntity; + public bool Handled = false; +} diff --git a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs index 7a81e1a424..13a1d3d565 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs @@ -170,13 +170,20 @@ namespace Content.Server.Cargo.Systems return; } - var tradeDestination = TryFulfillOrder(stationData, order, orderDatabase); + var ev = new FulfillCargoOrderEvent((station.Value, stationData), order, (uid, component)); + RaiseLocalEvent(ref ev); + ev.FulfillmentEntity ??= station.Value; - if (tradeDestination == null) + if (!ev.Handled) { - ConsolePopup(args.Session, Loc.GetString("cargo-console-unfulfilled")); - PlayDenySound(uid, component); - return; + ev.FulfillmentEntity = TryFulfillOrder((station.Value, stationData), order, orderDatabase); + + if (ev.FulfillmentEntity == null) + { + ConsolePopup(args.Session, Loc.GetString("cargo-console-unfulfilled")); + PlayDenySound(uid, component); + return; + } } _idCardSystem.TryFindIdCard(player, out var idCard); @@ -186,14 +193,14 @@ namespace Content.Server.Cargo.Systems var approverName = idCard.Comp?.FullName ?? Loc.GetString("access-reader-unknown-id"); var approverJob = idCard.Comp?.JobTitle ?? Loc.GetString("access-reader-unknown-id"); - var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast", + var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast", ("productName", Loc.GetString(order.ProductName)), ("orderAmount", order.OrderQuantity), ("approverName", approverName), ("approverJob", approverJob), ("cost", cost)); _radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false); - ConsolePopup(args.Session, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(tradeDestination.Value).EntityName))); + ConsolePopup(args.Session, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(ev.FulfillmentEntity.Value).EntityName))); // Log order approval _adminLogger.Add(LogType.Action, LogImpact.Low, @@ -201,10 +208,10 @@ namespace Content.Server.Cargo.Systems orderDatabase.Orders.Remove(order); DeductFunds(bank, cost); - UpdateOrders(station.Value, orderDatabase); + UpdateOrders(station.Value); } - private EntityUid? TryFulfillOrder(StationDataComponent stationData, CargoOrderData order, StationCargoOrderDatabaseComponent orderDatabase) + private EntityUid? TryFulfillOrder(Entity stationData, CargoOrderData order, StationCargoOrderDatabaseComponent orderDatabase) { // No slots at the trade station _listEnts.Clear(); @@ -357,7 +364,7 @@ namespace Content.Server.Cargo.Systems /// Updates all of the cargo-related consoles for a particular station. /// This should be called whenever orders change. /// - private void UpdateOrders(EntityUid dbUid, StationCargoOrderDatabaseComponent _) + private void UpdateOrders(EntityUid dbUid) { // Order added so all consoles need updating. var orderQuery = AllEntityQuery(); @@ -392,7 +399,7 @@ namespace Content.Server.Cargo.Systems string description, string dest, StationCargoOrderDatabaseComponent component, - StationDataComponent stationData + Entity stationData ) { DebugTools.Assert(_protoMan.HasIndex(spawnId)); @@ -414,7 +421,7 @@ namespace Content.Server.Cargo.Systems private bool TryAddOrder(EntityUid dbUid, CargoOrderData data, StationCargoOrderDatabaseComponent component) { component.Orders.Add(data); - UpdateOrders(dbUid, component); + UpdateOrders(dbUid); return true; } @@ -432,7 +439,7 @@ namespace Content.Server.Cargo.Systems { orderDB.Orders.RemoveAt(sequenceIdx); } - UpdateOrders(dbUid, orderDB); + UpdateOrders(dbUid); } public void ClearOrders(StationCargoOrderDatabaseComponent component) diff --git a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs index 42aabf2578..f83ec1a512 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs @@ -1,9 +1,13 @@ +using System.Linq; using Content.Server.Cargo.Components; using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Server.Station.Components; using Content.Shared.Cargo; using Content.Shared.Cargo.Components; using Content.Shared.DeviceLinking; using Robust.Shared.Audio; +using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Server.Cargo.Systems; @@ -13,10 +17,44 @@ public sealed partial class CargoSystem private void InitializeTelepad() { SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnTelepadPowerChange); // Shouldn't need re-anchored event SubscribeLocalEvent(OnTelepadAnchorChange); + SubscribeLocalEvent(OnTelepadFulfillCargoOrder); } + + private void OnTelepadFulfillCargoOrder(ref FulfillCargoOrderEvent args) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var tele, out var xform)) + { + if (tele.CurrentState != CargoTelepadState.Idle) + continue; + + if (!this.IsPowered(uid, EntityManager)) + continue; + + if (_station.GetOwningStation(uid, xform) != args.Station) + continue; + + // todo cannot be fucking asked to figure out device linking rn but this shouldn't just default to the first port. + if (!TryComp(uid, out var sinkComponent) || + sinkComponent.LinkedSources.FirstOrNull() is not { } console || + console != args.OrderConsole.Owner) + continue; + + for (var i = 0; i < args.Order.OrderQuantity; i++) + { + tele.CurrentOrders.Add(args.Order); + } + tele.Accumulator = tele.Delay; + args.Handled = true; + args.FulfillmentEntity = uid; + return; + } + } + private void UpdateTelepad(float frameTime) { var query = EntityQueryEnumerator(); @@ -33,14 +71,6 @@ public sealed partial class CargoSystem continue; } - if (!TryComp(uid, out var sinkComponent) || - sinkComponent.LinkedSources.FirstOrNull() is not { } console || - !HasComp(console)) - { - comp.Accumulator = comp.Delay; - continue; - } - comp.Accumulator -= frameTime; // Uhh listen teleporting takes time and I just want the 1 float. @@ -51,21 +81,22 @@ public sealed partial class CargoSystem continue; } - var station = _station.GetOwningStation(console); - - if (!TryComp(station, out var orderDatabase) || - orderDatabase.Orders.Count == 0) + if (comp.CurrentOrders.Count == 0) { comp.Accumulator += comp.Delay; continue; } var xform = Transform(uid); - if (FulfillNextOrder(orderDatabase, xform.Coordinates, comp.PrinterOutput)) + var currentOrder = comp.CurrentOrders.First(); + if (FulfillOrder(currentOrder, xform.Coordinates, comp.PrinterOutput)) { _audio.PlayPvs(_audio.GetSound(comp.TeleportSound), uid, AudioParams.Default.WithVolume(-8f)); - UpdateOrders(station.Value, orderDatabase); + if (_station.GetOwningStation(uid) is { } station) + UpdateOrders(station); + + comp.CurrentOrders.Remove(currentOrder); comp.CurrentState = CargoTelepadState.Teleporting; _appearance.SetData(uid, CargoTelepadVisuals.State, CargoTelepadState.Teleporting, appearance); } @@ -79,6 +110,29 @@ public sealed partial class CargoSystem _linker.EnsureSinkPorts(uid, telepad.ReceiverPort); } + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Comp.CurrentOrders.Count == 0) + return; + + if (_station.GetStations().Count == 0) + return; + + if (_station.GetOwningStation(ent) is not { } station) + { + station = _random.Pick(_station.GetStations().Where(HasComp).ToList()); + } + + if (!TryComp(station, out var db) || + !TryComp(station, out var data)) + return; + + foreach (var order in ent.Comp.CurrentOrders) + { + TryFulfillOrder((station, data), order, db); + } + } + private void SetEnabled(EntityUid uid, CargoTelepadComponent component, ApcPowerReceiverComponent? receiver = null, TransformComponent? xform = null) { diff --git a/Content.Server/Chemistry/ReagentEffectConditions/TotalHunger.cs b/Content.Server/Chemistry/ReagentEffectConditions/TotalHunger.cs new file mode 100644 index 0000000000..1dd12e632a --- /dev/null +++ b/Content.Server/Chemistry/ReagentEffectConditions/TotalHunger.cs @@ -0,0 +1,35 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Nutrition.Components; +using Content.Shared.FixedPoint; +using Robust.Shared.Prototypes; + +namespace Content.Server.Chemistry.ReagentEffectConditions +{ + public sealed partial class Hunger : ReagentEffectCondition + { + [DataField] + public float Max = float.PositiveInfinity; + + [DataField] + public float Min = 0; + + public override bool Condition(ReagentEffectArgs args) + { + if (args.EntityManager.TryGetComponent(args.SolutionEntity, out HungerComponent? hunger)) + { + var total = hunger.CurrentHunger; + if (total > Min && total < Max) + return true; + } + + return false; + } + + public override string GuidebookExplanation(IPrototypeManager prototype) + { + return Loc.GetString("reagent-effect-condition-guidebook-total-hunger", + ("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max), + ("min", Min)); + } + } +} diff --git a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs index 4290726cc4..fe53ea268c 100644 --- a/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs +++ b/Content.Server/CriminalRecords/Systems/CriminalRecordsConsoleSystem.cs @@ -253,6 +253,8 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS { var name = Identity.Name(uid, EntityManager); var xform = Transform(uid); + + // TODO use the entity's station? Not the station of the map that it happens to currently be on? var station = _station.GetStationInMap(xform.MapID); if (station != null && _stationRecords.GetRecordByName(station.Value, name) is { } id) diff --git a/Content.Server/Destructible/Thresholds/MinMax.cs b/Content.Server/Destructible/Thresholds/MinMax.cs index b438e7c0e8..c44864183a 100644 --- a/Content.Server/Destructible/Thresholds/MinMax.cs +++ b/Content.Server/Destructible/Thresholds/MinMax.cs @@ -1,4 +1,6 @@ -namespace Content.Server.Destructible.Thresholds +using Robust.Shared.Random; + +namespace Content.Server.Destructible.Thresholds { [Serializable] [DataDefinition] @@ -9,5 +11,16 @@ [DataField("max")] public int Max; + + public MinMax(int min, int max) + { + Min = min; + Max = max; + } + + public int Next(IRobustRandom random) + { + return random.Next(Min, Max + 1); + } } } diff --git a/Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs b/Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs similarity index 84% rename from Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs rename to Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs index 956768bdd9..b9e6fa5d4b 100644 --- a/Content.Server/GameTicking/Rules/Components/ActiveGameRuleComponent.cs +++ b/Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Server.GameTicking.Components; /// /// Added to game rules before and removed before . diff --git a/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs b/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs new file mode 100644 index 0000000000..de4be83627 --- /dev/null +++ b/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.GameTicking.Components; + +/// +/// Generic component used to track a gamerule that's start has been delayed. +/// +[RegisterComponent, AutoGenerateComponentPause] +public sealed partial class DelayedStartRuleComponent : Component +{ + /// + /// The time at which the rule will start properly. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField] + public TimeSpan RuleStartTime; +} diff --git a/Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs b/Content.Server/GameTicking/Components/EndedGameRuleComponent.cs similarity index 81% rename from Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs rename to Content.Server/GameTicking/Components/EndedGameRuleComponent.cs index 4484abd4d0..3234bfff3a 100644 --- a/Content.Server/GameTicking/Rules/Components/EndedGameRuleComponent.cs +++ b/Content.Server/GameTicking/Components/EndedGameRuleComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Server.GameTicking.Components; /// /// Added to game rules before . diff --git a/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs b/Content.Server/GameTicking/Components/GameRuleComponent.cs similarity index 83% rename from Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs rename to Content.Server/GameTicking/Components/GameRuleComponent.cs index 6309b97402..1e6c3f0ab1 100644 --- a/Content.Server/GameTicking/Rules/Components/GameRuleComponent.cs +++ b/Content.Server/GameTicking/Components/GameRuleComponent.cs @@ -1,6 +1,7 @@ +using Content.Server.Destructible.Thresholds; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -namespace Content.Server.GameTicking.Rules.Components; +namespace Content.Server.GameTicking.Components; /// /// Component attached to all gamerule entities. @@ -20,6 +21,12 @@ public sealed partial class GameRuleComponent : Component /// [DataField] public int MinPlayers; + + /// + /// A delay for when the rule the is started and when the starting logic actually runs. + /// + [DataField] + public MinMax? Delay; } /// diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs index b97a16ab99..a1946d34a0 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -100,7 +100,7 @@ namespace Content.Server.GameTicking SetGamePreset(LobbyEnabled ? _configurationManager.GetCVar(CCVars.GameLobbyDefaultPreset) : "sandbox"); } - public void SetGamePreset(GamePresetPrototype preset, bool force = false) + public void SetGamePreset(GamePresetPrototype? preset, bool force = false) { // Do nothing if this game ticker is a dummy! if (DummyTicker) diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs index 4ebe946af4..f52a3cb296 100644 --- a/Content.Server/GameTicking/GameTicker.GameRule.cs +++ b/Content.Server/GameTicking/GameTicker.GameRule.cs @@ -1,6 +1,6 @@ using System.Linq; using Content.Server.Administration; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; using Content.Shared.Administration; using Content.Shared.Database; using Content.Shared.Prototypes; @@ -102,6 +102,22 @@ public sealed partial class GameTicker if (MetaData(ruleEntity).EntityPrototype?.ID is not { } id) // you really fucked up return false; + // If we already have it, then we just skip the delay as it has already happened. + if (!RemComp(ruleEntity) && ruleData.Delay != null) + { + var delayTime = TimeSpan.FromSeconds(ruleData.Delay.Value.Next(_robustRandom)); + + if (delayTime > TimeSpan.Zero) + { + _sawmill.Info($"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}"); + _adminLogger.Add(LogType.EventStarted, $"Queued start for game rule {ToPrettyString(ruleEntity)} with delay {delayTime}"); + + var delayed = EnsureComp(ruleEntity); + delayed.RuleStartTime = _gameTiming.CurTime + (delayTime); + return true; + } + } + _allPreviousGameRules.Add((RoundDuration(), id)); _sawmill.Info($"Started game rule {ToPrettyString(ruleEntity)}"); _adminLogger.Add(LogType.EventStarted, $"Started game rule {ToPrettyString(ruleEntity)}"); @@ -255,6 +271,18 @@ public sealed partial class GameTicker } } + private void UpdateGameRules() + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var delay, out var rule)) + { + if (_gameTiming.CurTime < delay.RuleStartTime) + continue; + + StartGameRule(uid, rule); + } + } + #region Command Implementations [AdminCommand(AdminFlags.Fun)] @@ -323,38 +351,3 @@ public sealed partial class GameTicker #endregion } - -/* -/// -/// Raised broadcast when a game rule is selected, but not started yet. -/// -public sealed class GameRuleAddedEvent -{ - public GameRulePrototype Rule { get; } - - public GameRuleAddedEvent(GameRulePrototype rule) - { - Rule = rule; - } -} - -public sealed class GameRuleStartedEvent -{ - public GameRulePrototype Rule { get; } - - public GameRuleStartedEvent(GameRulePrototype rule) - { - Rule = rule; - } -} - -public sealed class GameRuleEndedEvent -{ - public GameRulePrototype Rule { get; } - - public GameRuleEndedEvent(GameRulePrototype rule) - { - Rule = rule; - } -} -*/ diff --git a/Content.Server/GameTicking/GameTicker.RoundFlow.cs b/Content.Server/GameTicking/GameTicker.RoundFlow.cs index 202daf256d..792d838169 100644 --- a/Content.Server/GameTicking/GameTicker.RoundFlow.cs +++ b/Content.Server/GameTicking/GameTicker.RoundFlow.cs @@ -165,7 +165,7 @@ namespace Content.Server.GameTicking var gridIds = _map.LoadMap(targetMapId, ev.GameMap.MapPath.ToString(), ev.Options); - _metaData.SetEntityName(_mapManager.GetMapEntityId(targetMapId), "Station map"); + _metaData.SetEntityName(_mapManager.GetMapEntityId(targetMapId), $"station map - {map.MapName}"); var gridUids = gridIds.ToList(); RaiseLocalEvent(new PostGameMapLoad(map, targetMapId, gridUids, stationName)); diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index efda3df0ca..fa23312268 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -133,6 +133,7 @@ namespace Content.Server.GameTicking return; base.Update(frameTime); UpdateRoundFlow(frameTime); + UpdateGameRules(); } } } diff --git a/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs new file mode 100644 index 0000000000..463aecbff5 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/LoadMapRuleComponent.cs @@ -0,0 +1,29 @@ +using Content.Server.Maps; +using Content.Shared.Whitelist; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// This is used for a game rule that loads a map when activated. +/// +[RegisterComponent] +public sealed partial class LoadMapRuleComponent : Component +{ + [DataField] + public MapId? Map; + + [DataField] + public ProtoId? GameMap ; + + [DataField] + public ResPath? MapPath; + + [DataField] + public List MapGrids = new(); + + [DataField] + public EntityWhitelist? SpawnerWhitelist; +} diff --git a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs index e6966c1e37..fa352eb320 100644 --- a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs @@ -8,7 +8,7 @@ namespace Content.Server.GameTicking.Rules.Components; /// /// Stores some configuration used by the ninja system. -/// Objectives and roundend summary are handled by . +/// Objectives and roundend summary are handled by . /// [RegisterComponent, Access(typeof(SpaceNinjaSystem))] public sealed partial class NinjaRuleComponent : Component diff --git a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs index e02d90c18b..bb1b7c8746 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeOperativeSpawnerComponent.cs @@ -1,6 +1,3 @@ -using Content.Shared.Roles; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - namespace Content.Server.GameTicking.Rules.Components; /// @@ -9,11 +6,5 @@ namespace Content.Server.GameTicking.Rules.Components; /// TODO: Remove once systems can request spawns from the ghost role system directly. /// [RegisterComponent] -public sealed partial class NukeOperativeSpawnerComponent : Component -{ - [DataField("name", required:true)] - public string OperativeName = default!; +public sealed partial class NukeOperativeSpawnerComponent : Component; - [DataField] - public NukeopSpawnPreset SpawnDetails = default!; -} diff --git a/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs index 358b157cdf..3d097cd7c7 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeOpsShuttleComponent.cs @@ -6,4 +6,6 @@ [RegisterComponent] public sealed partial class NukeOpsShuttleComponent : Component { + [DataField] + public EntityUid AssociatedRule; } diff --git a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs index c66a9d12a1..e05c3e5db6 100644 --- a/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/NukeopsRuleComponent.cs @@ -1,31 +1,16 @@ -using Content.Server.Maps; using Content.Server.RoundEnd; -using Content.Server.StationEvents.Events; using Content.Shared.Dataset; using Content.Shared.NPC.Prototypes; using Content.Shared.Roles; -using Robust.Shared.Map; +using Robust.Shared.Audio; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Utility; - namespace Content.Server.GameTicking.Rules.Components; -[RegisterComponent, Access(typeof(NukeopsRuleSystem), typeof(LoneOpsSpawnRule))] +[RegisterComponent, Access(typeof(NukeopsRuleSystem))] public sealed partial class NukeopsRuleComponent : Component { - /// - /// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative - /// - [DataField] - public int PlayersPerOperative = 10; - - [DataField] - public int MaxOps = 5; - /// /// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event. /// @@ -56,12 +41,6 @@ public sealed partial class NukeopsRuleComponent : Component [DataField] public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(3); - /// - /// Whether or not to spawn the nuclear operative outpost. Used by LoneOpsSpawn event. - /// - [DataField] - public bool SpawnOutpost = true; - /// /// Whether or not nukie left their outpost /// @@ -84,7 +63,7 @@ public sealed partial class NukeopsRuleComponent : Component /// This amount of TC will be given to each nukie /// [DataField] - public int WarTCAmountPerNukie = 40; + public int WarTcAmountPerNukie = 40; /// /// Delay between war declaration and nuke ops arrival on station map. Gives crew time to prepare @@ -98,49 +77,23 @@ public sealed partial class NukeopsRuleComponent : Component [DataField] public int WarDeclarationMinOps = 4; - [DataField] - public EntProtoId SpawnPointProto = "SpawnPointNukies"; - - [DataField] - public EntProtoId GhostSpawnPointProto = "SpawnPointGhostNukeOperative"; - - [DataField] - public string OperationName = "Test Operation"; - - [DataField] - public ProtoId OutpostMapPrototype = "NukieOutpost"; - [DataField] public WinType WinType = WinType.Neutral; [DataField] public List WinConditions = new (); - public MapId? NukiePlanet; - - // TODO: use components, don't just cache entity UIDs - // There have been (and probably still are) bugs where these refer to deleted entities from old rounds. - public EntityUid? NukieOutpost; - public EntityUid? NukieShuttle; + [DataField] public EntityUid? TargetStation; + [DataField] + public ProtoId Faction = "Syndicate"; + /// - /// Data to be used in for an operative once the Mind has been added. + /// Path to antagonist alert sound. /// [DataField] - public Dictionary OperativeMindPendingData = new(); - - [DataField(required: true)] - public ProtoId Faction = default!; - - [DataField] - public NukeopSpawnPreset CommanderSpawnDetails = new() { AntagRoleProto = "NukeopsCommander", GearProto = "SyndicateCommanderGearFull", NamePrefix = "nukeops-role-commander", NameList = "SyndicateNamesElite" }; - - [DataField] - public NukeopSpawnPreset AgentSpawnDetails = new() { AntagRoleProto = "NukeopsMedic", GearProto = "SyndicateOperativeMedicFull", NamePrefix = "nukeops-role-agent", NameList = "SyndicateNamesNormal" }; - - [DataField] - public NukeopSpawnPreset OperativeSpawnDetails = new(); + public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg"); } /// diff --git a/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs deleted file mode 100644 index 1d03b41d77..0000000000 --- a/Content.Server/GameTicking/Rules/Components/PiratesRuleComponent.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Robust.Shared.Audio; - -namespace Content.Server.GameTicking.Rules.Components; - -[RegisterComponent, Access(typeof(PiratesRuleSystem))] -public sealed partial class PiratesRuleComponent : Component -{ - [ViewVariables] - public List Pirates = new(); - [ViewVariables] - public EntityUid PirateShip = EntityUid.Invalid; - [ViewVariables] - public HashSet InitialItems = new(); - [ViewVariables] - public double InitialShipValue; - - /// - /// Path to antagonist alert sound. - /// - [DataField("pirateAlertSound")] - public SoundSpecifier PirateAlertSound = new SoundPathSpecifier( - "/Audio/Ambience/Antag/pirate_start.ogg", - AudioParams.Default.WithVolume(4)); -} diff --git a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs index 2ce3f1f9a6..3b19bbffb6 100644 --- a/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/RevolutionaryRuleComponent.cs @@ -22,43 +22,6 @@ public sealed partial class RevolutionaryRuleComponent : Component [DataField] public TimeSpan TimerWait = TimeSpan.FromSeconds(20); - /// - /// Stores players minds - /// - [DataField] - public Dictionary HeadRevs = new(); - - [DataField] - public ProtoId HeadRevPrototypeId = "HeadRev"; - - /// - /// Min players needed for Revolutionary gamemode to start. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public int MinPlayers = 15; - - /// - /// Max Head Revs allowed during selection. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public int MaxHeadRevs = 3; - - /// - /// The amount of Head Revs that will spawn per this amount of players. - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public int PlayersPerHeadRev = 15; - - /// - /// The gear head revolutionaries are given on spawn. - /// - [DataField] - public List StartingGear = new() - { - "Flash", - "ClothingEyesGlassesSunglasses" - }; - /// /// The time it takes after the last head is killed for the shuttle to arrive. /// diff --git a/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs index 9dfd6e6627..01a078625a 100644 --- a/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/ThiefRuleComponent.cs @@ -1,12 +1,11 @@ using Content.Shared.Random; -using Content.Shared.Roles; using Robust.Shared.Audio; using Robust.Shared.Prototypes; namespace Content.Server.GameTicking.Rules.Components; /// -/// Stores data for . +/// Stores data for . /// [RegisterComponent, Access(typeof(ThiefRuleSystem))] public sealed partial class ThiefRuleComponent : Component @@ -23,42 +22,9 @@ public sealed partial class ThiefRuleComponent : Component [DataField] public float BigObjectiveChance = 0.7f; - /// - /// Add a Pacified comp to thieves - /// - [DataField] - public bool PacifistThieves = true; - - [DataField] - public ProtoId ThiefPrototypeId = "Thief"; - [DataField] public float MaxObjectiveDifficulty = 2.5f; [DataField] public int MaxStealObjectives = 10; - - /// - /// Things that will be given to thieves - /// - [DataField] - public List StarterItems = new() { "ToolboxThief", "ClothingHandsChameleonThief" }; - - /// - /// All Thieves created by this rule - /// - [DataField] - public List ThievesMinds = new(); - - /// - /// Max Thiefs created by rule on roundstart - /// - [DataField] - public int MaxAllowThief = 3; - - /// - /// Sound played when making the player a thief via antag control or ghost role - /// - [DataField] - public SoundSpecifier? GreetingSound = new SoundPathSpecifier("/Audio/Misc/thief_greeting.ogg"); } diff --git a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs index e904d8a7c2..ea5c9a830b 100644 --- a/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/TraitorRuleComponent.cs @@ -57,4 +57,19 @@ public sealed partial class TraitorRuleComponent : Component /// [DataField] public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg"); + + /// + /// The amount of codewords that are selected. + /// + [DataField] + public int CodewordCount = 4; + + /// + /// The amount of TC traitors start with. + /// + [DataField] + public int StartingBalance = 20; + + [DataField] + public int MaxDifficulty = 20; } diff --git a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs index 4fe91e3a5f..59d1940eaf 100644 --- a/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/ZombieRuleComponent.cs @@ -8,12 +8,6 @@ namespace Content.Server.GameTicking.Rules.Components; [RegisterComponent, Access(typeof(ZombieRuleSystem))] public sealed partial class ZombieRuleComponent : Component { - [DataField] - public Dictionary InitialInfectedNames = new(); - - [DataField] - public ProtoId PatientZeroPrototypeId = "InitialInfected"; - /// /// When the round will next check for round end. /// @@ -26,61 +20,9 @@ public sealed partial class ZombieRuleComponent : Component [DataField] public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30); - /// - /// The time at which the initial infected will be chosen. - /// - [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] - public TimeSpan? StartTime; - - /// - /// The minimum amount of time after the round starts that the initial infected will be chosen. - /// - [DataField] - public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10); - - /// - /// The maximum amount of time after the round starts that the initial infected will be chosen. - /// - [DataField] - public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15); - - /// - /// The sound that plays when someone becomes an initial infected. - /// todo: this should have a unique sound instead of reusing the zombie one. - /// - [DataField] - public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg"); - - /// - /// The minimum amount of time initial infected have before they start taking infection damage. - /// - [DataField] - public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f); - - /// - /// The maximum amount of time initial infected have before they start taking damage. - /// - [DataField] - public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f); - - /// - /// How many players for each initial infected. - /// - [DataField] - public int PlayersPerInfected = 10; - - /// - /// The maximum number of initial infected. - /// - [DataField] - public int MaxInitialInfected = 6; - /// /// After this amount of the crew become zombies, the shuttle will be automatically called. /// [DataField] public float ZombieShuttleCallPercentage = 0.7f; - - [DataField] - public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead"; } diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs index 82ac755592..78b8a8a85c 100644 --- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Administration.Commands; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.KillTracking; using Content.Server.Mind; @@ -33,7 +34,6 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem(OnSpawnComplete); SubscribeLocalEvent(OnKillReported); SubscribeLocalEvent(OnPointChanged); - SubscribeLocalEvent(OnRoundEndTextAppend); } private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev) @@ -113,21 +113,17 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem(); - while (query.MoveNext(out var uid, out var dm, out var point, out var rule)) - { - if (!GameTicker.IsGameRuleAdded(uid, rule)) - continue; + if (!TryComp(uid, out var point)) + return; - if (dm.Victor != null && _player.TryGetPlayerData(dm.Victor.Value, out var data)) - { - ev.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName))); - ev.AddLine(""); - } - ev.AddLine(Loc.GetString("point-scoreboard-header")); - ev.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup()); + if (component.Victor != null && _player.TryGetPlayerData(component.Victor.Value, out var data)) + { + args.AddLine(Loc.GetString("point-scoreboard-winner", ("player", data.UserName))); + args.AddLine(""); } + args.AddLine(Loc.GetString("point-scoreboard-header")); + args.AddLine(new FormattedMessage(point.Scoreboard).ToMarkup()); } } diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs index a60a2bfe22..4534333417 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; using Robust.Shared.Collections; @@ -15,31 +16,6 @@ public abstract partial class GameRuleSystem where T: IComponent return EntityQueryEnumerator(); } - protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out _, out _, out _, out var gameRule)) - { - var minPlayers = gameRule.MinPlayers; - if (!ev.Forced && ev.Players.Length < minPlayers) - { - ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players", - ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers), - ("presetName", localizedPresetName))); - ev.Cancel(); - continue; - } - - if (ev.Players.Length == 0) - { - ChatManager.DispatchServerAnnouncement(Loc.GetString("preset-no-one-ready")); - ev.Cancel(); - } - } - - return !ev.Cancelled; - } - /// /// Utility function for finding a random event-eligible station entity /// diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs index 363c2ad7f7..bcad146c22 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs @@ -1,6 +1,6 @@ using Content.Server.Atmos.EntitySystems; using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; using Robust.Server.GameObjects; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -22,9 +22,31 @@ public abstract partial class GameRuleSystem : EntitySystem where T : ICompon { base.Initialize(); + SubscribeLocalEvent(OnStartAttempt); SubscribeLocalEvent(OnGameRuleAdded); SubscribeLocalEvent(OnGameRuleStarted); SubscribeLocalEvent(OnGameRuleEnded); + SubscribeLocalEvent(OnRoundEndTextAppend); + } + + private void OnStartAttempt(RoundStartAttemptEvent args) + { + if (args.Forced || args.Cancelled) + return; + + var query = QueryActiveRules(); + while (query.MoveNext(out var uid, out _, out _, out var gameRule)) + { + var minPlayers = gameRule.MinPlayers; + if (args.Players.Length >= minPlayers) + continue; + + ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-players", + ("readyPlayersCount", args.Players.Length), + ("minimumPlayers", minPlayers), + ("presetName", ToPrettyString(uid)))); + args.Cancel(); + } } private void OnGameRuleAdded(EntityUid uid, T component, ref GameRuleAddedEvent args) @@ -48,6 +70,12 @@ public abstract partial class GameRuleSystem : EntitySystem where T : ICompon Ended(uid, component, ruleData, args); } + private void OnRoundEndTextAppend(Entity ent, ref RoundEndTextAppendEvent args) + { + if (!TryComp(ent, out var ruleData)) + return; + AppendRoundEndText(ent, ent, ruleData, ref args); + } /// /// Called when the gamerule is added @@ -73,6 +101,14 @@ public abstract partial class GameRuleSystem : EntitySystem where T : ICompon } + /// + /// Called at the end of a round when text needs to be added for a game rule. + /// + protected virtual void AppendRoundEndText(EntityUid uid, T component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args) + { + + } + /// /// Called on an active gamerule entity in the Update function /// diff --git a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs index b775b7af56..01fa387595 100644 --- a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs @@ -1,5 +1,6 @@ using System.Threading; using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Robust.Server.Player; using Robust.Shared.Player; diff --git a/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs b/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs index 01fd97d9a7..3da55e30c9 100644 --- a/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.KillTracking; using Content.Shared.Chat; diff --git a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs new file mode 100644 index 0000000000..aba9ed9e58 --- /dev/null +++ b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs @@ -0,0 +1,80 @@ +using Content.Server.Antag; +using Content.Server.GameTicking.Components; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Spawners.Components; +using Robust.Server.GameObjects; +using Robust.Server.Maps; +using Robust.Shared.Prototypes; + +namespace Content.Server.GameTicking.Rules; + +public sealed class LoadMapRuleSystem : GameRuleSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly MapSystem _map = default!; + [Dependency] private readonly MapLoaderSystem _mapLoader = default!; + [Dependency] private readonly TransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSelectLocation); + SubscribeLocalEvent(OnGridSplit); + } + + private void OnGridSplit(ref GridSplitEvent args) + { + var rule = QueryActiveRules(); + while (rule.MoveNext(out _, out var mapComp, out _)) + { + if (!mapComp.MapGrids.Contains(args.Grid)) + continue; + + mapComp.MapGrids.AddRange(args.NewGrids); + break; + } + } + + protected override void Added(EntityUid uid, LoadMapRuleComponent comp, GameRuleComponent rule, GameRuleAddedEvent args) + { + if (comp.Map != null) + return; + + _map.CreateMap(out var mapId); + comp.Map = mapId; + + if (comp.GameMap != null) + { + var gameMap = _prototypeManager.Index(comp.GameMap.Value); + comp.MapGrids.AddRange(GameTicker.LoadGameMap(gameMap, comp.Map.Value, new MapLoadOptions())); + } + else if (comp.MapPath != null) + { + if (_mapLoader.TryLoad(comp.Map.Value, comp.MapPath.Value.ToString(), out var roots, new MapLoadOptions { LoadMap = true })) + comp.MapGrids.AddRange(roots); + } + else + { + Log.Error($"No valid map prototype or map path associated with the rule {ToPrettyString(uid)}"); + } + } + + private void OnSelectLocation(Entity ent, ref AntagSelectLocationEvent args) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var xform)) + { + if (xform.MapID != ent.Comp.Map) + continue; + + if (xform.GridUid == null || !ent.Comp.MapGrids.Contains(xform.GridUid.Value)) + continue; + + if (ent.Comp.SpawnerWhitelist != null && !ent.Comp.SpawnerWhitelist.IsValid(uid, EntityManager)) + continue; + + args.Coordinates.Add(_transform.GetMapCoordinates(xform)); + } + } +} diff --git a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs index e792a004df..cae99fee9f 100644 --- a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs @@ -1,5 +1,6 @@ using System.Threading; using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Timer = Robust.Shared.Timing.Timer; @@ -33,6 +34,7 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem TimerFired(component), component.TimerCancel.Token); @@ -49,6 +51,7 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem GameTicker.RestartRound()); } diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index d521e26396..2f8b9dc927 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -1,31 +1,20 @@ -using Content.Server.Administration.Commands; -using Content.Server.Administration.Managers; using Content.Server.Antag; using Content.Server.Communications; using Content.Server.GameTicking.Rules.Components; -using Content.Server.Ghost.Roles.Components; -using Content.Server.Ghost.Roles.Events; using Content.Server.Humanoid; -using Content.Server.Mind; using Content.Server.Nuke; using Content.Server.NukeOps; using Content.Server.Popups; using Content.Server.Preferences.Managers; -using Content.Server.RandomMetadata; using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Shuttles.Events; using Content.Server.Shuttles.Systems; -using Content.Server.Spawners.Components; using Content.Server.Station.Components; -using Content.Server.Station.Systems; using Content.Server.Store.Components; using Content.Server.Store.Systems; -using Content.Shared.CCVar; -using Content.Shared.Dataset; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Prototypes; -using Content.Shared.Mind.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.NPC.Components; @@ -33,45 +22,30 @@ using Content.Shared.NPC.Systems; using Content.Shared.Nuke; using Content.Shared.NukeOps; using Content.Shared.Preferences; -using Content.Shared.Roles; using Content.Shared.Store; using Content.Shared.Tag; using Content.Shared.Zombies; -using Robust.Server.Player; -using Robust.Shared.Configuration; using Robust.Shared.Map; -using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; using System.Linq; +using Content.Server.GameTicking.Components; namespace Content.Server.GameTicking.Rules; public sealed class NukeopsRuleSystem : GameRuleSystem { - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IServerPreferencesManager _prefs = default!; - [Dependency] private readonly IAdminManager _adminManager = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly EmergencyShuttleSystem _emergency = default!; [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; - [Dependency] private readonly RandomMetadataSystem _randomMetadata = default!; - [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly RoundEndSystem _roundEndSystem = default!; - [Dependency] private readonly SharedRoleSystem _roles = default!; - [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; [Dependency] private readonly StoreSystem _store = default!; [Dependency] private readonly TagSystem _tag = default!; - [Dependency] private readonly AntagSelectionSystem _antagSelection = default!; - - private ISawmill _sawmill = default!; [ValidatePrototypeId] private const string TelecrystalCurrencyPrototype = "Telecrystal"; @@ -79,141 +53,67 @@ public sealed class NukeopsRuleSystem : GameRuleSystem [ValidatePrototypeId] private const string NukeOpsUplinkTagPrototype = "NukeOpsUplink"; - [ValidatePrototypeId] - public const string NukeopsId = "Nukeops"; - - [ValidatePrototypeId] - private const string OperationPrefixDataset = "operationPrefix"; - - [ValidatePrototypeId] - private const string OperationSuffixDataset = "operationSuffix"; - public override void Initialize() { base.Initialize(); - _sawmill = _logManager.GetSawmill("NukeOps"); - - SubscribeLocalEvent(OnStartAttempt); - SubscribeLocalEvent(OnPlayersSpawning); - SubscribeLocalEvent(OnRoundEndText); SubscribeLocalEvent(OnNukeExploded); SubscribeLocalEvent(OnRunLevelChanged); SubscribeLocalEvent(OnNukeDisarm); SubscribeLocalEvent(OnComponentRemove); SubscribeLocalEvent(OnMobStateChanged); - SubscribeLocalEvent(OnPlayersGhostSpawning); - SubscribeLocalEvent(OnMindAdded); SubscribeLocalEvent(OnOperativeZombified); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShuttleFTLAttempt); SubscribeLocalEvent(OnWarDeclared); SubscribeLocalEvent(OnShuttleCallAttempt); + + SubscribeLocalEvent(OnAntagSelectEntity); + SubscribeLocalEvent(OnAfterAntagEntSelected); } protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { - base.Started(uid, component, gameRule, args); + var eligible = new List>(); + var eligibleQuery = EntityQueryEnumerator(); + while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member)) + { + if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member))) + continue; - if (GameTicker.RunLevel == GameRunLevel.InRound) - SpawnOperativesForGhostRoles(uid, component); + eligible.Add((eligibleUid, eligibleComp, member)); + } + + if (eligible.Count == 0) + return; + + component.TargetStation = RobustRandom.Pick(eligible); } #region Event Handlers - - private void OnStartAttempt(RoundStartAttemptEvent ev) + protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule, + ref RoundEndTextAppendEvent args) { - TryRoundStartAttempt(ev, Loc.GetString("nukeops-title")); - } + var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}"); + args.AddLine(winText); - private void OnPlayersSpawning(RulePlayerSpawningEvent ev) - { - var query = QueryActiveRules(); - while (query.MoveNext(out var uid, out _, out var nukeops, out _)) + foreach (var cond in component.WinConditions) { - if (!SpawnMap((uid, nukeops))) - { - _sawmill.Info("Failed to load map for nukeops"); - continue; - } - - //Handle there being nobody readied up - if (ev.PlayerPool.Count == 0) - continue; - - var commanderEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.CommanderSpawnDetails.AntagRoleProto); - var agentEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.AgentSpawnDetails.AntagRoleProto); - var operativeEligible = _antagSelection.GetEligibleSessions(ev.PlayerPool, nukeops.OperativeSpawnDetails.AntagRoleProto); - //Calculate how large the nukeops team needs to be - var nukiesToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, nukeops.PlayersPerOperative, nukeops.MaxOps); - - //Select Nukies - //Select Commander, priority : commanderEligible, agentEligible, operativeEligible, all players - var selectedCommander = _antagSelection.ChooseAntags(1, commanderEligible, agentEligible, operativeEligible, ev.PlayerPool).FirstOrDefault(); - //Select Agent, priority : agentEligible, operativeEligible, all players - var selectedAgent = _antagSelection.ChooseAntags(1, agentEligible, operativeEligible, ev.PlayerPool).FirstOrDefault(); - //Select Operatives, priority : operativeEligible, all players - var selectedOperatives = _antagSelection.ChooseAntags(nukiesToSelect - 2, operativeEligible, ev.PlayerPool); - - //Create the team! - //If the session is null, they will be spawned as ghost roles (provided the cvar is set) - var operatives = new List { new NukieSpawn(selectedCommander, nukeops.CommanderSpawnDetails) }; - if (nukiesToSelect > 1) - operatives.Add(new NukieSpawn(selectedAgent, nukeops.AgentSpawnDetails)); - - for (var i = 0; i < nukiesToSelect - 2; i++) - { - //Use up all available sessions first, then spawn the rest as ghost roles (if enabled) - if (selectedOperatives.Count > i) - { - operatives.Add(new NukieSpawn(selectedOperatives[i], nukeops.OperativeSpawnDetails)); - } - else - { - operatives.Add(new NukieSpawn(null, nukeops.OperativeSpawnDetails)); - } - } - - SpawnOperatives(operatives, _cfg.GetCVar(CCVars.NukeopsSpawnGhostRoles), nukeops); - - foreach (var nukieSpawn in operatives) - { - if (nukieSpawn.Session == null) - continue; - - GameTicker.PlayerJoinGame(nukieSpawn.Session); - } - } - } - - private void OnRoundEndText(RoundEndTextAppendEvent ev) - { - var ruleQuery = QueryActiveRules(); - while (ruleQuery.MoveNext(out _, out _, out var nukeops, out _)) - { - var winText = Loc.GetString($"nukeops-{nukeops.WinType.ToString().ToLower()}"); - ev.AddLine(winText); - - foreach (var cond in nukeops.WinConditions) - { - var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}"); - ev.AddLine(text); - } + var text = Loc.GetString($"nukeops-cond-{cond.ToString().ToLower()}"); + args.AddLine(text); } - ev.AddLine(Loc.GetString("nukeops-list-start")); + args.AddLine(Loc.GetString("nukeops-list-start")); - var nukiesQuery = EntityQueryEnumerator(); - while (nukiesQuery.MoveNext(out var nukeopsUid, out _, out var mindContainer)) + var antags =_antag.GetAntagIdentifiers(uid); + + foreach (var (_, sessionData, name) in antags) { - if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer)) - continue; - - ev.AddLine(mind.Session != null - ? Loc.GetString("nukeops-list-name-user", ("name", Name(nukeopsUid)), ("user", mind.Session.Name)) - : Loc.GetString("nukeops-list-name", ("name", Name(nukeopsUid)))); + args.AddLine(Loc.GetString("nukeops-list-name-user", ("name", name), ("user", sessionData.UserName))); } } @@ -224,10 +124,10 @@ public sealed class NukeopsRuleSystem : GameRuleSystem { if (ev.OwningStation != null) { - if (ev.OwningStation == nukeops.NukieOutpost) + if (ev.OwningStation == GetOutpost(uid)) { nukeops.WinConditions.Add(WinCondition.NukeExplodedOnNukieOutpost); - SetWinType(uid, WinType.CrewMajor, nukeops); + SetWinType((uid, nukeops), WinType.CrewMajor); continue; } @@ -242,7 +142,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem } nukeops.WinConditions.Add(WinCondition.NukeExplodedOnCorrectStation); - SetWinType(uid, WinType.OpsMajor, nukeops); + SetWinType((uid, nukeops), WinType.OpsMajor); correctStation = true; } @@ -263,19 +163,85 @@ public sealed class NukeopsRuleSystem : GameRuleSystem private void OnRunLevelChanged(GameRunLevelChangedEvent ev) { + if (ev.New is not GameRunLevel.PostRound) + return; + var query = QueryActiveRules(); while (query.MoveNext(out var uid, out _, out var nukeops, out _)) { - switch (ev.New) + OnRoundEnd((uid, nukeops)); + } + } + + private void OnRoundEnd(Entity ent) + { + // If the win condition was set to operative/crew major win, ignore. + if (ent.Comp.WinType == WinType.OpsMajor || ent.Comp.WinType == WinType.CrewMajor) + return; + + var nukeQuery = AllEntityQuery(); + var centcomms = _emergency.GetCentcommMaps(); + + while (nukeQuery.MoveNext(out var nuke, out var nukeTransform)) + { + if (nuke.Status != NukeStatus.ARMED) + continue; + + // UH OH + if (nukeTransform.MapUid != null && centcomms.Contains(nukeTransform.MapUid.Value)) { - case GameRunLevel.InRound: - OnRoundStart(uid, nukeops); - break; - case GameRunLevel.PostRound: - OnRoundEnd(uid, nukeops); - break; + ent.Comp.WinConditions.Add(WinCondition.NukeActiveAtCentCom); + SetWinType((ent, ent), WinType.OpsMajor); + return; + } + + if (nukeTransform.GridUid == null || ent.Comp.TargetStation == null) + continue; + + if (!TryComp(ent.Comp.TargetStation.Value, out StationDataComponent? data)) + continue; + + foreach (var grid in data.Grids) + { + if (grid != nukeTransform.GridUid) + continue; + + ent.Comp.WinConditions.Add(WinCondition.NukeActiveInStation); + SetWinType(ent, WinType.OpsMajor); + return; } } + + if (_antag.AllAntagsAlive(ent.Owner)) + { + SetWinType(ent, WinType.OpsMinor); + ent.Comp.WinConditions.Add(WinCondition.AllNukiesAlive); + return; + } + + ent.Comp.WinConditions.Add(_antag.AnyAliveAntags(ent.Owner) + ? WinCondition.SomeNukiesAlive + : WinCondition.AllNukiesDead); + + var diskAtCentCom = false; + var diskQuery = AllEntityQuery(); + while (diskQuery.MoveNext(out _, out var transform)) + { + diskAtCentCom = transform.MapUid != null && centcomms.Contains(transform.MapUid.Value); + + // TODO: The target station should be stored, and the nuke disk should store its original station. + // This is fine for now, because we can assume a single station in base SS14. + break; + } + + // If the disk is currently at Central Command, the crew wins - just slightly. + // This also implies that some nuclear operatives have died. + SetWinType(ent, diskAtCentCom + ? WinType.CrewMinor + : WinType.OpsMinor); + ent.Comp.WinConditions.Add(diskAtCentCom + ? WinCondition.NukeDiskOnCentCom + : WinCondition.NukeDiskNotOnCentCom); } private void OnNukeDisarm(NukeDisarmSuccessEvent ev) @@ -294,66 +260,31 @@ public sealed class NukeopsRuleSystem : GameRuleSystem CheckRoundShouldEnd(); } - private void OnPlayersGhostSpawning(EntityUid uid, NukeOperativeComponent component, GhostRoleSpawnerUsedEvent args) - { - var spawner = args.Spawner; - - if (!TryComp(spawner, out var nukeOpSpawner)) - return; - - HumanoidCharacterProfile? profile = null; - if (TryComp(args.Spawned, out ActorComponent? actor)) - profile = _prefs.GetPreferences(actor.PlayerSession.UserId).SelectedCharacter as HumanoidCharacterProfile; - - // TODO: this is kinda awful for multi-nukies - foreach (var nukeops in EntityQuery()) - { - SetupOperativeEntity(uid, nukeOpSpawner.OperativeName, nukeOpSpawner.SpawnDetails, profile); - - nukeops.OperativeMindPendingData.Add(uid, nukeOpSpawner.SpawnDetails.AntagRoleProto); - } - } - - private void OnMindAdded(EntityUid uid, NukeOperativeComponent component, MindAddedMessage args) - { - if (!_mind.TryGetMind(uid, out var mindId, out var mind)) - return; - - var query = QueryActiveRules(); - while (query.MoveNext(out _, out _, out var nukeops, out _)) - { - if (nukeops.OperativeMindPendingData.TryGetValue(uid, out var role) || !nukeops.SpawnOutpost || - nukeops.RoundEndBehavior == RoundEndBehavior.Nothing) - { - role ??= nukeops.OperativeSpawnDetails.AntagRoleProto; - _roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = role }); - nukeops.OperativeMindPendingData.Remove(uid); - } - - if (mind.Session is not { } playerSession) - return; - - if (GameTicker.RunLevel != GameRunLevel.InRound) - return; - - if (nukeops.TargetStation != null && !string.IsNullOrEmpty(Name(nukeops.TargetStation.Value))) - { - NotifyNukie(playerSession, component, nukeops); - } - } - } - private void OnOperativeZombified(EntityUid uid, NukeOperativeComponent component, ref EntityZombifiedEvent args) { RemCompDeferred(uid, component); } + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + var map = Transform(ent).MapID; + + var rules = EntityQueryEnumerator(); + while (rules.MoveNext(out var uid, out _, out var mapRule)) + { + if (map != mapRule.Map) + continue; + ent.Comp.AssociatedRule = uid; + break; + } + } + private void OnShuttleFTLAttempt(ref ConsoleFTLAttemptEvent ev) { var query = QueryActiveRules(); - while (query.MoveNext(out _, out _, out var nukeops, out _)) + while (query.MoveNext(out var uid, out _, out var nukeops, out _)) { - if (ev.Uid != nukeops.NukieShuttle) + if (ev.Uid != GetShuttle((uid, nukeops))) continue; if (nukeops.WarDeclaredTime != null) @@ -397,12 +328,12 @@ public sealed class NukeopsRuleSystem : GameRuleSystem { // TODO: this is VERY awful for multi-nukies var query = QueryActiveRules(); - while (query.MoveNext(out _, out _, out var nukeops, out _)) + while (query.MoveNext(out var uid, out _, out var nukeops, out _)) { if (nukeops.WarDeclaredTime != null) continue; - if (Transform(ev.DeclaratorEntity).MapID != nukeops.NukiePlanet) + if (TryComp(uid, out var mapComp) && Transform(ev.DeclaratorEntity).MapID != mapComp.Map) continue; var newStatus = GetWarCondition(nukeops, ev.Status); @@ -448,161 +379,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem if (!_tag.HasTag(uid, NukeOpsUplinkTagPrototype)) continue; - if (!nukieRule.NukieOutpost.HasValue) + if (GetOutpost(uid) is not {} outpost) continue; - if (Transform(uid).MapID != Transform(nukieRule.NukieOutpost.Value).MapID) // Will receive bonus TC only on their start outpost + if (Transform(uid).MapID != Transform(outpost).MapID) // Will receive bonus TC only on their start outpost continue; - _store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.WarTCAmountPerNukie } }, uid, component); + _store.TryAddCurrency(new () { { TelecrystalCurrencyPrototype, nukieRule.WarTcAmountPerNukie } }, uid, component); var msg = Loc.GetString("store-currency-war-boost-given", ("target", uid)); _popupSystem.PopupEntity(msg, uid); } } - private void OnRoundStart(EntityUid uid, NukeopsRuleComponent? component = null) + private void SetWinType(Entity ent, WinType type, bool endRound = true) { - if (!Resolve(uid, ref component)) - return; - - // TODO: This needs to try and target a Nanotrasen station. At the very least, - // we can only currently guarantee that NT stations are the only station to - // exist in the base game. - - var eligible = new List>(); - var eligibleQuery = EntityQueryEnumerator(); - while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member)) - { - if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member))) - continue; - - eligible.Add((eligibleUid, eligibleComp, member)); - } - - if (eligible.Count == 0) - return; - - component.TargetStation = RobustRandom.Pick(eligible); - component.OperationName = _randomMetadata.GetRandomFromSegments([OperationPrefixDataset, OperationSuffixDataset], " "); - - var filter = Filter.Empty(); - var query = EntityQueryEnumerator(); - while (query.MoveNext(out _, out var nukeops, out var actor)) - { - NotifyNukie(actor.PlayerSession, nukeops, component); - filter.AddPlayer(actor.PlayerSession); - } - } - - private void OnRoundEnd(EntityUid uid, NukeopsRuleComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - // If the win condition was set to operative/crew major win, ignore. - if (component.WinType == WinType.OpsMajor || component.WinType == WinType.CrewMajor) - return; - - var nukeQuery = AllEntityQuery(); - var centcomms = _emergency.GetCentcommMaps(); - - while (nukeQuery.MoveNext(out var nuke, out var nukeTransform)) - { - if (nuke.Status != NukeStatus.ARMED) - continue; - - // UH OH - if (nukeTransform.MapUid != null && centcomms.Contains(nukeTransform.MapUid.Value)) - { - component.WinConditions.Add(WinCondition.NukeActiveAtCentCom); - SetWinType(uid, WinType.OpsMajor, component); - return; - } - - if (nukeTransform.GridUid == null || component.TargetStation == null) - continue; - - if (!TryComp(component.TargetStation.Value, out StationDataComponent? data)) - continue; - - foreach (var grid in data.Grids) - { - if (grid != nukeTransform.GridUid) - continue; - - component.WinConditions.Add(WinCondition.NukeActiveInStation); - SetWinType(uid, WinType.OpsMajor, component); - return; - } - } - - var allAlive = true; - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var nukeopsUid, out _, out var mindContainer, out var mobState)) - { - // mind got deleted somehow so ignore it - if (!_mind.TryGetMind(nukeopsUid, out _, out var mind, mindContainer)) - continue; - - // check if player got gibbed or ghosted or something - count as dead - if (mind.OwnedEntity != null && - // if the player somehow isn't a mob anymore that also counts as dead - // have to be alive, not crit or dead - mobState.CurrentState is MobState.Alive) - { - continue; - } - - allAlive = false; - break; - } - - // If all nuke ops were alive at the end of the round, - // the nuke ops win. This is to prevent people from - // running away the moment nuke ops appear. - if (allAlive) - { - SetWinType(uid, WinType.OpsMinor, component); - component.WinConditions.Add(WinCondition.AllNukiesAlive); - return; - } - - component.WinConditions.Add(WinCondition.SomeNukiesAlive); - - var diskAtCentCom = false; - var diskQuery = AllEntityQuery(); - - while (diskQuery.MoveNext(out _, out var transform)) - { - diskAtCentCom = transform.MapUid != null && centcomms.Contains(transform.MapUid.Value); - - // TODO: The target station should be stored, and the nuke disk should store its original station. - // This is fine for now, because we can assume a single station in base SS14. - break; - } - - // If the disk is currently at Central Command, the crew wins - just slightly. - // This also implies that some nuclear operatives have died. - if (diskAtCentCom) - { - SetWinType(uid, WinType.CrewMinor, component); - component.WinConditions.Add(WinCondition.NukeDiskOnCentCom); - } - // Otherwise, the nuke ops win. - else - { - SetWinType(uid, WinType.OpsMinor, component); - component.WinConditions.Add(WinCondition.NukeDiskNotOnCentCom); - } - } - - private void SetWinType(EntityUid uid, WinType type, NukeopsRuleComponent? component = null, bool endRound = true) - { - if (!Resolve(uid, ref component)) - return; - - component.WinType = type; + ent.Comp.WinType = type; if (endRound && (type == WinType.CrewMajor || type == WinType.OpsMajor)) _roundEndSystem.EndRound(); @@ -613,243 +405,130 @@ public sealed class NukeopsRuleSystem : GameRuleSystem var query = QueryActiveRules(); while (query.MoveNext(out var uid, out _, out var nukeops, out _)) { - if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor) - continue; + CheckRoundShouldEnd((uid, nukeops)); + } + } - // If there are any nuclear bombs that are active, immediately return. We're not over yet. - var armed = false; - foreach (var nuke in EntityQuery()) - { - if (nuke.Status == NukeStatus.ARMED) - { - armed = true; - break; - } - } - if (armed) - continue; + private void CheckRoundShouldEnd(Entity ent) + { + var nukeops = ent.Comp; - MapId? shuttleMapId = Exists(nukeops.NukieShuttle) - ? Transform(nukeops.NukieShuttle.Value).MapID + if (nukeops.RoundEndBehavior == RoundEndBehavior.Nothing || nukeops.WinType == WinType.CrewMajor || nukeops.WinType == WinType.OpsMajor) + return; + + + // If there are any nuclear bombs that are active, immediately return. We're not over yet. + foreach (var nuke in EntityQuery()) + { + if (nuke.Status == NukeStatus.ARMED) + return; + } + + var shuttle = GetShuttle((ent, ent)); + + MapId? shuttleMapId = Exists(shuttle) + ? Transform(shuttle.Value).MapID + : null; + + MapId? targetStationMap = null; + if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data)) + { + var grid = data.Grids.FirstOrNull(); + targetStationMap = grid != null + ? Transform(grid.Value).MapID : null; - - MapId? targetStationMap = null; - if (nukeops.TargetStation != null && TryComp(nukeops.TargetStation, out StationDataComponent? data)) - { - var grid = data.Grids.FirstOrNull(); - targetStationMap = grid != null - ? Transform(grid.Value).MapID - : null; - } - - // Check if there are nuke operatives still alive on the same map as the shuttle, - // or on the same map as the station. - // If there are, the round can continue. - var operatives = EntityQuery(true); - var operativesAlive = operatives - .Where(ent => - ent.Item3.MapID == shuttleMapId - || ent.Item3.MapID == targetStationMap) - .Any(ent => ent.Item2.CurrentState == MobState.Alive && ent.Item1.Running); - - if (operativesAlive) - continue; // There are living operatives than can access the shuttle, or are still on the station's map. - - // Check that there are spawns available and that they can access the shuttle. - var spawnsAvailable = EntityQuery(true).Any(); - if (spawnsAvailable && shuttleMapId == nukeops.NukiePlanet) - continue; // Ghost spawns can still access the shuttle. Continue the round. - - // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives, - // and there are no nuclear operatives on the target station's map. - nukeops.WinConditions.Add(spawnsAvailable - ? WinCondition.NukiesAbandoned - : WinCondition.AllNukiesDead); - - SetWinType(uid, WinType.CrewMajor, nukeops, false); - _roundEndSystem.DoRoundEndBehavior( - nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement); - - // prevent it called multiple times - nukeops.RoundEndBehavior = RoundEndBehavior.Nothing; - } - } - - private bool SpawnMap(Entity ent) - { - if (!ent.Comp.SpawnOutpost - || ent.Comp.NukiePlanet != null) - return true; - - ent.Comp.NukiePlanet = _mapManager.CreateMap(); - var gameMap = _prototypeManager.Index(ent.Comp.OutpostMapPrototype); - ent.Comp.NukieOutpost = GameTicker.LoadGameMap(gameMap, ent.Comp.NukiePlanet.Value, null)[0]; - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var grid, out _, out var shuttleTransform)) - { - if (shuttleTransform.MapID != ent.Comp.NukiePlanet) - continue; - - ent.Comp.NukieShuttle = grid; - break; } - return true; + // Check if there are nuke operatives still alive on the same map as the shuttle, + // or on the same map as the station. + // If there are, the round can continue. + var operatives = EntityQuery(true); + var operativesAlive = operatives + .Where(op => + op.Item3.MapID == shuttleMapId + || op.Item3.MapID == targetStationMap) + .Any(op => op.Item2.CurrentState == MobState.Alive && op.Item1.Running); + + if (operativesAlive) + return; // There are living operatives than can access the shuttle, or are still on the station's map. + + // Check that there are spawns available and that they can access the shuttle. + var spawnsAvailable = EntityQuery(true).Any(); + if (spawnsAvailable && CompOrNull(ent)?.Map == shuttleMapId) + return; // Ghost spawns can still access the shuttle. Continue the round. + + // The shuttle is inaccessible to both living nuke operatives and yet to spawn nuke operatives, + // and there are no nuclear operatives on the target station's map. + nukeops.WinConditions.Add(spawnsAvailable + ? WinCondition.NukiesAbandoned + : WinCondition.AllNukiesDead); + + SetWinType(ent, WinType.CrewMajor, false); + _roundEndSystem.DoRoundEndBehavior( + nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement); + + // prevent it called multiple times + nukeops.RoundEndBehavior = RoundEndBehavior.Nothing; } - /// - /// Adds missing nuke operative components, equips starting gear and renames the entity. - /// - private void SetupOperativeEntity(EntityUid mob, string name, NukeopSpawnPreset spawnDetails, HumanoidCharacterProfile? profile) + // this should really go anywhere else but im tired. + private void OnAntagSelectEntity(Entity ent, ref AntagSelectEntityEvent args) { - _metaData.SetEntityName(mob, name); - EnsureComp(mob); - - if (profile != null) - _humanoid.LoadProfile(mob, profile); - - var gear = _prototypeManager.Index(spawnDetails.GearProto); - _stationSpawning.EquipStartingGear(mob, gear); - - _npcFaction.RemoveFaction(mob, "NanoTrasen", false); - _npcFaction.AddFaction(mob, "Syndicate"); - } - - private void SpawnOperatives(List sessions, bool spawnGhostRoles, NukeopsRuleComponent component) - { - if (component.NukieOutpost is not { Valid: true } outpostUid) + if (args.Handled) return; - var spawns = new List(); - foreach (var (_, meta, xform) in EntityQuery(true)) + var profile = args.Session != null + ? _prefs.GetPreferences(args.Session.UserId).SelectedCharacter as HumanoidCharacterProfile + : HumanoidCharacterProfile.RandomWithSpecies(); + if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species)) { - if (meta.EntityPrototype?.ID != component.SpawnPointProto.Id) - continue; - - if (xform.ParentUid != component.NukieOutpost) - continue; - - spawns.Add(xform.Coordinates); - break; + species = _prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies); } - //Fallback, spawn at the centre of the map - if (spawns.Count == 0) - { - spawns.Add(Transform(outpostUid).Coordinates); - _sawmill.Warning($"Fell back to default spawn for nukies!"); - } - - //Spawn the team - foreach (var nukieSession in sessions) - { - var name = $"{Loc.GetString(nukieSession.Type.NamePrefix)} {RobustRandom.PickAndTake(_prototypeManager.Index(nukieSession.Type.NameList).Values.ToList())}"; - - var nukeOpsAntag = _prototypeManager.Index(nukieSession.Type.AntagRoleProto); - - //If a session is available, spawn mob and transfer mind into it - if (nukieSession.Session != null) - { - var profile = _prefs.GetPreferences(nukieSession.Session.UserId).SelectedCharacter as HumanoidCharacterProfile; - if (!_prototypeManager.TryIndex(profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies, out SpeciesPrototype? species)) - { - species = _prototypeManager.Index(SharedHumanoidAppearanceSystem.DefaultSpecies); - } - - var mob = Spawn(species.Prototype, RobustRandom.Pick(spawns)); - SetupOperativeEntity(mob, name, nukieSession.Type, profile); - - var newMind = _mind.CreateMind(nukieSession.Session.UserId, name); - _mind.SetUserId(newMind, nukieSession.Session.UserId); - _roles.MindAddRole(newMind, new NukeopsRoleComponent { PrototypeId = nukieSession.Type.AntagRoleProto }); - - _mind.TransferTo(newMind, mob); - } - //Otherwise, spawn as a ghost role - else if (spawnGhostRoles) - { - var spawnPoint = Spawn(component.GhostSpawnPointProto, RobustRandom.Pick(spawns)); - var ghostRole = EnsureComp(spawnPoint); - EnsureComp(spawnPoint); - ghostRole.RoleName = Loc.GetString(nukeOpsAntag.Name); - ghostRole.RoleDescription = Loc.GetString(nukeOpsAntag.Objective); - - var nukeOpSpawner = EnsureComp(spawnPoint); - nukeOpSpawner.OperativeName = name; - nukeOpSpawner.SpawnDetails = nukieSession.Type; - } - } + args.Entity = Spawn(species.Prototype); + _humanoid.LoadProfile(args.Entity.Value, profile); } - /// - /// Display a greeting message and play a sound for a nukie - /// - private void NotifyNukie(ICommonSession session, NukeOperativeComponent nukeop, NukeopsRuleComponent nukeopsRule) + private void OnAfterAntagEntSelected(Entity ent, ref AfterAntagEntitySelectedEvent args) { - if (nukeopsRule.TargetStation is not { } station) + if (ent.Comp.TargetStation is not { } station) return; - _antagSelection.SendBriefing(session, Loc.GetString("nukeops-welcome", ("station", station), ("name", nukeopsRule.OperationName)), Color.Red, nukeop.GreetSoundNotification); + _antag.SendBriefing(args.Session, Loc.GetString("nukeops-welcome", + ("station", station), + ("name", Name(ent))), + Color.Red, + ent.Comp.GreetSoundNotification); } - /// - /// Spawn nukie ghost roles if this gamerule was started mid round - /// - private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null) + /// + /// Is this method the shitty glue holding together the last of my sanity? yes. + /// Do i have a better solution? not presently. + /// + private EntityUid? GetOutpost(Entity ent) { - if (!Resolve(uid, ref component)) - return; + if (!Resolve(ent, ref ent.Comp, false)) + return null; - if (!SpawnMap((uid, component))) - { - _sawmill.Info("Failed to load map for nukeops"); - return; - } - - var numNukies = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerOperative, component.MaxOps); - - //Dont continue if we have no nukies to spawn - if (numNukies == 0) - return; - - //Fill the ranks, commander first, then agent, then operatives - //TODO: Possible alternative team compositions? Like multiple commanders or agents - var operatives = new List(); - if (numNukies >= 1) - operatives.Add(new NukieSpawn(null, component.CommanderSpawnDetails)); - if (numNukies >= 2) - operatives.Add(new NukieSpawn(null, component.AgentSpawnDetails)); - if (numNukies >= 3) - { - for (var i = 2; i < numNukies; i++) - { - operatives.Add(new NukieSpawn(null, component.OperativeSpawnDetails)); - } - } - - SpawnOperatives(operatives, true, component); + return ent.Comp.MapGrids.FirstOrNull(); } - //For admins forcing someone to nukeOps. - public void MakeLoneNukie(EntityUid entity) + /// + /// Is this method the shitty glue holding together the last of my sanity? yes. + /// Do i have a better solution? not presently. + /// + private EntityUid? GetShuttle(Entity ent) { - if (!_mind.TryGetMind(entity, out var mindId, out var mindComponent)) - return; + if (!Resolve(ent, ref ent.Comp, false)) + return null; - //ok hardcoded value bad but so is everything else here - _roles.MindAddRole(mindId, new NukeopsRoleComponent { PrototypeId = NukeopsId }, mindComponent); - SetOutfitCommand.SetOutfit(entity, "SyndicateOperativeGearFull", EntityManager); - } - - private sealed class NukieSpawn - { - public ICommonSession? Session { get; private set; } - public NukeopSpawnPreset Type { get; private set; } - - public NukieSpawn(ICommonSession? session, NukeopSpawnPreset type) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) { - Session = session; - Type = type; + if (comp.AssociatedRule == ent.Owner) + return uid; } + + return null; } } diff --git a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs b/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs deleted file mode 100644 index 0a749d2e01..0000000000 --- a/Content.Server/GameTicking/Rules/PiratesRuleSystem.cs +++ /dev/null @@ -1,321 +0,0 @@ -using System.Linq; -using System.Numerics; -using Content.Server.Administration.Commands; -using Content.Server.Cargo.Systems; -using Content.Server.Chat.Managers; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Preferences.Managers; -using Content.Server.Spawners.Components; -using Content.Server.Station.Components; -using Content.Server.Station.Systems; -using Content.Shared.CCVar; -using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Prototypes; -using Content.Shared.Mind; -using Content.Shared.NPC.Prototypes; -using Content.Shared.NPC.Systems; -using Content.Shared.Preferences; -using Content.Shared.Roles; -using Robust.Server.GameObjects; -using Robust.Server.Maps; -using Robust.Server.Player; -using Robust.Shared.Audio; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Configuration; -using Robust.Shared.Enums; -using Robust.Shared.Map; -using Robust.Shared.Map.Components; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; -using Robust.Shared.Utility; - -namespace Content.Server.GameTicking.Rules; - -/// -/// This handles the Pirates minor antag, which is designed to coincide with other modes on occasion. -/// -public sealed class PiratesRuleSystem : GameRuleSystem -{ - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IChatManager _chatManager = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IServerPreferencesManager _prefs = default!; - [Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!; - [Dependency] private readonly PricingSystem _pricingSystem = default!; - [Dependency] private readonly MapLoaderSystem _map = default!; - [Dependency] private readonly NamingSystem _namingSystem = default!; - [Dependency] private readonly NpcFactionSystem _npcFaction = default!; - [Dependency] private readonly SharedMindSystem _mindSystem = default!; - [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; - - [ValidatePrototypeId] - private const string GameRuleId = "Pirates"; - - [ValidatePrototypeId] - private const string MobId = "MobHuman"; - - [ValidatePrototypeId] - private const string SpeciesId = "Human"; - - [ValidatePrototypeId] - private const string PirateFactionId = "Syndicate"; - - [ValidatePrototypeId] - private const string EnemyFactionId = "NanoTrasen"; - - [ValidatePrototypeId] - private const string GearId = "PirateGear"; - - [ValidatePrototypeId] - private const string SpawnPointId = "SpawnPointPirates"; - - /// - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnPlayerSpawningEvent); - SubscribeLocalEvent(OnRoundEndTextEvent); - SubscribeLocalEvent(OnStartAttempt); - } - - private void OnRoundEndTextEvent(RoundEndTextAppendEvent ev) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var pirates, out var gameRule)) - { - if (Deleted(pirates.PirateShip)) - { - // Major loss, the ship somehow got annihilated. - ev.AddLine(Loc.GetString("pirates-no-ship")); - } - else - { - List<(double, EntityUid)> mostValuableThefts = new(); - - var comp1 = pirates; - var finalValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid => - { - foreach (var mindId in comp1.Pirates) - { - if (TryComp(mindId, out MindComponent? mind) && mind.CurrentEntity == uid) - return false; // Don't appraise the pirates twice, we count them in separately. - } - - return true; - }, (uid, price) => - { - if (comp1.InitialItems.Contains(uid)) - return; - - mostValuableThefts.Add((price, uid)); - mostValuableThefts.Sort((i1, i2) => i2.Item1.CompareTo(i1.Item1)); - if (mostValuableThefts.Count > 5) - mostValuableThefts.Pop(); - }); - - foreach (var mindId in pirates.Pirates) - { - if (TryComp(mindId, out MindComponent? mind) && mind.CurrentEntity is not null) - finalValue += _pricingSystem.GetPrice(mind.CurrentEntity.Value); - } - - var score = finalValue - pirates.InitialShipValue; - - ev.AddLine(Loc.GetString("pirates-final-score", ("score", $"{score:F2}"))); - ev.AddLine(Loc.GetString("pirates-final-score-2", ("finalPrice", $"{finalValue:F2}"))); - - ev.AddLine(""); - ev.AddLine(Loc.GetString("pirates-most-valuable")); - - foreach (var (price, obj) in mostValuableThefts) - { - ev.AddLine(Loc.GetString("pirates-stolen-item-entry", ("entity", obj), ("credits", $"{price:F2}"))); - } - - if (mostValuableThefts.Count == 0) - ev.AddLine(Loc.GetString("pirates-stole-nothing")); - } - - ev.AddLine(""); - ev.AddLine(Loc.GetString("pirates-list-start")); - foreach (var pirate in pirates.Pirates) - { - if (TryComp(pirate, out MindComponent? mind)) - { - ev.AddLine($"- {mind.CharacterName} ({mind.Session?.Name})"); - } - } - } - } - - private void OnPlayerSpawningEvent(RulePlayerSpawningEvent ev) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var pirates, out var gameRule)) - { - // Forgive me for copy-pasting nukies. - if (!GameTicker.IsGameRuleAdded(uid, gameRule)) - return; - - pirates.Pirates.Clear(); - pirates.InitialItems.Clear(); - - // Between 1 and : needs at least n players per op. - var numOps = Math.Max(1, - (int) Math.Min( - Math.Floor((double) ev.PlayerPool.Count / _cfg.GetCVar(CCVars.PiratesPlayersPerOp)), - _cfg.GetCVar(CCVars.PiratesMaxOps))); - var ops = new ICommonSession[numOps]; - for (var i = 0; i < numOps; i++) - { - ops[i] = _random.PickAndTake(ev.PlayerPool); - } - - var map = "/Maps/Shuttles/pirate.yml"; - var xformQuery = GetEntityQuery(); - - var aabbs = EntityQuery().SelectMany(x => - x.Grids.Select(x => - xformQuery.GetComponent(x).WorldMatrix.TransformBox(Comp(x).LocalAABB))) - .ToArray(); - - var aabb = aabbs[0]; - - for (var i = 1; i < aabbs.Length; i++) - { - aabb.Union(aabbs[i]); - } - - // (Not commented?) - var a = MathF.Max(aabb.Height / 2f, aabb.Width / 2f) * 2.5f; - - var gridId = _map.LoadGrid(GameTicker.DefaultMap, map, new MapLoadOptions - { - Offset = aabb.Center + new Vector2(a, a), - LoadMap = false, - }); - - if (!gridId.HasValue) - { - Log.Error($"Gridid was null when loading \"{map}\", aborting."); - foreach (var session in ops) - { - ev.PlayerPool.Add(session); - } - - return; - } - - pirates.PirateShip = gridId.Value; - - // TODO: Loot table or something - var pirateGear = _prototypeManager.Index(GearId); // YARRR - - var spawns = new List(); - - // Forgive me for hardcoding prototypes - foreach (var (_, meta, xform) in - EntityQuery(true)) - { - if (meta.EntityPrototype?.ID != SpawnPointId || xform.ParentUid != pirates.PirateShip) - continue; - - spawns.Add(xform.Coordinates); - } - - if (spawns.Count == 0) - { - spawns.Add(Transform(pirates.PirateShip).Coordinates); - Log.Warning($"Fell back to default spawn for pirates!"); - } - - for (var i = 0; i < ops.Length; i++) - { - var sex = _random.Prob(0.5f) ? Sex.Male : Sex.Female; - var gender = sex == Sex.Male ? Gender.Male : Gender.Female; - - var name = _namingSystem.GetName(SpeciesId, gender); - - var session = ops[i]; - var newMind = _mindSystem.CreateMind(session.UserId, name); - _mindSystem.SetUserId(newMind, session.UserId); - - var mob = Spawn(MobId, _random.Pick(spawns)); - _metaData.SetEntityName(mob, name); - - _mindSystem.TransferTo(newMind, mob); - var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile; - _stationSpawningSystem.EquipStartingGear(mob, pirateGear); - - _npcFaction.RemoveFaction(mob, EnemyFactionId, false); - _npcFaction.AddFaction(mob, PirateFactionId); - - pirates.Pirates.Add(newMind); - - // Notificate every player about a pirate antagonist role with sound - _audioSystem.PlayGlobal(pirates.PirateAlertSound, session); - - GameTicker.PlayerJoinGame(session); - } - - pirates.InitialShipValue = _pricingSystem.AppraiseGrid(pirates.PirateShip, uid => - { - pirates.InitialItems.Add(uid); - return true; - }); // Include the players in the appraisal. - } - } - - //Forcing one player to be a pirate. - public void MakePirate(EntityUid entity) - { - if (!_mindSystem.TryGetMind(entity, out var mindId, out var mind)) - return; - - SetOutfitCommand.SetOutfit(entity, GearId, EntityManager); - - var pirateRule = EntityQuery().FirstOrDefault(); - if (pirateRule == null) - { - //todo fuck me this shit is awful - GameTicker.StartGameRule(GameRuleId, out var ruleEntity); - pirateRule = Comp(ruleEntity); - } - - // Notificate every player about a pirate antagonist role with sound - if (mind.Session != null) - { - _audioSystem.PlayGlobal(pirateRule.PirateAlertSound, mind.Session); - } - } - - private void OnStartAttempt(RoundStartAttemptEvent ev) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var pirates, out var gameRule)) - { - if (!GameTicker.IsGameRuleActive(uid, gameRule)) - return; - - var minPlayers = _cfg.GetCVar(CCVars.PiratesMinPlayers); - if (!ev.Forced && ev.Players.Length < minPlayers) - { - _chatManager.SendAdminAnnouncement(Loc.GetString("nukeops-not-enough-ready-players", - ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers))); - ev.Cancel(); - return; - } - - if (ev.Players.Length == 0) - { - _chatManager.DispatchServerAnnouncement(Loc.GetString("nukeops-no-one-ready")); - ev.Cancel(); - } - } - } -} diff --git a/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs b/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs index b11c28fb2b..5215da96aa 100644 --- a/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat.Managers; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Systems; using Content.Shared.Chat; diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs index ba9fb2ccbc..e89d4614ff 100644 --- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs @@ -14,7 +14,6 @@ using Content.Server.Station.Systems; using Content.Shared.Database; using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; -using Content.Shared.Inventory; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mindshield.Components; @@ -24,12 +23,11 @@ using Content.Shared.Mobs.Systems; using Content.Shared.NPC.Prototypes; using Content.Shared.NPC.Systems; using Content.Shared.Revolutionary.Components; -using Content.Shared.Roles; using Content.Shared.Stunnable; using Content.Shared.Zombies; using Robust.Shared.Prototypes; using Robust.Shared.Timing; -using System.Linq; +using Content.Server.GameTicking.Components; namespace Content.Server.GameTicking.Rules; @@ -40,7 +38,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem RevolutionaryNpcFaction = "Revolutionary"; @@ -60,23 +57,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem(OnStartAttempt); - SubscribeLocalEvent(OnPlayerJobAssigned); SubscribeLocalEvent(OnCommandMobStateChanged); SubscribeLocalEvent(OnHeadRevMobStateChanged); - SubscribeLocalEvent(OnRoundEndText); SubscribeLocalEvent(OnGetBriefing); SubscribeLocalEvent(OnPostFlash); } - //Set miniumum players - protected override void Added(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) - { - base.Added(uid, component, gameRule, args); - - gameRule.MinPlayers = component.MinPlayers; - } - protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { base.Started(uid, component, gameRule, args); @@ -98,40 +84,29 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem(); - while (query.MoveNext(out var headrev)) + // This is (revsLost, commandsLost) concatted together + // (moony wrote this comment idk what it means) + var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0); + args.AddLine(Loc.GetString(Outcomes[index])); + + var sessionData = _antag.GetAntagIdentifiers(uid); + args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count))); + foreach (var (mind, data, name) in sessionData) { - // This is (revsLost, commandsLost) concatted together - // (moony wrote this comment idk what it means) - var index = (commandLost ? 1 : 0) | (revsLost ? 2 : 0); - ev.AddLine(Loc.GetString(Outcomes[index])); + var count = CompOrNull(mind)?.ConvertedCount ?? 0; + args.AddLine(Loc.GetString("rev-headrev-name-user", + ("name", name), + ("username", data.UserName), + ("count", count))); - ev.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", headrev.HeadRevs.Count))); - foreach (var player in headrev.HeadRevs) - { - // TODO: when role entities are a thing this has to change - var count = CompOrNull(player.Value)?.ConvertedCount ?? 0; - - _mind.TryGetSession(player.Value, out var session); - var username = session?.Name; - if (username != null) - { - ev.AddLine(Loc.GetString("rev-headrev-name-user", - ("name", player.Key), - ("username", username), ("count", count))); - } - else - { - ev.AddLine(Loc.GetString("rev-headrev-name", - ("name", player.Key), ("count", count))); - } - - // TODO: someone suggested listing all alive? revs maybe implement at some point - } + // TODO: someone suggested listing all alive? revs maybe implement at some point } } @@ -144,57 +119,6 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem chosen, ProtoId antagProto, RevolutionaryRuleComponent comp) - { - foreach (var headRev in chosen) - GiveHeadRev(headRev, antagProto, comp); - } - private void GiveHeadRev(EntityUid chosen, ProtoId antagProto, RevolutionaryRuleComponent comp) - { - RemComp(chosen); - - var inCharacterName = MetaData(chosen).EntityName; - - if (!_mind.TryGetMind(chosen, out var mind, out _)) - return; - - if (!_role.MindHasRole(mind)) - { - _role.MindAddRole(mind, new RevolutionaryRoleComponent { PrototypeId = antagProto }, silent: true); - } - - comp.HeadRevs.Add(inCharacterName, mind); - _inventory.SpawnItemsOnEntity(chosen, comp.StartingGear); - var revComp = EnsureComp(chosen); - EnsureComp(chosen); - - _antagSelection.SendBriefing(chosen, Loc.GetString("head-rev-role-greeting"), Color.CornflowerBlue, revComp.RevStartSound); - } - /// /// Called when a Head Rev uses a flash in melee to convert somebody else. /// @@ -232,22 +156,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem(entity)) - return; - - var revRule = EntityQuery().FirstOrDefault(); - if (revRule == null) - { - GameTicker.StartGameRule("Revolutionary", out var ruleEnt); - revRule = Comp(ruleEnt); - } - - GiveHeadRev(entity, revRule.HeadRevPrototypeId, revRule); + _antag.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound); } //TODO: Enemies of the revolution @@ -308,7 +217,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem { [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly AntagSelectionSystem _antagSelection = default!; [Dependency] private readonly MindSystem _mindSystem = default!; - [Dependency] private readonly SharedRoleSystem _roleSystem = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly ObjectivesSystem _objectives = default!; - [Dependency] private readonly InventorySystem _inventory = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnPlayersSpawned); + SubscribeLocalEvent(AfterAntagSelected); SubscribeLocalEvent(OnGetBriefing); SubscribeLocalEvent(OnObjectivesTextGetInfo); } - private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev) + private void AfterAntagSelected(Entity ent, ref AfterAntagEntitySelectedEvent args) { - var query = QueryActiveRules(); - while (query.MoveNext(out var uid, out _, out var comp, out var gameRule)) - { - //Get all players eligible for this role, allow selecting existing antags - //TO DO: When voxes specifies are added, increase their chance of becoming a thief by 4 times >:) - var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.ThiefPrototypeId, acceptableAntags: AntagAcceptability.All, allowNonHumanoids: true); - - //Abort if there are none - if (eligiblePlayers.Count == 0) - { - Log.Warning($"No eligible thieves found, ending game rule {ToPrettyString(uid):rule}"); - GameTicker.EndGameRule(uid, gameRule); - continue; - } - - //Calculate number of thieves to choose - var thiefCount = _random.Next(1, comp.MaxAllowThief + 1); - - //Select our theives - var thieves = _antagSelection.ChooseAntags(thiefCount, eligiblePlayers); - - MakeThief(thieves, comp, comp.PacifistThieves); - } - } - - public void MakeThief(List players, ThiefRuleComponent thiefRule, bool addPacified) - { - foreach (var thief in players) - { - MakeThief(thief, thiefRule, addPacified); - } - } - - public void MakeThief(EntityUid thief, ThiefRuleComponent thiefRule, bool addPacified) - { - if (!_mindSystem.TryGetMind(thief, out var mindId, out var mind)) + if (!_mindSystem.TryGetMind(args.EntityUid, out var mindId, out var mind)) return; - if (HasComp(mindId)) - return; - - // Assign thief roles - _roleSystem.MindAddRole(mindId, new ThiefRoleComponent - { - PrototypeId = thiefRule.ThiefPrototypeId, - }, silent: true); - - //Add Pacified - //To Do: Long-term this should just be using the antag code to add components. - if (addPacified) //This check is important because some servers may want to disable the thief's pacifism. Do not remove. - { - EnsureComp(thief); - } - //Generate objectives - GenerateObjectives(mindId, mind, thiefRule); - - //Send briefing here to account for humanoid/animal - _antagSelection.SendBriefing(thief, MakeBriefing(thief), null, thiefRule.GreetingSound); - - // Give starting items - _inventory.SpawnItemsOnEntity(thief, thiefRule.StarterItems); - - thiefRule.ThievesMinds.Add(mindId); - } - - public void AdminMakeThief(EntityUid entity, bool addPacified) - { - var thiefRule = EntityQuery().FirstOrDefault(); - if (thiefRule == null) - { - GameTicker.StartGameRule("Thief", out var ruleEntity); - thiefRule = Comp(ruleEntity); - } - - if (HasComp(entity)) - return; - - MakeThief(entity, thiefRule, addPacified); + GenerateObjectives(mindId, mind, ent); } private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule) @@ -160,8 +79,7 @@ public sealed class ThiefRuleSystem : GameRuleSystem private string MakeBriefing(EntityUid thief) { var isHuman = HasComp(thief); - var briefing = "\n"; - briefing = isHuman + var briefing = isHuman ? Loc.GetString("thief-role-greeting-human") : Loc.GetString("thief-role-greeting-animal"); @@ -169,9 +87,9 @@ public sealed class ThiefRuleSystem : GameRuleSystem return briefing; } - private void OnObjectivesTextGetInfo(Entity thiefs, ref ObjectivesTextGetInfoEvent args) + private void OnObjectivesTextGetInfo(Entity ent, ref ObjectivesTextGetInfoEvent args) { - args.Minds = thiefs.Comp.ThievesMinds; + args.Minds = _antag.GetAntagMindEntityUids(ent.Owner); args.AgentName = Loc.GetString("thief-round-end-agent-name"); } } diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs index 769d7e0a5b..b6bcd5ee1e 100644 --- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs @@ -5,97 +5,61 @@ using Content.Server.Objectives; using Content.Server.PDA.Ringer; using Content.Server.Roles; using Content.Server.Traitor.Uplink; -using Content.Shared.CCVar; -using Content.Shared.Dataset; using Content.Shared.Mind; -using Content.Shared.Mobs.Systems; using Content.Shared.NPC.Systems; using Content.Shared.Objectives.Components; using Content.Shared.PDA; using Content.Shared.Roles; using Content.Shared.Roles.Jobs; -using Robust.Server.Player; -using Robust.Shared.Configuration; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Robust.Shared.Timing; using System.Linq; using System.Text; +using Content.Server.GameTicking.Components; namespace Content.Server.GameTicking.Rules; public sealed class TraitorRuleSystem : GameRuleSystem { - [Dependency] private readonly AntagSelectionSystem _antagSelection = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly NpcFactionSystem _npcFaction = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly UplinkSystem _uplink = default!; [Dependency] private readonly MindSystem _mindSystem = default!; [Dependency] private readonly SharedRoleSystem _roleSystem = default!; [Dependency] private readonly SharedJobSystem _jobs = default!; [Dependency] private readonly ObjectivesSystem _objectives = default!; - [Dependency] private readonly IGameTiming _timing = default!; - private int PlayersPerTraitor => _cfg.GetCVar(CCVars.TraitorPlayersPerTraitor); - private int MaxTraitors => _cfg.GetCVar(CCVars.TraitorMaxTraitors); + public const int MaxPicks = 20; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnStartAttempt); - SubscribeLocalEvent(OnPlayersSpawned); - SubscribeLocalEvent(HandleLatejoin); + SubscribeLocalEvent(AfterEntitySelected); SubscribeLocalEvent(OnObjectivesTextGetInfo); SubscribeLocalEvent(OnObjectivesTextPrepend); } - //Set min players on game rule protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) { base.Added(uid, component, gameRule, args); - - gameRule.MinPlayers = _cfg.GetCVar(CCVars.TraitorMinPlayers); - } - - protected override void Started(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); MakeCodewords(component); } - protected override void ActiveTick(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, float frameTime) + private void AfterEntitySelected(Entity ent, ref AfterAntagEntitySelectedEvent args) { - base.ActiveTick(uid, component, gameRule, frameTime); - - if (component.SelectionStatus < TraitorRuleComponent.SelectionState.Started && component.AnnounceAt < _timing.CurTime) - { - DoTraitorStart(component); - component.SelectionStatus = TraitorRuleComponent.SelectionState.Started; - } - } - - /// - /// Check for enough players - /// - /// - private void OnStartAttempt(RoundStartAttemptEvent ev) - { - TryRoundStartAttempt(ev, Loc.GetString("traitor-title")); + MakeTraitor(args.EntityUid, ent); } private void MakeCodewords(TraitorRuleComponent component) { - var codewordCount = _cfg.GetCVar(CCVars.TraitorCodewordCount); - var adjectives = _prototypeManager.Index(component.CodewordAdjectives).Values; - var verbs = _prototypeManager.Index(component.CodewordVerbs).Values; + var adjectives = _prototypeManager.Index(component.CodewordAdjectives).Values; + var verbs = _prototypeManager.Index(component.CodewordVerbs).Values; var codewordPool = adjectives.Concat(verbs).ToList(); - var finalCodewordCount = Math.Min(codewordCount, codewordPool.Count); + var finalCodewordCount = Math.Min(component.CodewordCount, codewordPool.Count); component.Codewords = new string[finalCodewordCount]; for (var i = 0; i < finalCodewordCount; i++) { @@ -103,66 +67,19 @@ public sealed class TraitorRuleSystem : GameRuleSystem } } - private void DoTraitorStart(TraitorRuleComponent component) - { - var eligiblePlayers = _antagSelection.GetEligiblePlayers(_playerManager.Sessions, component.TraitorPrototypeId); - - if (eligiblePlayers.Count == 0) - return; - - var traitorsToSelect = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, PlayersPerTraitor, MaxTraitors); - - var selectedTraitors = _antagSelection.ChooseAntags(traitorsToSelect, eligiblePlayers); - - MakeTraitor(selectedTraitors, component); - } - - private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev) - { - //Start the timer - var query = QueryActiveRules(); - while (query.MoveNext(out _, out var comp, out var gameRuleComponent)) - { - var delay = TimeSpan.FromSeconds( - _cfg.GetCVar(CCVars.TraitorStartDelay) + - _random.NextFloat(0f, _cfg.GetCVar(CCVars.TraitorStartDelayVariance))); - - //Set the delay for choosing traitors - comp.AnnounceAt = _timing.CurTime + delay; - - comp.SelectionStatus = TraitorRuleComponent.SelectionState.ReadyToStart; - } - } - - public bool MakeTraitor(List traitors, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true) - { - foreach (var traitor in traitors) - { - MakeTraitor(traitor, component, giveUplink, giveObjectives); - } - - return true; - } - public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true) { //Grab the mind if it wasnt provided if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind)) return false; - if (HasComp(mindId)) - { - Log.Error($"Player {mind.CharacterName} is already a traitor."); - return false; - } - var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords))); Note[]? code = null; if (giveUplink) { // Calculate the amount of currency on the uplink. - var startingBalance = _cfg.GetCVar(CCVars.TraitorStartingBalance); + var startingBalance = component.StartingBalance; if (_jobs.MindTryGetJob(mindId, out _, out var prototype)) startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0); @@ -180,19 +97,14 @@ public sealed class TraitorRuleSystem : GameRuleSystem Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#")))); } - _antagSelection.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification); + _antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code), null, component.GreetSoundNotification); component.TraitorMinds.Add(mindId); - // Assign traitor roles - _roleSystem.MindAddRole(mindId, new TraitorRoleComponent - { - PrototypeId = component.TraitorPrototypeId - }, mind, true); // Assign briefing _roleSystem.MindAddRole(mindId, new RoleBriefingComponent { - Briefing = briefing.ToString() + Briefing = briefing }, mind, true); // Change the faction @@ -202,11 +114,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem // Give traitors their objectives if (giveObjectives) { - var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty); - var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks); var difficulty = 0f; - Log.Debug($"Attempting {maxPicks} objective picks with {maxDifficulty} difficulty"); - for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++) + for (var pick = 0; pick < MaxPicks && component.MaxDifficulty > difficulty; pick++) { var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup); if (objective == null) @@ -222,53 +131,9 @@ public sealed class TraitorRuleSystem : GameRuleSystem return true; } - private void HandleLatejoin(PlayerSpawnCompleteEvent ev) - { - var query = QueryActiveRules(); - while (query.MoveNext(out _, out var comp, out _)) - { - if (comp.TotalTraitors >= MaxTraitors) - continue; - - if (!ev.LateJoin) - continue; - - if (!_antagSelection.IsPlayerEligible(ev.Player, comp.TraitorPrototypeId)) - continue; - - //If its before we have selected traitors, continue - if (comp.SelectionStatus < TraitorRuleComponent.SelectionState.Started) - continue; - - // the nth player we adjust our probabilities around - var target = PlayersPerTraitor * comp.TotalTraitors + 1; - var chance = 1f / PlayersPerTraitor; - - // If we have too many traitors, divide by how many players below target for next traitor we are. - if (ev.JoinOrder < target) - { - chance /= (target - ev.JoinOrder); - } - else // Tick up towards 100% chance. - { - chance *= ((ev.JoinOrder + 1) - target); - } - - if (chance > 1) - chance = 1; - - // Now that we've calculated our chance, roll and make them a traitor if we roll under. - // You get one shot. - if (_random.Prob(chance)) - { - MakeTraitor(ev.Mob, comp); - } - } - } - private void OnObjectivesTextGetInfo(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextGetInfoEvent args) { - args.Minds = comp.TraitorMinds; + args.Minds = _antag.GetAntagMindEntityUids(uid); args.AgentName = Loc.GetString("traitor-round-end-agent-name"); } @@ -277,27 +142,6 @@ public sealed class TraitorRuleSystem : GameRuleSystem args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords))); } - /// - /// Start this game rule manually - /// - public TraitorRuleComponent StartGameRule() - { - var comp = EntityQuery().FirstOrDefault(); - if (comp == null) - { - GameTicker.StartGameRule("Traitor", out var ruleEntity); - comp = Comp(ruleEntity); - } - - return comp; - } - - public void MakeTraitorAdmin(EntityUid entity, bool giveUplink, bool giveObjectives) - { - var traitorRule = StartGameRule(); - MakeTraitor(entity, traitorRule, giveUplink, giveObjectives); - } - private string GenerateBriefing(string[] codewords, Note[]? uplinkCode) { var sb = new StringBuilder(); @@ -312,9 +156,11 @@ public sealed class TraitorRuleSystem : GameRuleSystem public List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind) { List<(EntityUid Id, MindComponent Mind)> allTraitors = new(); - foreach (var traitor in EntityQuery()) + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var traitor)) { - foreach (var role in GetOtherTraitorMindsAliveAndConnected(ourMind, traitor)) + foreach (var role in GetOtherTraitorMindsAliveAndConnected(ourMind, (uid, traitor))) { if (!allTraitors.Contains(role)) allTraitors.Add(role); @@ -324,20 +170,15 @@ public sealed class TraitorRuleSystem : GameRuleSystem return allTraitors; } - private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, TraitorRuleComponent component) + private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, Entity rule) { var traitors = new List<(EntityUid Id, MindComponent Mind)>(); - foreach (var traitor in component.TraitorMinds) + foreach (var mind in _antag.GetAntagMinds(rule.Owner)) { - if (TryComp(traitor, out MindComponent? mind) && - mind.OwnedEntity != null && - mind.Session != null && - mind != ourMind && - _mobStateSystem.IsAlive(mind.OwnedEntity.Value) && - mind.CurrentEntity == mind.OwnedEntity) - { - traitors.Add((traitor, mind)); - } + if (mind.Comp == ourMind) + continue; + + traitors.Add((mind, mind)); } return traitors; diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs index 54e8bcf8b7..f22c208408 100644 --- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs @@ -1,112 +1,90 @@ -using Content.Server.Actions; using Content.Server.Antag; using Content.Server.Chat.Systems; using Content.Server.GameTicking.Rules.Components; using Content.Server.Popups; -using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Server.Zombies; -using Content.Shared.CCVar; using Content.Shared.Humanoid; using Content.Shared.Mind; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; -using Content.Shared.Roles; using Content.Shared.Zombies; -using Robust.Server.Player; -using Robust.Shared.Configuration; using Robust.Shared.Player; -using Robust.Shared.Random; using Robust.Shared.Timing; using System.Globalization; +using Content.Server.GameTicking.Components; namespace Content.Server.GameTicking.Rules; public sealed class ZombieRuleSystem : GameRuleSystem { - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly RoundEndSystem _roundEnd = default!; [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly ActionsSystem _action = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly ZombieSystem _zombie = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; - [Dependency] private readonly SharedRoleSystem _roles = default!; [Dependency] private readonly StationSystem _station = default!; - [Dependency] private readonly AntagSelectionSystem _antagSelection = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnStartAttempt); - SubscribeLocalEvent(OnRoundEndText); SubscribeLocalEvent(OnZombifySelf); } - /// - /// Set the required minimum players for this gamemode to start - /// - protected override void Added(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args) + protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, + ref RoundEndTextAppendEvent args) { - base.Added(uid, component, gameRule, args); + base.AppendRoundEndText(uid, component, gameRule, ref args); - gameRule.MinPlayers = _cfg.GetCVar(CCVars.ZombieMinPlayers); - } + // This is just the general condition thing used for determining the win/lose text + var fraction = GetInfectedFraction(true, true); - private void OnRoundEndText(RoundEndTextAppendEvent ev) - { - foreach (var zombie in EntityQuery()) + if (fraction <= 0) + args.AddLine(Loc.GetString("zombie-round-end-amount-none")); + else if (fraction <= 0.25) + args.AddLine(Loc.GetString("zombie-round-end-amount-low")); + else if (fraction <= 0.5) + args.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture)))); + else if (fraction < 1) + args.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture)))); + else + args.AddLine(Loc.GetString("zombie-round-end-amount-all")); + + var antags = _antag.GetAntagIdentifiers(uid); + args.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", antags.Count))); + foreach (var (_, data, entName) in antags) { - // This is just the general condition thing used for determining the win/lose text - var fraction = GetInfectedFraction(true, true); + args.AddLine(Loc.GetString("zombie-round-end-user-was-initial", + ("name", entName), + ("username", data.UserName))); + } - if (fraction <= 0) - ev.AddLine(Loc.GetString("zombie-round-end-amount-none")); - else if (fraction <= 0.25) - ev.AddLine(Loc.GetString("zombie-round-end-amount-low")); - else if (fraction <= 0.5) - ev.AddLine(Loc.GetString("zombie-round-end-amount-medium", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture)))); - else if (fraction < 1) - ev.AddLine(Loc.GetString("zombie-round-end-amount-high", ("percent", Math.Round((fraction * 100), 2).ToString(CultureInfo.InvariantCulture)))); - else - ev.AddLine(Loc.GetString("zombie-round-end-amount-all")); - - ev.AddLine(Loc.GetString("zombie-round-end-initial-count", ("initialCount", zombie.InitialInfectedNames.Count))); - foreach (var player in zombie.InitialInfectedNames) + var healthy = GetHealthyHumans(); + // Gets a bunch of the living players and displays them if they're under a threshold. + // InitialInfected is used for the threshold because it scales with the player count well. + if (healthy.Count <= 0 || healthy.Count > 2 * antags.Count) + return; + args.AddLine(""); + args.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count))); + foreach (var survivor in healthy) + { + var meta = MetaData(survivor); + var username = string.Empty; + if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null) { - ev.AddLine(Loc.GetString("zombie-round-end-user-was-initial", - ("name", player.Key), - ("username", player.Value))); + username = mind.Session.Name; } - var healthy = GetHealthyHumans(); - // Gets a bunch of the living players and displays them if they're under a threshold. - // InitialInfected is used for the threshold because it scales with the player count well. - if (healthy.Count <= 0 || healthy.Count > 2 * zombie.InitialInfectedNames.Count) - continue; - ev.AddLine(""); - ev.AddLine(Loc.GetString("zombie-round-end-survivor-count", ("count", healthy.Count))); - foreach (var survivor in healthy) - { - var meta = MetaData(survivor); - var username = string.Empty; - if (_mindSystem.TryGetMind(survivor, out _, out var mind) && mind.Session != null) - { - username = mind.Session.Name; - } - - ev.AddLine(Loc.GetString("zombie-round-end-user-was-survivor", - ("name", meta.EntityName), - ("username", username))); - } + args.AddLine(Loc.GetString("zombie-round-end-user-was-survivor", + ("name", meta.EntityName), + ("username", username))); } } @@ -134,38 +112,20 @@ public sealed class ZombieRuleSystem : GameRuleSystem _roundEnd.EndRound(); } - /// - /// Check we have enough players to start this game mode, if not - cancel and announce - /// - private void OnStartAttempt(RoundStartAttemptEvent ev) - { - TryRoundStartAttempt(ev, Loc.GetString("zombie-title")); - } - protected override void Started(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { base.Started(uid, component, gameRule, args); - var delay = _random.Next(component.MinStartDelay, component.MaxStartDelay); - component.StartTime = _timing.CurTime + delay; + component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; } protected override void ActiveTick(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, float frameTime) { base.ActiveTick(uid, component, gameRule, frameTime); - - if (component.StartTime.HasValue && component.StartTime < _timing.CurTime) - { - InfectInitialPlayers(component); - component.StartTime = null; - component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; - } - - if (component.NextRoundEndCheck.HasValue && component.NextRoundEndCheck < _timing.CurTime) - { - CheckRoundEnd(component); - component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; - } + if (!component.NextRoundEndCheck.HasValue || component.NextRoundEndCheck > _timing.CurTime) + return; + CheckRoundEnd(component); + component.NextRoundEndCheck = _timing.CurTime + component.EndCheckDelay; } private void OnZombifySelf(EntityUid uid, PendingZombieComponent component, ZombifySelfActionEvent args) @@ -232,81 +192,4 @@ public sealed class ZombieRuleSystem : GameRuleSystem } return healthy; } - - /// - /// Infects the first players with the passive zombie virus. - /// Also records their names for the end of round screen. - /// - /// - /// The reason this code is written separately is to facilitate - /// allowing this gamemode to be started midround. As such, it doesn't need - /// any information besides just running. - /// - private void InfectInitialPlayers(ZombieRuleComponent component) - { - //Get all players with initial infected enabled, and exclude those with the ZombieImmuneComponent and roles with CanBeAntag = False - var eligiblePlayers = _antagSelection.GetEligiblePlayers( - _playerManager.Sessions, - component.PatientZeroPrototypeId, - includeAllJobs: false, - customExcludeCondition: player => HasComp(player) || HasComp(player) - ); - - //And get all players, excluding ZombieImmune and roles with CanBeAntag = False - to fill any leftover initial infected slots - var allPlayers = _antagSelection.GetEligiblePlayers( - _playerManager.Sessions, - component.PatientZeroPrototypeId, - acceptableAntags: Shared.Antag.AntagAcceptability.All, - includeAllJobs: false , - ignorePreferences: true, - customExcludeCondition: HasComp - ); - - //If there are no players to choose, abort - if (allPlayers.Count == 0) - return; - - //How many initial infected should we select - var initialInfectedCount = _antagSelection.CalculateAntagCount(_playerManager.PlayerCount, component.PlayersPerInfected, component.MaxInitialInfected); - - //Choose the required number of initial infected from the eligible players, making up any shortfall by choosing from all players - var initialInfected = _antagSelection.ChooseAntags(initialInfectedCount, eligiblePlayers, allPlayers); - - //Make brain craving - MakeZombie(initialInfected, component); - - //Send the briefing, play greeting sound - _antagSelection.SendBriefing(initialInfected, Loc.GetString("zombie-patientzero-role-greeting"), Color.Plum, component.InitialInfectedSound); - } - - private void MakeZombie(List entities, ZombieRuleComponent component) - { - foreach (var entity in entities) - { - MakeZombie(entity, component); - } - } - private void MakeZombie(EntityUid entity, ZombieRuleComponent component) - { - if (!_mindSystem.TryGetMind(entity, out var mind, out var mindComponent)) - return; - - //Add the role to the mind silently (to avoid repeating job assignment) - _roles.MindAddRole(mind, new InitialInfectedRoleComponent { PrototypeId = component.PatientZeroPrototypeId }, silent: true); - EnsureComp(entity); - - //Add the zombie components and grace period - var pending = EnsureComp(entity); - pending.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace); - EnsureComp(entity); - EnsureComp(entity); - - //Add the zombify action - _action.AddAction(entity, ref pending.Action, component.ZombifySelfActionPrototype, entity); - - //Get names for the round end screen, incase they leave mid-round - var inCharacterName = MetaData(entity).EntityName; - var accountName = mindComponent.Session == null ? string.Empty : mindComponent.Session.Name; - component.InitialInfectedNames.Add(inCharacterName, accountName); - } } diff --git a/Content.Server/Geras/GerasComponent.cs b/Content.Server/Geras/GerasComponent.cs new file mode 100644 index 0000000000..eaf792502f --- /dev/null +++ b/Content.Server/Geras/GerasComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Actions; +using Content.Shared.Polymorph; +using Robust.Shared.Prototypes; + +namespace Content.Server.Geras; + +/// +/// This component assigns the entity with a polymorph action. +/// +[RegisterComponent] +public sealed partial class GerasComponent : Component +{ + [DataField] public ProtoId GerasPolymorphId = "SlimeMorphGeras"; + + [DataField] public ProtoId GerasAction = "ActionMorphGeras"; + + [DataField] public EntityUid? GerasActionEntity; +} diff --git a/Content.Server/Geras/GerasSystem.cs b/Content.Server/Geras/GerasSystem.cs new file mode 100644 index 0000000000..e25ea8f028 --- /dev/null +++ b/Content.Server/Geras/GerasSystem.cs @@ -0,0 +1,41 @@ +using Content.Server.Actions; +using Content.Server.Polymorph.Systems; +using Content.Server.Popups; +using Content.Shared.Actions; +using Content.Shared.Geras; +using Robust.Shared.Player; + +namespace Content.Server.Geras; + +/// +public sealed class GerasSystem : SharedGerasSystem +{ + [Dependency] private readonly ActionsSystem _actionsSystem = default!; + [Dependency] private readonly PolymorphSystem _polymorphSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnMorphIntoGeras); + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(EntityUid uid, GerasComponent component, MapInitEvent args) + { + // try to add geras action + _actionsSystem.AddAction(uid, ref component.GerasActionEntity, component.GerasAction); + } + + private void OnMorphIntoGeras(EntityUid uid, GerasComponent component, MorphIntoGeras args) + { + var ent = _polymorphSystem.PolymorphEntity(uid, component.GerasPolymorphId); + + if (!ent.HasValue) + return; + + _popupSystem.PopupEntity(Loc.GetString("geras-popup-morph-message-others", ("entity", ent.Value)), ent.Value, Filter.PvsExcept(ent.Value), true); + _popupSystem.PopupEntity(Loc.GetString("geras-popup-morph-message-user"), ent.Value, ent.Value); + args.Handled = true; + } +} diff --git a/Content.Server/Mapping/MappingCommand.cs b/Content.Server/Mapping/MappingCommand.cs index 08f3dcccf9..46534f7059 100644 --- a/Content.Server/Mapping/MappingCommand.cs +++ b/Content.Server/Mapping/MappingCommand.cs @@ -53,7 +53,7 @@ namespace Content.Server.Mapping } #if DEBUG - shell.WriteError(Loc.GetString("cmd-mapping-warning")); + shell.WriteLine(Loc.GetString("cmd-mapping-warning")); #endif MapId mapId; diff --git a/Content.Server/Objectives/ObjectivesSystem.cs b/Content.Server/Objectives/ObjectivesSystem.cs index 20205b8b72..47fe4eb5f8 100644 --- a/Content.Server/Objectives/ObjectivesSystem.cs +++ b/Content.Server/Objectives/ObjectivesSystem.cs @@ -1,10 +1,7 @@ using Content.Server.GameTicking; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Mind; using Content.Server.Shuttles.Systems; using Content.Shared.Cuffs.Components; using Content.Shared.Mind; -using Content.Shared.Mobs.Systems; using Content.Shared.Objectives.Components; using Content.Shared.Objectives.Systems; using Content.Shared.Random; @@ -12,7 +9,9 @@ using Content.Shared.Random.Helpers; using Robust.Shared.Prototypes; using Robust.Shared.Random; using System.Linq; +using Content.Server.GameTicking.Components; using System.Text; +using Robust.Server.Player; namespace Content.Server.Objectives; @@ -20,8 +19,8 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem { [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly MindSystem _mind = default!; [Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!; public override void Initialize() @@ -179,7 +178,9 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem .ThenByDescending(x => x.completedObjectives); foreach (var (summary, _, _) in sortedAgents) + { result.AppendLine(summary); + } } public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto) @@ -244,8 +245,14 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem return null; var name = mind.CharacterName; - _mind.TryGetSession(mindId, out var session); - var username = session?.Name; + var username = (string?) null; + + if (mind.OriginalOwnerUserId != null && + _player.TryGetPlayerData(mind.OriginalOwnerUserId.Value, out var sessionData)) + { + username = sessionData.UserName; + } + if (username != null) { diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.cs index 8cae15d70d..e6ba1d02af 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.cs @@ -199,7 +199,7 @@ public sealed partial class PolymorphSystem : EntitySystem var targetTransformComp = Transform(uid); - var child = Spawn(configuration.Entity, targetTransformComp.Coordinates); + var child = Spawn(configuration.Entity, _transform.GetMapCoordinates(uid, targetTransformComp), rotation: _transform.GetWorldRotation(uid)); MakeSentientCommand.MakeSentient(child, EntityManager); diff --git a/Content.Server/Popups/PopupSystem.cs b/Content.Server/Popups/PopupSystem.cs index d1163a2be1..4aa3d39224 100644 --- a/Content.Server/Popups/PopupSystem.cs +++ b/Content.Server/Popups/PopupSystem.cs @@ -88,11 +88,19 @@ namespace Content.Server.Popups RaiseNetworkEvent(new PopupEntityEvent(message, type, GetNetEntity(uid)), actor.PlayerSession); } + public override void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small) + { + } + public override void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small) { // do nothing duh its for client only } + public override void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small) + { + } + public override void PopupEntity(string? message, EntityUid uid, ICommonSession recipient, PopupType type = PopupType.Small) { if (message == null) diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs index 107d09c898..0e20f007d7 100644 --- a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs @@ -17,6 +17,7 @@ using Robust.Shared.Player; using Robust.Shared.Utility; using System.Linq; using System.Diagnostics.CodeAnalysis; +using Content.Server.GameTicking.Components; namespace Content.Server.Power.EntitySystems; @@ -723,8 +724,8 @@ internal sealed partial class PowerMonitoringConsoleSystem : SharedPowerMonitori } } - // Designates a supplied entity as a 'collection master'. Other entities which share this - // entities collection name and are attached on the same load network are assigned this entity + // Designates a supplied entity as a 'collection master'. Other entities which share this + // entities collection name and are attached on the same load network are assigned this entity // as the master that represents them on the console UI. This way you can have one device // represent multiple connected devices private void AssignEntityAsCollectionMaster diff --git a/Content.Server/Preferences/Managers/IServerPreferencesManager.cs b/Content.Server/Preferences/Managers/IServerPreferencesManager.cs index a36b053717..1808592ef5 100644 --- a/Content.Server/Preferences/Managers/IServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/IServerPreferencesManager.cs @@ -16,6 +16,7 @@ namespace Content.Server.Preferences.Managers bool TryGetCachedPreferences(NetUserId userId, [NotNullWhen(true)] out PlayerPreferences? playerPreferences); PlayerPreferences GetPreferences(NetUserId userId); + PlayerPreferences? GetPreferencesOrNull(NetUserId? userId); IEnumerable> GetSelectedProfilesForPlayers(List userIds); bool HavePreferencesLoaded(ICommonSession session); } diff --git a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs index 0f8cb83f10..a1eb8aad82 100644 --- a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs @@ -256,6 +256,20 @@ namespace Content.Server.Preferences.Managers return prefs; } + /// + /// Retrieves preferences for the given username from storage or returns null. + /// Creates and saves default preferences if they are not found, then returns them. + /// + public PlayerPreferences? GetPreferencesOrNull(NetUserId? userId) + { + if (userId == null) + return null; + + if (_cachedPlayerPrefs.TryGetValue(userId.Value, out var pref)) + return pref.Prefs; + return null; + } + private async Task GetOrCreatePreferencesAsync(NetUserId userId) { var prefs = await _db.GetPlayerPreferencesAsync(userId); diff --git a/Content.Server/Radio/Components/RadioJammerComponent.cs b/Content.Server/Radio/Components/RadioJammerComponent.cs deleted file mode 100644 index 93504ef957..0000000000 --- a/Content.Server/Radio/Components/RadioJammerComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Server.Radio.EntitySystems; - -namespace Content.Server.Radio.Components; - -/// -/// When activated () prevents from sending messages in range -/// -[RegisterComponent] -[Access(typeof(JammerSystem))] -public sealed partial class RadioJammerComponent : Component -{ - [DataField("range"), ViewVariables(VVAccess.ReadWrite)] - public float Range = 8f; - - /// - /// Power usage per second when enabled - /// - [DataField("wattage"), ViewVariables(VVAccess.ReadWrite)] - public float Wattage = 2f; -} diff --git a/Content.Server/Radio/EntitySystems/JammerSystem.cs b/Content.Server/Radio/EntitySystems/JammerSystem.cs index 5a2a854017..4f58cb21e1 100644 --- a/Content.Server/Radio/EntitySystems/JammerSystem.cs +++ b/Content.Server/Radio/EntitySystems/JammerSystem.cs @@ -1,26 +1,22 @@ using Content.Server.DeviceNetwork.Components; -using Content.Server.DeviceNetwork.Systems; -using Content.Server.Medical.CrewMonitoring; using Content.Server.Popups; using Content.Server.Power.EntitySystems; using Content.Server.PowerCell; using Content.Server.Radio.Components; -using Content.Server.Station.Systems; using Content.Shared.DeviceNetwork.Components; using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.PowerCell.Components; +using Content.Shared.RadioJammer; +using Content.Shared.Radio.EntitySystems; namespace Content.Server.Radio.EntitySystems; -public sealed class JammerSystem : EntitySystem +public sealed class JammerSystem : SharedJammerSystem { [Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly BatterySystem _battery = default!; - [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly StationSystem _stationSystem = default!; - [Dependency] private readonly SingletonDeviceNetServerSystem _singletonServerSystem = default!; public override void Initialize() { @@ -35,14 +31,37 @@ public sealed class JammerSystem : EntitySystem public override void Update(float frameTime) { var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var _, out var jam)) { - if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery) && - !_battery.TryUseCharge(batteryUid.Value, jam.Wattage * frameTime, battery)) + + if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery)) { - RemComp(uid); - RemComp(uid); + if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage(jam) * frameTime, battery)) + { + ChangeLEDState(false, uid); + RemComp(uid); + RemComp(uid); + } + else + { + var percentCharged = battery.CurrentCharge / battery.MaxCharge; + if (percentCharged > .50) + { + ChangeChargeLevel(RadioJammerChargeLevel.High, uid); + } + else if (percentCharged < .15) + { + ChangeChargeLevel(RadioJammerChargeLevel.Low, uid); + } + else + { + ChangeChargeLevel(RadioJammerChargeLevel.Medium, uid); + } + } + } + } } @@ -50,40 +69,49 @@ public sealed class JammerSystem : EntitySystem { var activated = !HasComp(uid) && _powerCell.TryGetBatteryFromSlot(uid, out var battery) && - battery.CurrentCharge > comp.Wattage; + battery.CurrentCharge > GetCurrentWattage(comp); if (activated) { + ChangeLEDState(true, uid); EnsureComp(uid); EnsureComp(uid, out var jammingComp); - jammingComp.Range = comp.Range; + jammingComp.Range = GetCurrentRange(comp); jammingComp.JammableNetworks.Add(DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString()); Dirty(uid, jammingComp); } else { - RemComp(uid); - RemComp(uid); + ChangeLEDState(false, uid); + RemCompDeferred(uid); + RemCompDeferred(uid); } var state = Loc.GetString(activated ? "radio-jammer-component-on-state" : "radio-jammer-component-off-state"); var message = Loc.GetString("radio-jammer-component-on-use", ("state", state)); - _popup.PopupEntity(message, args.User, args.User); + Popup.PopupEntity(message, args.User, args.User); args.Handled = true; } private void OnPowerCellChanged(EntityUid uid, ActiveRadioJammerComponent comp, PowerCellChangedEvent args) { if (args.Ejected) - RemComp(uid); + { + ChangeLEDState(false, uid); + RemCompDeferred(uid); + } } private void OnExamine(EntityUid uid, RadioJammerComponent comp, ExaminedEvent args) { if (args.IsInDetailsRange) { - var msg = HasComp(uid) + var powerIndicator = HasComp(uid) ? Loc.GetString("radio-jammer-component-examine-on-state") : Loc.GetString("radio-jammer-component-examine-off-state"); - args.PushMarkup(msg); + args.PushMarkup(powerIndicator); + + var powerLevel = Loc.GetString(comp.Settings[comp.SelectedPowerLevel].Name); + var switchIndicator = Loc.GetString("radio-jammer-component-switch-setting", ("powerLevel", powerLevel)); + args.PushMarkup(switchIndicator); } } @@ -102,7 +130,7 @@ public sealed class JammerSystem : EntitySystem while (query.MoveNext(out _, out _, out var jam, out var transform)) { - if (source.InRange(EntityManager, _transform, transform.Coordinates, jam.Range)) + if (source.InRange(EntityManager, _transform, transform.Coordinates, GetCurrentRange(jam))) { return true; } diff --git a/Content.Server/RandomMetadata/RandomMetadataSystem.cs b/Content.Server/RandomMetadata/RandomMetadataSystem.cs index c088d57fd9..0c254c52ac 100644 --- a/Content.Server/RandomMetadata/RandomMetadataSystem.cs +++ b/Content.Server/RandomMetadata/RandomMetadataSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Dataset; +using Content.Shared.Dataset; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -47,9 +47,12 @@ public sealed class RandomMetadataSystem : EntitySystem var outputSegments = new List(); foreach (var segment in segments) { - outputSegments.Add(_prototype.TryIndex(segment, out var proto) - ? Loc.GetString(_random.Pick(proto.Values)) - : Loc.GetString(segment)); + if (_prototype.TryIndex(segment, out var proto)) + outputSegments.Add(_random.Pick(proto.Values)); + else if (Loc.TryGetString(segment, out var localizedSegment)) + outputSegments.Add(localizedSegment); + else + outputSegments.Add(segment); } return string.Join(separator, outputSegments); } diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs index ffd5f75bf2..68a2624500 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.Abilities.cs @@ -238,7 +238,7 @@ public sealed partial class RevenantSystem { //hardcoded damage specifiers til i die. var dspec = new DamageSpecifier(); - dspec.DamageDict.Add("Structural", 15); + dspec.DamageDict.Add("Structural", 60); _damage.TryChangeDamage(ent, dspec, origin: uid); } diff --git a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs index 506fd61d55..75f8618798 100644 --- a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs +++ b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs @@ -1,5 +1,6 @@ using System.Numerics; using Content.Server.GameTicking; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Spawners.Components; using JetBrains.Annotations; diff --git a/Content.Server/Speech/Components/PirateAccentComponent.cs b/Content.Server/Speech/Components/PirateAccentComponent.cs index 0559d9854b..b5b292775d 100644 --- a/Content.Server/Speech/Components/PirateAccentComponent.cs +++ b/Content.Server/Speech/Components/PirateAccentComponent.cs @@ -15,6 +15,7 @@ public sealed partial class PirateAccentComponent : Component { "accent-pirate-prefix-1", "accent-pirate-prefix-2", - "accent-pirate-prefix-3" + "accent-pirate-prefix-3", + "accent-pirate-prefix-4", }; } diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs index 36d30f50ee..e7e0957239 100644 --- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Administration; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs deleted file mode 100644 index 92911e0858..0000000000 --- a/Content.Server/StationEvents/Components/LoneOpsSpawnRuleComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Content.Server.StationEvents.Events; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.StationEvents.Components; - -[RegisterComponent, Access(typeof(LoneOpsSpawnRule))] -public sealed partial class LoneOpsSpawnRuleComponent : Component -{ - [DataField("loneOpsShuttlePath")] - public string LoneOpsShuttlePath = "Maps/Shuttles/striker.yml"; - - [DataField("gameRuleProto", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string GameRuleProto = "Nukeops"; - - [DataField("additionalRule")] - public EntityUid? AdditionalRule; -} diff --git a/Content.Server/StationEvents/Events/AnomalySpawnRule.cs b/Content.Server/StationEvents/Events/AnomalySpawnRule.cs index 48a3b900c4..96633834ee 100644 --- a/Content.Server/StationEvents/Events/AnomalySpawnRule.cs +++ b/Content.Server/StationEvents/Events/AnomalySpawnRule.cs @@ -1,4 +1,5 @@ using Content.Server.Anomaly; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs b/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs index 0eed77f154..b3ed10999e 100644 --- a/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs +++ b/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; +using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Robust.Shared.Random; diff --git a/Content.Server/StationEvents/Events/BluespaceLockerRule.cs b/Content.Server/StationEvents/Events/BluespaceLockerRule.cs index 709b750334..eef9850e73 100644 --- a/Content.Server/StationEvents/Events/BluespaceLockerRule.cs +++ b/Content.Server/StationEvents/Events/BluespaceLockerRule.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; +using Content.Server.GameTicking.Rules.Components; using Content.Server.Resist; using Content.Server.Station.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/BreakerFlipRule.cs b/Content.Server/StationEvents/Events/BreakerFlipRule.cs index 494779fe35..16d3fd8c95 100644 --- a/Content.Server/StationEvents/Events/BreakerFlipRule.cs +++ b/Content.Server/StationEvents/Events/BreakerFlipRule.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; +using Content.Server.GameTicking.Rules.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Station.Components; diff --git a/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs b/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs index 249a14a9b8..ccfb8aee58 100644 --- a/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs +++ b/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; using Content.Server.Station.Systems; diff --git a/Content.Server/StationEvents/Events/CargoGiftsRule.cs b/Content.Server/StationEvents/Events/CargoGiftsRule.cs index 194786fca7..c27cd30278 100644 --- a/Content.Server/StationEvents/Events/CargoGiftsRule.cs +++ b/Content.Server/StationEvents/Events/CargoGiftsRule.cs @@ -2,6 +2,7 @@ using System.Linq; using Content.Server.Cargo.Components; using Content.Server.Cargo.Systems; using Content.Server.GameTicking; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; using Content.Server.StationEvents.Components; @@ -69,7 +70,7 @@ public sealed class CargoGiftsRule : StationEventSystem Loc.GetString(component.Description), Loc.GetString(component.Dest), cargoDb, - stationData! + (station.Value, stationData) )) { break; diff --git a/Content.Server/StationEvents/Events/ClericalErrorRule.cs b/Content.Server/StationEvents/Events/ClericalErrorRule.cs index dd4473952c..854ee685b3 100644 --- a/Content.Server/StationEvents/Events/ClericalErrorRule.cs +++ b/Content.Server/StationEvents/Events/ClericalErrorRule.cs @@ -1,4 +1,5 @@ -using Content.Server.GameTicking.Rules.Components; +using Content.Server.GameTicking.Components; +using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Content.Server.StationRecords; using Content.Server.StationRecords.Systems; diff --git a/Content.Server/StationEvents/Events/FalseAlarmRule.cs b/Content.Server/StationEvents/Events/FalseAlarmRule.cs index 05e9435b40..e5317a5449 100644 --- a/Content.Server/StationEvents/Events/FalseAlarmRule.cs +++ b/Content.Server/StationEvents/Events/FalseAlarmRule.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using JetBrains.Annotations; diff --git a/Content.Server/StationEvents/Events/GasLeakRule.cs b/Content.Server/StationEvents/Events/GasLeakRule.cs index 68544e416c..1221612171 100644 --- a/Content.Server/StationEvents/Events/GasLeakRule.cs +++ b/Content.Server/StationEvents/Events/GasLeakRule.cs @@ -1,4 +1,5 @@ using Content.Server.Atmos.EntitySystems; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Robust.Shared.Audio; diff --git a/Content.Server/StationEvents/Events/ImmovableRodRule.cs b/Content.Server/StationEvents/Events/ImmovableRodRule.cs index 1b8fb6be1f..cacb839cd3 100644 --- a/Content.Server/StationEvents/Events/ImmovableRodRule.cs +++ b/Content.Server/StationEvents/Events/ImmovableRodRule.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.ImmovableRod; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/IonStormRule.cs b/Content.Server/StationEvents/Events/IonStormRule.cs index cd3cd63ae8..8361cc6048 100644 --- a/Content.Server/StationEvents/Events/IonStormRule.cs +++ b/Content.Server/StationEvents/Events/IonStormRule.cs @@ -1,5 +1,5 @@ +using Content.Server.GameTicking.Components; using System.Linq; -using Content.Server.GameTicking.Rules.Components; using Content.Server.Silicons.Laws; using Content.Server.Station.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs index 3fa12cd4e9..5b56e03846 100644 --- a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs +++ b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs b/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs deleted file mode 100644 index 4b15e59099..0000000000 --- a/Content.Server/StationEvents/Events/LoneOpsSpawnRule.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Robust.Server.GameObjects; -using Robust.Server.Maps; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.StationEvents.Components; -using Content.Server.RoundEnd; - -namespace Content.Server.StationEvents.Events; - -public sealed class LoneOpsSpawnRule : StationEventSystem -{ - [Dependency] private readonly MapLoaderSystem _map = default!; - - protected override void Started(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) - { - base.Started(uid, component, gameRule, args); - - // Loneops can only spawn if there is no nukeops active - if (GameTicker.IsGameRuleAdded()) - { - ForceEndSelf(uid, gameRule); - return; - } - - var shuttleMap = MapManager.CreateMap(); - var options = new MapLoadOptions - { - LoadMap = true, - }; - - _map.TryLoad(shuttleMap, component.LoneOpsShuttlePath, out _, options); - - var nukeopsEntity = GameTicker.AddGameRule(component.GameRuleProto); - component.AdditionalRule = nukeopsEntity; - var nukeopsComp = Comp(nukeopsEntity); - nukeopsComp.SpawnOutpost = false; - nukeopsComp.RoundEndBehavior = RoundEndBehavior.Nothing; - GameTicker.StartGameRule(nukeopsEntity); - } - - protected override void Ended(EntityUid uid, LoneOpsSpawnRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args) - { - base.Ended(uid, component, gameRule, args); - - if (component.AdditionalRule != null) - GameTicker.EndGameRule(component.AdditionalRule.Value); - } -} diff --git a/Content.Server/StationEvents/Events/MassHallucinationsRule.cs b/Content.Server/StationEvents/Events/MassHallucinationsRule.cs index 722a489541..d6f609bee1 100644 --- a/Content.Server/StationEvents/Events/MassHallucinationsRule.cs +++ b/Content.Server/StationEvents/Events/MassHallucinationsRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Content.Server.Traits.Assorted; diff --git a/Content.Server/StationEvents/Events/MeteorSwarmRule.cs b/Content.Server/StationEvents/Events/MeteorSwarmRule.cs index ad56479b37..455011259d 100644 --- a/Content.Server/StationEvents/Events/MeteorSwarmRule.cs +++ b/Content.Server/StationEvents/Events/MeteorSwarmRule.cs @@ -1,4 +1,5 @@ using System.Numerics; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Robust.Shared.Map; diff --git a/Content.Server/StationEvents/Events/NinjaSpawnRule.cs b/Content.Server/StationEvents/Events/NinjaSpawnRule.cs index 8ad5c8602e..d9d68a386c 100644 --- a/Content.Server/StationEvents/Events/NinjaSpawnRule.cs +++ b/Content.Server/StationEvents/Events/NinjaSpawnRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Ninja.Systems; using Content.Server.Station.Components; diff --git a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs index 5503438df8..d547fc9446 100644 --- a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs +++ b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs @@ -1,4 +1,5 @@ using System.Threading; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; diff --git a/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs b/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs index c3cd719cc4..87d50fc8b2 100644 --- a/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs +++ b/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; using Content.Server.Storage.Components; diff --git a/Content.Server/StationEvents/Events/RandomSentienceRule.cs b/Content.Server/StationEvents/Events/RandomSentienceRule.cs index 4b7606d01f..06bb470602 100644 --- a/Content.Server/StationEvents/Events/RandomSentienceRule.cs +++ b/Content.Server/StationEvents/Events/RandomSentienceRule.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Ghost.Roles.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/RandomSpawnRule.cs b/Content.Server/StationEvents/Events/RandomSpawnRule.cs index c514acc623..77744d44e4 100644 --- a/Content.Server/StationEvents/Events/RandomSpawnRule.cs +++ b/Content.Server/StationEvents/Events/RandomSpawnRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/SolarFlareRule.cs b/Content.Server/StationEvents/Events/SolarFlareRule.cs index a4ec74b43b..0370b4ee61 100644 --- a/Content.Server/StationEvents/Events/SolarFlareRule.cs +++ b/Content.Server/StationEvents/Events/SolarFlareRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Radio; using Robust.Shared.Random; diff --git a/Content.Server/StationEvents/Events/StationEventSystem.cs b/Content.Server/StationEvents/Events/StationEventSystem.cs index 7f05f8940d..cbdae9e9e3 100644 --- a/Content.Server/StationEvents/Events/StationEventSystem.cs +++ b/Content.Server/StationEvents/Events/StationEventSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Systems; diff --git a/Content.Server/StationEvents/Events/VentClogRule.cs b/Content.Server/StationEvents/Events/VentClogRule.cs index e263a5f4f6..867f41dccc 100644 --- a/Content.Server/StationEvents/Events/VentClogRule.cs +++ b/Content.Server/StationEvents/Events/VentClogRule.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using Robust.Shared.Random; using System.Linq; using Content.Server.Fluids.EntitySystems; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationEvents/Events/VentCrittersRule.cs b/Content.Server/StationEvents/Events/VentCrittersRule.cs index cdcf2bf6ff..c2605039bc 100644 --- a/Content.Server/StationEvents/Events/VentCrittersRule.cs +++ b/Content.Server/StationEvents/Events/VentCrittersRule.cs @@ -1,3 +1,4 @@ +using Content.Server.GameTicking.Components; using Content.Server.StationEvents.Components; using Content.Server.GameTicking.Rules.Components; using Content.Server.Station.Components; diff --git a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs index ef3b5cf18a..6c1ad4f489 100644 --- a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs +++ b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs @@ -1,4 +1,5 @@ using Content.Server.GameTicking; +using Content.Server.GameTicking.Components; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; diff --git a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs index 67f50d7a4e..58c4c876c5 100644 --- a/Content.Server/StationRecords/Systems/StationRecordsSystem.cs +++ b/Content.Server/StationRecords/Systems/StationRecordsSystem.cs @@ -211,7 +211,7 @@ public sealed class StationRecordsSystem : SharedStationRecordsSystem /// public uint? GetRecordByName(EntityUid station, string name, StationRecordsComponent? records = null) { - if (!Resolve(station, ref records)) + if (!Resolve(station, ref records, false)) return null; foreach (var (id, record) in GetRecordsOfType(station, records)) diff --git a/Content.Server/Storage/EntitySystems/StorageSystem.cs b/Content.Server/Storage/EntitySystems/StorageSystem.cs index 27694ee61c..5d41e0a521 100644 --- a/Content.Server/Storage/EntitySystems/StorageSystem.cs +++ b/Content.Server/Storage/EntitySystems/StorageSystem.cs @@ -76,13 +76,13 @@ public sealed partial class StorageSystem : SharedStorageSystem }; if (uiOpen) { - verb.Text = Loc.GetString("verb-common-close-ui"); + verb.Text = Loc.GetString("comp-storage-verb-close-storage"); verb.Icon = new SpriteSpecifier.Texture( new("/Textures/Interface/VerbIcons/close.svg.192dpi.png")); } else { - verb.Text = Loc.GetString("verb-common-open-ui"); + verb.Text = Loc.GetString("comp-storage-verb-open-storage"); verb.Icon = new SpriteSpecifier.Texture( new("/Textures/Interface/VerbIcons/open.svg.192dpi.png")); } diff --git a/Content.Server/Traitor/Systems/AutoTraitorSystem.cs b/Content.Server/Traitor/Systems/AutoTraitorSystem.cs index 15deae2552..e9307effbc 100644 --- a/Content.Server/Traitor/Systems/AutoTraitorSystem.cs +++ b/Content.Server/Traitor/Systems/AutoTraitorSystem.cs @@ -1,6 +1,7 @@ -using Content.Server.GameTicking.Rules; +using Content.Server.Antag; using Content.Server.Traitor.Components; using Content.Shared.Mind.Components; +using Robust.Shared.Prototypes; namespace Content.Server.Traitor.Systems; @@ -9,7 +10,10 @@ namespace Content.Server.Traitor.Systems; /// public sealed class AutoTraitorSystem : EntitySystem { - [Dependency] private readonly TraitorRuleSystem _traitorRule = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; + + [ValidatePrototypeId] + private const string DefaultTraitorRule = "Traitor"; public override void Initialize() { @@ -20,44 +24,6 @@ public sealed class AutoTraitorSystem : EntitySystem private void OnMindAdded(EntityUid uid, AutoTraitorComponent comp, MindAddedMessage args) { - TryMakeTraitor(uid, comp); - } - - /// - /// Sets the GiveUplink field. - /// - public void SetGiveUplink(EntityUid uid, bool giveUplink, AutoTraitorComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return; - - comp.GiveUplink = giveUplink; - } - - /// - /// Sets the GiveObjectives field. - /// - public void SetGiveObjectives(EntityUid uid, bool giveObjectives, AutoTraitorComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return; - - comp.GiveObjectives = giveObjectives; - } - - /// - /// Checks if there is a mind, then makes it a traitor using the options. - /// - public bool TryMakeTraitor(EntityUid uid, AutoTraitorComponent? comp = null) - { - if (!Resolve(uid, ref comp)) - return false; - - //Start the rule if it has not already been started - var traitorRuleComponent = _traitorRule.StartGameRule(); - _traitorRule.MakeTraitor(uid, traitorRuleComponent, giveUplink: comp.GiveUplink, giveObjectives: comp.GiveObjectives); - // prevent spamming anything if it fails - RemComp(uid); - return true; + _antag.ForceMakeAntag(args.Mind.Comp.Session, DefaultTraitorRule); } } diff --git a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs index cdaed3f928..79192f6b49 100644 --- a/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs +++ b/Content.Server/Traitor/Uplink/Commands/AddUplinkCommand.cs @@ -83,12 +83,9 @@ namespace Content.Server.Traitor.Uplink.Commands uplinkEntity = eUid; } - // Get TC count - var tcCount = _cfgManager.GetCVar(CCVars.TraitorStartingBalance); - Logger.Debug(_entManager.ToPrettyString(user)); // Finally add uplink var uplinkSys = _entManager.System(); - if (!uplinkSys.AddUplink(user, FixedPoint2.New(tcCount), uplinkEntity: uplinkEntity)) + if (!uplinkSys.AddUplink(user, 20, uplinkEntity: uplinkEntity)) { shell.WriteLine(Loc.GetString("add-uplink-command-error-2")); } diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactExamineTriggerSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactExamineTriggerSystem.cs index cbade1682e..b7afbcfc8b 100644 --- a/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactExamineTriggerSystem.cs +++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Triggers/Systems/ArtifactExamineTriggerSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components; using Content.Shared.Examine; +using Content.Shared.Ghost; namespace Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Systems; @@ -15,6 +16,10 @@ public sealed class ArtifactExamineTriggerSystem : EntitySystem private void OnExamine(EntityUid uid, ArtifactExamineTriggerComponent component, ExaminedEvent args) { + // Prevent ghosts from activating this trigger unless they have CanGhostInteract + if (TryComp(args.Examiner, out var ghost) && !ghost.CanGhostInteract) + return; + _artifact.TryActivateArtifact(uid); } } diff --git a/Content.Server/Zombies/PendingZombieComponent.cs b/Content.Server/Zombies/PendingZombieComponent.cs index 10b62c05dc..98eae74f06 100644 --- a/Content.Server/Zombies/PendingZombieComponent.cs +++ b/Content.Server/Zombies/PendingZombieComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Damage; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Server.Zombies; @@ -35,6 +36,21 @@ public sealed partial class PendingZombieComponent : Component [DataField("gracePeriod"), ViewVariables(VVAccess.ReadWrite)] public TimeSpan GracePeriod = TimeSpan.Zero; + /// + /// The minimum amount of time initial infected have before they start taking infection damage. + /// + [DataField] + public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f); + + /// + /// The maximum amount of time initial infected have before they start taking damage. + /// + [DataField] + public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f); + + [DataField] + public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead"; + /// /// The chance each second that a warning will be shown. /// diff --git a/Content.Server/Zombies/ZombieSystem.cs b/Content.Server/Zombies/ZombieSystem.cs index 080bef44e7..09c8fa26db 100644 --- a/Content.Server/Zombies/ZombieSystem.cs +++ b/Content.Server/Zombies/ZombieSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using Content.Server.Actions; using Content.Server.Body.Systems; using Content.Server.Chat; using Content.Server.Chat.Systems; @@ -30,6 +31,7 @@ namespace Content.Server.Zombies [Dependency] private readonly BloodstreamSystem _bloodstream = default!; [Dependency] private readonly DamageableSystem _damageable = default!; [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly AutoEmoteSystem _autoEmote = default!; [Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; @@ -74,6 +76,8 @@ namespace Content.Server.Zombies } component.NextTick = _timing.CurTime + TimeSpan.FromSeconds(1f); + component.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace); + _actions.AddAction(uid, ref component.Action, component.ZombifySelfActionPrototype); } public override void Update(float frameTime) diff --git a/Content.Shared/Actions/Events/ValidateActionEntityTargetEvent.cs b/Content.Shared/Actions/Events/ValidateActionEntityTargetEvent.cs new file mode 100644 index 0000000000..9f22e7973a --- /dev/null +++ b/Content.Shared/Actions/Events/ValidateActionEntityTargetEvent.cs @@ -0,0 +1,4 @@ +namespace Content.Shared.Actions.Events; + +[ByRefEvent] +public record struct ValidateActionEntityTargetEvent(EntityUid User, EntityUid Target, bool Cancelled = false); diff --git a/Content.Shared/Actions/Events/ValidateActionWorldTargetEvent.cs b/Content.Shared/Actions/Events/ValidateActionWorldTargetEvent.cs new file mode 100644 index 0000000000..43e398aad4 --- /dev/null +++ b/Content.Shared/Actions/Events/ValidateActionWorldTargetEvent.cs @@ -0,0 +1,6 @@ +using Robust.Shared.Map; + +namespace Content.Shared.Actions.Events; + +[ByRefEvent] +public record struct ValidateActionWorldTargetEvent(EntityUid User, EntityCoordinates Target, bool Cancelled = false); diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 9f3fb96410..e1b76f517e 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -8,14 +8,13 @@ using Content.Shared.Hands; using Content.Shared.Interaction; using Content.Shared.Inventory.Events; using Content.Shared.Mind; -using Content.Shared.Mobs.Components; +using Content.Shared.Rejuvenate; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Timing; using Robust.Shared.Utility; -using Content.Shared.Rejuvenate; namespace Content.Shared.Actions; @@ -389,7 +388,7 @@ public abstract class SharedActionsSystem : EntitySystem var targetWorldPos = _transformSystem.GetWorldPosition(entityTarget); _rotateToFaceSystem.TryFaceCoordinates(user, targetWorldPos); - if (!ValidateEntityTarget(user, entityTarget, entityAction)) + if (!ValidateEntityTarget(user, entityTarget, (actionEnt, entityAction))) return; _adminLogger.Add(LogType.Action, @@ -413,7 +412,7 @@ public abstract class SharedActionsSystem : EntitySystem var entityCoordinatesTarget = GetCoordinates(netCoordinatesTarget); _rotateToFaceSystem.TryFaceCoordinates(user, entityCoordinatesTarget.ToMapPos(EntityManager, _transformSystem)); - if (!ValidateWorldTarget(user, entityCoordinatesTarget, worldAction)) + if (!ValidateWorldTarget(user, entityCoordinatesTarget, (actionEnt, worldAction))) return; _adminLogger.Add(LogType.Action, @@ -445,7 +444,17 @@ public abstract class SharedActionsSystem : EntitySystem PerformAction(user, component, actionEnt, action, performEvent, curTime); } - public bool ValidateEntityTarget(EntityUid user, EntityUid target, EntityTargetActionComponent action) + public bool ValidateEntityTarget(EntityUid user, EntityUid target, Entity actionEnt) + { + if (!ValidateEntityTargetBase(user, target, actionEnt)) + return false; + + var ev = new ValidateActionEntityTargetEvent(user, target); + RaiseLocalEvent(actionEnt, ref ev); + return !ev.Cancelled; + } + + private bool ValidateEntityTargetBase(EntityUid user, EntityUid target, EntityTargetActionComponent action) { if (!target.IsValid() || Deleted(target)) return false; @@ -484,7 +493,17 @@ public abstract class SharedActionsSystem : EntitySystem return _interactionSystem.CanAccessViaStorage(user, target); } - public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, WorldTargetActionComponent action) + public bool ValidateWorldTarget(EntityUid user, EntityCoordinates coords, Entity action) + { + if (!ValidateWorldTargetBase(user, coords, action)) + return false; + + var ev = new ValidateActionWorldTargetEvent(user, coords); + RaiseLocalEvent(action, ref ev); + return !ev.Cancelled; + } + + private bool ValidateWorldTargetBase(EntityUid user, EntityCoordinates coords, WorldTargetActionComponent action) { if (action.CheckCanInteract && !_actionBlockerSystem.CanInteract(user, null)) return false; diff --git a/Content.Shared/Antag/AntagAcceptability.cs b/Content.Shared/Antag/AntagAcceptability.cs index 98abe713eb..02d0b5f58f 100644 --- a/Content.Shared/Antag/AntagAcceptability.cs +++ b/Content.Shared/Antag/AntagAcceptability.cs @@ -20,3 +20,8 @@ public enum AntagAcceptability All } +public enum AntagSelectionTime : byte +{ + PrePlayerSpawn, + PostPlayerSpawn +} diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 83f8ad3b9d..ef7cdf7b61 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -403,91 +403,6 @@ namespace Content.Shared.CCVar public static readonly CVarDef DiscordRoundEndRoleWebhook = CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY); - - /* - * Suspicion - */ - - public static readonly CVarDef SuspicionMinPlayers = - CVarDef.Create("suspicion.min_players", 5); - - public static readonly CVarDef SuspicionMinTraitors = - CVarDef.Create("suspicion.min_traitors", 2); - - public static readonly CVarDef SuspicionPlayersPerTraitor = - CVarDef.Create("suspicion.players_per_traitor", 6); - - public static readonly CVarDef SuspicionStartingBalance = - CVarDef.Create("suspicion.starting_balance", 20); - - public static readonly CVarDef SuspicionMaxTimeSeconds = - CVarDef.Create("suspicion.max_time_seconds", 300); - - /* - * Traitor - */ - - public static readonly CVarDef TraitorMinPlayers = - CVarDef.Create("traitor.min_players", 5); - - public static readonly CVarDef TraitorMaxTraitors = - CVarDef.Create("traitor.max_traitors", 12); // Assuming average server maxes somewhere from like 50-80 people - - public static readonly CVarDef TraitorPlayersPerTraitor = - CVarDef.Create("traitor.players_per_traitor", 10); - - public static readonly CVarDef TraitorCodewordCount = - CVarDef.Create("traitor.codeword_count", 4); - - public static readonly CVarDef TraitorStartingBalance = - CVarDef.Create("traitor.starting_balance", 20); - - public static readonly CVarDef TraitorMaxDifficulty = - CVarDef.Create("traitor.max_difficulty", 5); - - public static readonly CVarDef TraitorMaxPicks = - CVarDef.Create("traitor.max_picks", 20); - - public static readonly CVarDef TraitorStartDelay = - CVarDef.Create("traitor.start_delay", 4f * 60f); - - public static readonly CVarDef TraitorStartDelayVariance = - CVarDef.Create("traitor.start_delay_variance", 3f * 60f); - - /* - * TraitorDeathMatch - */ - - public static readonly CVarDef TraitorDeathMatchStartingBalance = - CVarDef.Create("traitordm.starting_balance", 20); - - /* - * Zombie - */ - - public static readonly CVarDef ZombieMinPlayers = - CVarDef.Create("zombie.min_players", 20); - - /* - * Pirates - */ - - public static readonly CVarDef PiratesMinPlayers = - CVarDef.Create("pirates.min_players", 25); - - public static readonly CVarDef PiratesMaxOps = - CVarDef.Create("pirates.max_pirates", 6); - - public static readonly CVarDef PiratesPlayersPerOp = - CVarDef.Create("pirates.players_per_pirate", 5); - - /* - * Nukeops - */ - - public static readonly CVarDef NukeopsSpawnGhostRoles = - CVarDef.Create("nukeops.spawn_ghost_roles", false); - /* * Tips */ diff --git a/Content.Shared/Cargo/CargoOrderData.cs b/Content.Shared/Cargo/CargoOrderData.cs index 831010cedd..ce05d92236 100644 --- a/Content.Shared/Cargo/CargoOrderData.cs +++ b/Content.Shared/Cargo/CargoOrderData.cs @@ -1,47 +1,55 @@ using Robust.Shared.Serialization; -using Content.Shared.Access.Components; using System.Text; namespace Content.Shared.Cargo { - [NetSerializable, Serializable] - public sealed class CargoOrderData + [DataDefinition, NetSerializable, Serializable] + public sealed partial class CargoOrderData { /// /// Price when the order was added. /// + [DataField] public int Price; /// /// A unique (arbitrary) ID which identifies this order. /// - public readonly int OrderId; + [DataField] + public int OrderId { get; private set; } /// /// Prototype Id for the item to be created /// - public readonly string ProductId; + [DataField] + public string ProductId { get; private set; } /// /// Prototype Name /// - public readonly string ProductName; + [DataField] + public string ProductName { get; private set; } /// /// The number of items in the order. Not readonly, as it might change /// due to caps on the amount of orders that can be placed. /// + [DataField] public int OrderQuantity; /// /// How many instances of this order that we've already dispatched /// + [DataField] public int NumDispatched = 0; - public readonly string Requester; + [DataField] + public string Requester { get; private set; } // public String RequesterRank; // TODO Figure out how to get Character ID card data // public int RequesterId; - public readonly string Reason; + [DataField] + public string Reason { get; private set; } public bool Approved => Approver is not null; + [DataField] public string? Approver; public CargoOrderData(int orderId, string productId, string productName, int price, int amount, string requester, string reason) diff --git a/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs b/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs index 911ea41cca..5bc3934768 100644 --- a/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs +++ b/Content.Shared/Cargo/Components/SharedCargoTelepadComponent.cs @@ -12,11 +12,14 @@ namespace Content.Shared.Cargo.Components; [RegisterComponent, NetworkedComponent, Access(typeof(SharedCargoSystem))] public sealed partial class CargoTelepadComponent : Component { + [DataField] + public List CurrentOrders = new(); + /// /// The actual amount of time it takes to teleport from the telepad /// [DataField("delay"), ViewVariables(VVAccess.ReadWrite)] - public float Delay = 10f; + public float Delay = 5f; /// /// How much time we've accumulated until next teleport. diff --git a/Content.Shared/Climbing/Systems/BonkSystem.cs b/Content.Shared/Climbing/Systems/BonkSystem.cs index ea4e04c621..f59fe92573 100644 --- a/Content.Shared/Climbing/Systems/BonkSystem.cs +++ b/Content.Shared/Climbing/Systems/BonkSystem.cs @@ -107,17 +107,16 @@ public sealed partial class BonkSystem : EntitySystem var doAfterArgs = new DoAfterArgs(EntityManager, user, bonkableComponent.BonkDelay, new BonkDoAfterEvent(), uid, target: uid, used: climber) { BreakOnMove = true, - BreakOnDamage = true + BreakOnDamage = true, + DuplicateCondition = DuplicateConditions.SameTool | DuplicateConditions.SameTarget }; - _doAfter.TryStartDoAfter(doAfterArgs); - - return true; + return _doAfter.TryStartDoAfter(doAfterArgs); } - private void OnAttemptClimb(EntityUid uid, BonkableComponent component, AttemptClimbEvent args) + private void OnAttemptClimb(EntityUid uid, BonkableComponent component, ref AttemptClimbEvent args) { - if (args.Cancelled || !HasComp(args.Climber) || !HasComp(args.User)) + if (args.Cancelled) return; if (TryStartBonk(uid, args.User, args.Climber, component)) diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs index 1bdaecf730..ac01c4e9ac 100644 --- a/Content.Shared/Climbing/Systems/ClimbSystem.cs +++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs @@ -222,7 +222,8 @@ public sealed partial class ClimbSystem : VirtualController used: entityToMove) { BreakOnMove = true, - BreakOnDamage = true + BreakOnDamage = true, + DuplicateCondition = DuplicateConditions.SameTool | DuplicateConditions.SameTarget }; _audio.PlayPredicted(comp.StartClimbSound, climbable, user); diff --git a/Content.Shared/DoAfter/DoAfterEvent.cs b/Content.Shared/DoAfter/DoAfterEvent.cs index c01505f9b2..bc9abdab87 100644 --- a/Content.Shared/DoAfter/DoAfterEvent.cs +++ b/Content.Shared/DoAfter/DoAfterEvent.cs @@ -73,7 +73,7 @@ public sealed partial class DoAfterAttemptEvent : CancellableEntityEvent public readonly DoAfter DoAfter; /// - /// The event that the DoAfter will raise after sucesfully finishing. Given that this event has the data + /// The event that the DoAfter will raise after successfully finishing. Given that this event has the data /// required to perform the interaction, it should also contain the data required to validate/attempt the /// interaction. /// diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs index 455491f524..4f77a271b3 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs @@ -104,6 +104,7 @@ public abstract partial class SharedDoAfterSystem : EntitySystem doAfter.AttemptEvent = _factory.CreateInstance(evType, new object[] { doAfter, args.Event }); } + args.Event.DoAfter = doAfter; if (args.EventTarget != null) RaiseLocalEvent(args.EventTarget.Value, doAfter.AttemptEvent, args.Broadcast); else diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs index d20da49485..9ad649683d 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs @@ -245,8 +245,9 @@ public abstract partial class SharedDoAfterSystem : EntitySystem if (args.AttemptFrequency == AttemptFrequency.StartAndEnd && !TryAttemptEvent(doAfter)) return false; - if (args.Delay <= TimeSpan.Zero || - _tag.HasTag(args.User, "InstantDoAfters")) + // TODO DO AFTER + // Why does this tag exist? Just make this a bool on the component? + if (args.Delay <= TimeSpan.Zero || _tag.HasTag(args.User, "InstantDoAfters")) { RaiseDoAfterEvents(doAfter, comp); // We don't store instant do-afters. This is just a lazy way of hiding them from client-side visuals. diff --git a/Content.Shared/Geras/SharedGerasSystem.cs b/Content.Shared/Geras/SharedGerasSystem.cs new file mode 100644 index 0000000000..f5dea466a2 --- /dev/null +++ b/Content.Shared/Geras/SharedGerasSystem.cs @@ -0,0 +1,16 @@ +using Content.Shared.Actions; + +namespace Content.Shared.Geras; + +/// +/// A Geras is the small morph of a slime. This system handles exactly that. +/// +public abstract class SharedGerasSystem : EntitySystem +{ + +} + +public sealed partial class MorphIntoGeras : InstantActionEvent +{ + +} diff --git a/Content.Shared/Hands/Components/HandsComponent.cs b/Content.Shared/Hands/Components/HandsComponent.cs index f1f25a69f7..919d55f294 100644 --- a/Content.Shared/Hands/Components/HandsComponent.cs +++ b/Content.Shared/Hands/Components/HandsComponent.cs @@ -126,9 +126,43 @@ public sealed class HandsComponentState : ComponentState /// /// What side of the body this hand is on. /// +/// +/// public enum HandLocation : byte { Left, Middle, Right } + +/// +/// What side of the UI a hand is on. +/// +/// +/// +public enum HandUILocation : byte +{ + Left, + Right +} + +/// +/// Helper functions for working with . +/// +public static class HandLocationExt +{ + /// + /// Convert a into the appropriate . + /// This maps "middle" hands to . + /// + public static HandUILocation GetUILocation(this HandLocation location) + { + return location switch + { + HandLocation.Left => HandUILocation.Left, + HandLocation.Middle => HandUILocation.Right, + HandLocation.Right => HandUILocation.Right, + _ => throw new ArgumentOutOfRangeException(nameof(location), location, null) + }; + } +} diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs index 32339eb03a..6d4d332479 100644 --- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs +++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Interactions.cs @@ -8,6 +8,7 @@ using Content.Shared.Localizations; using Robust.Shared.Input.Binding; using Robust.Shared.Map; using Robust.Shared.Player; +using Robust.Shared.Utility; namespace Content.Shared.Hands.EntitySystems; @@ -181,27 +182,21 @@ public abstract partial class SharedHandsSystem : EntitySystem } //TODO: Actually shows all items/clothing/etc. - private void HandleExamined(EntityUid uid, HandsComponent handsComp, ExaminedEvent args) + private void HandleExamined(EntityUid examinedUid, HandsComponent handsComp, ExaminedEvent args) { - var held = EnumerateHeld(uid, handsComp) - .Where(x => !HasComp(x)).ToList(); + var heldItemNames = EnumerateHeld(examinedUid, handsComp) + .Where(entity => !HasComp(entity)) + .Select(item => FormattedMessage.EscapeText(Identity.Name(item, EntityManager))) + .Select(itemName => Loc.GetString("comp-hands-examine-wrapper", ("item", itemName))) + .ToList(); + + var locKey = heldItemNames.Count != 0 ? "comp-hands-examine" : "comp-hands-examine-empty"; + var locUser = ("user", Identity.Entity(examinedUid, EntityManager)); + var locItems = ("items", ContentLocalizationManager.FormatList(heldItemNames)); using (args.PushGroup(nameof(HandsComponent))) { - if (!held.Any()) - { - args.PushText(Loc.GetString("comp-hands-examine-empty", - ("user", Identity.Entity(uid, EntityManager)))); - return; - } - - var heldList = ContentLocalizationManager.FormatList(held - .Select(x => Loc.GetString("comp-hands-examine-wrapper", - ("item", Identity.Entity(x, EntityManager)))).ToList()); - - args.PushMarkup(Loc.GetString("comp-hands-examine", - ("user", Identity.Entity(uid, EntityManager)), - ("items", heldList))); + args.PushMarkup(Loc.GetString(locKey, locUser, locItems)); } } } diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index 2a846d7fe2..ffb78dcf1f 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -267,8 +267,11 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem /// The mob's entity UID. /// The character profile to load. /// Humanoid component of the entity - public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, HumanoidAppearanceComponent? humanoid = null) + public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile, HumanoidAppearanceComponent? humanoid = null) { + if (profile == null) + return; + if (!Resolve(uid, ref humanoid)) { return; diff --git a/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs b/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs index 3857063783..e7a88b6ef2 100644 --- a/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs +++ b/Content.Shared/IdentityManagement/Components/IdentityBlockerComponent.cs @@ -6,6 +6,7 @@ namespace Content.Shared.IdentityManagement.Components; [RegisterComponent, NetworkedComponent] public sealed partial class IdentityBlockerComponent : Component { + [DataField] public bool Enabled = true; /// diff --git a/Content.Shared/Interaction/Components/ClumsyComponent.cs b/Content.Shared/Interaction/Components/ClumsyComponent.cs index 5b72fc224c..824696c838 100644 --- a/Content.Shared/Interaction/Components/ClumsyComponent.cs +++ b/Content.Shared/Interaction/Components/ClumsyComponent.cs @@ -1,22 +1,24 @@ using Content.Shared.Damage; using Robust.Shared.Audio; +using Robust.Shared.GameStates; -namespace Content.Shared.Interaction.Components +namespace Content.Shared.Interaction.Components; + +/// +/// A simple clumsy tag-component. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ClumsyComponent : Component { /// - /// A simple clumsy tag-component. + /// Damage dealt to a clumsy character when they try to fire a gun. /// - [RegisterComponent] - public sealed partial class ClumsyComponent : Component - { - [DataField("clumsyDamage", required: true)] - [ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier ClumsyDamage = default!; + [DataField(required: true), AutoNetworkedField] + public DamageSpecifier ClumsyDamage = default!; - /// - /// Sound to play when clumsy interactions fail - /// - [DataField("clumsySound")] - public SoundSpecifier ClumsySound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); - } + /// + /// Sound to play when clumsy interactions fail. + /// + [DataField] + public SoundSpecifier ClumsySound = new SoundPathSpecifier("/Audio/Items/bikehorn.ogg"); } diff --git a/Content.Shared/Inventory/InventorySystem.Helpers.cs b/Content.Shared/Inventory/InventorySystem.Helpers.cs index 811387d375..7e325abe21 100644 --- a/Content.Shared/Inventory/InventorySystem.Helpers.cs +++ b/Content.Shared/Inventory/InventorySystem.Helpers.cs @@ -1,8 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Content.Shared.Hands.Components; using Content.Shared.Storage.EntitySystems; -using Robust.Shared.Containers; using Robust.Shared.Prototypes; namespace Content.Shared.Inventory; @@ -96,7 +94,7 @@ public partial class InventorySystem /// /// The entity that you want to spawn an item on /// A list of prototype IDs that you want to spawn in the bag. - public void SpawnItemsOnEntity(EntityUid entity, List items) + public void SpawnItemsOnEntity(EntityUid entity, List items) { foreach (var item in items) { diff --git a/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs b/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs index e3ca466b08..27539dd22b 100644 --- a/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs +++ b/Content.Shared/Medical/SuitSensor/SharedSuitSensor.cs @@ -24,7 +24,7 @@ public sealed class SuitSensorStatus public bool IsAlive; public int? TotalDamage; public int? TotalDamageThreshold; - public float? DamagePercentage => TotalDamageThreshold == null || TotalDamage == null ? null : TotalDamage / TotalDamageThreshold; + public float? DamagePercentage => TotalDamageThreshold == null || TotalDamage == null ? null : TotalDamage / (float) TotalDamageThreshold; public NetCoordinates? Coordinates; } diff --git a/Content.Shared/NukeOps/NukeOperativeComponent.cs b/Content.Shared/NukeOps/NukeOperativeComponent.cs index cdbefece9d..d19f0ae3e9 100644 --- a/Content.Shared/NukeOps/NukeOperativeComponent.cs +++ b/Content.Shared/NukeOps/NukeOperativeComponent.cs @@ -13,14 +13,9 @@ namespace Content.Shared.NukeOps; [RegisterComponent, NetworkedComponent] public sealed partial class NukeOperativeComponent : Component { - /// - /// Path to antagonist alert sound. - /// - [DataField("greetSoundNotification")] - public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/nukeops_start.ogg"); /// - /// + /// /// [DataField("syndStatusIcon", customTypeSerializer: typeof(PrototypeIdSerializer))] public string SyndStatusIcon = "SyndicateFaction"; diff --git a/Content.Shared/Popups/SharedPopupSystem.cs b/Content.Shared/Popups/SharedPopupSystem.cs index 10e8ca9be1..38d2030cd5 100644 --- a/Content.Shared/Popups/SharedPopupSystem.cs +++ b/Content.Shared/Popups/SharedPopupSystem.cs @@ -82,12 +82,24 @@ namespace Content.Shared.Popups /// public abstract void PopupEntity(string? message, EntityUid uid, Filter filter, bool recordReplay, PopupType type = PopupType.Small); + /// + /// Variant of that only runs on the client, outside of prediction. + /// Useful for shared code that is always ran by both sides to avoid duplicate popups. + /// + public abstract void PopupClient(string? message, EntityUid? recipient, PopupType type = PopupType.Small); + /// /// Variant of that only runs on the client, outside of prediction. /// Useful for shared code that is always ran by both sides to avoid duplicate popups. /// public abstract void PopupClient(string? message, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small); + /// + /// Variant of that only runs on the client, outside of prediction. + /// Useful for shared code that is always ran by both sides to avoid duplicate popups. + /// + public abstract void PopupClient(string? message, EntityCoordinates coordinates, EntityUid? recipient, PopupType type = PopupType.Small); + /// /// Variant of for use with prediction. The local client will show /// the popup to the recipient, and the server will show it to every other player in PVS range. If recipient is null, the local client diff --git a/Content.Shared/Radio/Components/SharedRadioJammerComponent.cs b/Content.Shared/Radio/Components/SharedRadioJammerComponent.cs new file mode 100644 index 0000000000..e5e52a3e47 --- /dev/null +++ b/Content.Shared/Radio/Components/SharedRadioJammerComponent.cs @@ -0,0 +1,74 @@ +using Robust.Shared.Serialization; +using Robust.Shared.GameStates; + +namespace Content.Shared.RadioJammer; + +/// +/// When activated () prevents from sending messages in range +/// Suit sensors will also stop working. +/// +[NetworkedComponent, RegisterComponent] +public sealed partial class RadioJammerComponent : Component +{ + [DataDefinition] + public partial struct RadioJamSetting + { + /// + /// Power usage per second when enabled. + /// + [DataField(required: true)] + public float Wattage; + + /// + /// Range of the jammer. + /// + [DataField(required: true)] + public float Range; + + /// + /// The message that is displayed when switched + /// to this setting. + /// + [DataField(required: true)] + public LocId Message = string.Empty; + + /// + /// Name of the setting. + /// + [DataField(required: true)] + public LocId Name = string.Empty; + } + + /// + /// List of all the settings for the radio jammer. + /// + [DataField(required: true), ViewVariables(VVAccess.ReadOnly)] + public RadioJamSetting[] Settings; + + /// + /// Index of the currently selected setting. + /// + [DataField] + public int SelectedPowerLevel = 1; +} + +[Serializable, NetSerializable] +public enum RadioJammerChargeLevel : byte +{ + Low, + Medium, + High +} + +[Serializable, NetSerializable] +public enum RadioJammerLayers : byte +{ + LED +} + +[Serializable, NetSerializable] +public enum RadioJammerVisuals : byte +{ + ChargeLevel, + LEDOn +} diff --git a/Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs b/Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs new file mode 100644 index 0000000000..e1f632735c --- /dev/null +++ b/Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs @@ -0,0 +1,78 @@ +using Content.Shared.Popups; +using Content.Shared.DeviceNetwork.Components; +using Content.Shared.Verbs; +using Content.Shared.RadioJammer; + +namespace Content.Shared.Radio.EntitySystems; + +public abstract class SharedJammerSystem : EntitySystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] protected readonly SharedPopupSystem Popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnGetVerb); + } + + private void OnGetVerb(Entity entity, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract) + return; + + var user = args.User; + + byte index = 0; + foreach (var setting in entity.Comp.Settings) + { + // This is because Act wont work with index. + // Needs it to be saved in the loop. + var currIndex = index; + var verb = new Verb + { + Priority = currIndex, + Category = VerbCategory.PowerLevel, + Disabled = entity.Comp.SelectedPowerLevel == currIndex, + Act = () => + { + entity.Comp.SelectedPowerLevel = currIndex; + if (TryComp(entity.Owner, out var jammerComp)) + { + // This is a little sketcy but only way to do it. + jammerComp.Range = GetCurrentRange(entity.Comp); + Dirty(entity.Owner, jammerComp); + } + Popup.PopupPredicted(Loc.GetString(setting.Message), user, user); + }, + Text = Loc.GetString(setting.Name), + }; + args.Verbs.Add(verb); + index++; + } + } + + public float GetCurrentWattage(RadioJammerComponent jammer) + { + return jammer.Settings[jammer.SelectedPowerLevel].Wattage; + } + + public float GetCurrentRange(RadioJammerComponent jammer) + { + return jammer.Settings[jammer.SelectedPowerLevel].Range; + } + + protected void ChangeLEDState(bool isLEDOn, EntityUid uid, + AppearanceComponent? appearance = null) + { + _appearance.SetData(uid, RadioJammerVisuals.LEDOn, isLEDOn, appearance); + } + + protected void ChangeChargeLevel(RadioJammerChargeLevel chargeLevel, EntityUid uid, + AppearanceComponent? appearance = null) + { + _appearance.SetData(uid, RadioJammerVisuals.ChargeLevel, chargeLevel, appearance); + } + +} diff --git a/Content.Shared/Roles/SharedRoleSystem.cs b/Content.Shared/Roles/SharedRoleSystem.cs index e8053e4c67..1a732eb750 100644 --- a/Content.Shared/Roles/SharedRoleSystem.cs +++ b/Content.Shared/Roles/SharedRoleSystem.cs @@ -62,6 +62,32 @@ public abstract class SharedRoleSystem : EntitySystem _antagTypes.Add(typeof(T)); } + public void MindAddRole(EntityUid mindId, Component component, MindComponent? mind = null, bool silent = false) + { + if (!Resolve(mindId, ref mind)) + return; + + if (HasComp(mindId, component.GetType())) + { + throw new ArgumentException($"We already have this role: {component}"); + } + + EntityManager.AddComponent(mindId, component); + var antagonist = IsAntagonistRole(component.GetType()); + + var mindEv = new MindRoleAddedEvent(silent); + RaiseLocalEvent(mindId, ref mindEv); + + var message = new RoleAddedEvent(mindId, mind, antagonist, silent); + if (mind.OwnedEntity != null) + { + RaiseLocalEvent(mind.OwnedEntity.Value, message, true); + } + + _adminLogger.Add(LogType.Mind, LogImpact.Low, + $"'Role {component}' added to mind of {_minds.MindOwnerLoggingString(mind)}"); + } + /// /// Gives this mind a new role. /// @@ -137,11 +163,13 @@ public abstract class SharedRoleSystem : EntitySystem public bool MindHasRole(EntityUid mindId) where T : IComponent { + DebugTools.Assert(HasComp(mindId)); return HasComp(mindId); } public List MindGetAllRoles(EntityUid mindId) { + DebugTools.Assert(HasComp(mindId)); var ev = new MindGetAllRolesEvent(new List()); RaiseLocalEvent(mindId, ref ev); return ev.Roles; @@ -152,6 +180,7 @@ public abstract class SharedRoleSystem : EntitySystem if (mindId == null) return false; + DebugTools.Assert(HasComp(mindId)); var ev = new MindIsAntagonistEvent(); RaiseLocalEvent(mindId.Value, ref ev); return ev.IsAntagonist; @@ -177,6 +206,11 @@ public abstract class SharedRoleSystem : EntitySystem return _antagTypes.Contains(typeof(T)); } + public bool IsAntagonistRole(Type component) + { + return _antagTypes.Contains(component); + } + /// /// Play a sound for the mind, if it has a session attached. /// Use this for role greeting sounds. diff --git a/Content.Shared/Station/SharedStationSpawningSystem.cs b/Content.Shared/Station/SharedStationSpawningSystem.cs index 49ef8509db..363fb3f91e 100644 --- a/Content.Shared/Station/SharedStationSpawningSystem.cs +++ b/Content.Shared/Station/SharedStationSpawningSystem.cs @@ -1,16 +1,17 @@ using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Inventory; -using Content.Shared.Preferences; using Content.Shared.Roles; using Content.Shared.Storage; using Content.Shared.Storage.EntitySystems; using Robust.Shared.Collections; +using Robust.Shared.Prototypes; namespace Content.Shared.Station; public abstract class SharedStationSpawningSystem : EntitySystem { + [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [Dependency] protected readonly InventorySystem InventorySystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly SharedStorageSystem _storage = default!; @@ -21,8 +22,22 @@ public abstract class SharedStationSpawningSystem : EntitySystem /// /// Entity to load out. /// Starting gear to use. - public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear) + public void EquipStartingGear(EntityUid entity, ProtoId? startingGear) { + PrototypeManager.TryIndex(startingGear, out var gearProto); + EquipStartingGear(entity, gearProto); + } + + /// + /// Equips starting gear onto the given entity. + /// + /// Entity to load out. + /// Starting gear to use. + public void EquipStartingGear(EntityUid entity, StartingGearPrototype? startingGear) + { + if (startingGear == null) + return; + if (InventorySystem.TryGetSlots(entity, out var slotDefinitions)) { foreach (var slot in slotDefinitions) diff --git a/Content.Shared/Verbs/VerbCategory.cs b/Content.Shared/Verbs/VerbCategory.cs index 31cbe58abe..f8ef906e6a 100644 --- a/Content.Shared/Verbs/VerbCategory.cs +++ b/Content.Shared/Verbs/VerbCategory.cs @@ -86,5 +86,7 @@ namespace Content.Shared.Verbs public static readonly VerbCategory Lever = new("verb-categories-lever", null); public static readonly VerbCategory SelectType = new("verb-categories-select-type", null); + + public static readonly VerbCategory PowerLevel = new("verb-categories-power-level", null); } } diff --git a/Content.YAMLLinter/Program.cs b/Content.YAMLLinter/Program.cs index b23faa48fc..7f0b740fe8 100644 --- a/Content.YAMLLinter/Program.cs +++ b/Content.YAMLLinter/Program.cs @@ -99,7 +99,7 @@ namespace Content.YAMLLinter yamlErrors[kind] = set; } - fieldErrors = protoMan.ValidateFields(prototypes); + fieldErrors = protoMan.ValidateStaticFields(prototypes); }); return (yamlErrors, fieldErrors); diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index d623b48d44..a357848aa5 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,147 +1,4 @@ Entries: -- author: Ubaser - changes: - - message: Food Service research is now roundstart. - type: Tweak - id: 5916 - time: '2024-02-11T06:37:12.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25046 -- author: SlamBamActionman - changes: - - message: Added setting to toggle chat name color in Options. - type: Add - id: 5917 - time: '2024-02-11T06:38:55.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/24625 -- author: Emisse - changes: - - message: Ambuzol now requires zombie blood - type: Tweak - id: 5918 - time: '2024-02-11T20:47:58.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25119 -- author: Plykiya - changes: - - message: Door remotes no longer have their 5G signals absorbed by mobs, machines, - or Ian's cute butt. - type: Fix - id: 5919 - time: '2024-02-12T02:34:13.532550+00:00' - url: null -- author: FungiFellow - changes: - - message: Lowered Reoccurence Delay for Ion Storm - type: Tweak - id: 5920 - time: '2024-02-12T02:35:10.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25135 -- author: PoorMansDreams - changes: - - message: Ammo is now tipped! - type: Tweak - id: 5921 - time: '2024-02-12T03:02:52.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25103 -- author: PoorMansDreams - changes: - - message: Buyable Janitorial Trolley! - type: Add - id: 5922 - time: '2024-02-12T06:35:19.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25139 -- author: Plykiya - changes: - - message: Stun batons and stun prods now display their hits remaining. - type: Tweak - id: 5923 - time: '2024-02-12T06:36:06.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25141 -- author: Ilya246 - changes: - - message: Autolathes may now make empty air tanks. - type: Add - id: 5924 - time: '2024-02-12T06:37:28.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25130 -- author: Ubaser - changes: - - message: Resprited the CE's jetpack. - type: Tweak - id: 5925 - time: '2024-02-12T20:44:23.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25150 -- author: EdenTheLiznerd - changes: - - message: Deathnettles are no longer able to bypass armor and don't do as much - damage - type: Tweak - id: 5926 - time: '2024-02-13T01:20:02.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25068 -- author: Vasilis - changes: - - message: You can now inspect peoples id's and health though glass if you are within - details range. - type: Tweak - id: 5927 - time: '2024-02-13T06:41:56.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25163 -- author: metalgearsloth - changes: - - message: Fix decal error spam in console due to a missing overlay. - type: Fix - id: 5928 - time: '2024-02-13T07:10:44.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25170 -- author: jamessimo - changes: - - message: Can now "wink" with the text emote ;) or ;] - type: Add - - message: Can now "tearfully smile" with the text emote :') and similar variants - type: Add - - message: Added more ways to "cry" with the emote :'( and similar variants - type: Tweak - id: 5929 - time: '2024-02-13T15:43:20.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25129 -- author: icekot8 - changes: - - message: Medical berets added to MediDrobe - type: Add - id: 5930 - time: '2024-02-13T21:37:23.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25176 -- author: PoorMansDreams - changes: - - message: Alternate ammo now has the proper appearance when spent. - type: Fix - id: 5931 - time: '2024-02-13T21:40:15.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25167 -- author: Agoichi - changes: - - message: All hoods have tag "Hides Hair" - type: Tweak - - message: Plague Doctor's hat and Witch hat (with red hair) have tag "Hides Hair" - type: Tweak - id: 5932 - time: '2024-02-13T21:43:19.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25142 -- author: Blackern5000 - changes: - - message: Advanced topical meds now cost significantly less chemicals - type: Tweak - id: 5933 - time: '2024-02-13T22:03:13.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/24948 -- author: Tayrtahn - changes: - - message: Drink bottles can now be opened and closed from the verbs menu, and some - have been given tamper-evident seals. - type: Add - id: 5934 - time: '2024-02-13T22:08:07.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/24780 - author: TheShuEd changes: - message: Darkness is coming. A new shadow anomaly added. @@ -3846,3 +3703,150 @@ id: 6415 time: '2024-04-21T16:09:26.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26545 +- author: deltanedas + changes: + - message: Flaming mice no longer completely engulf people they touch. + type: Tweak + id: 6416 + time: '2024-04-22T08:42:26.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27202 +- author: Weax + changes: + - message: The CLF3 reaction now requires heating first before you can engulf chemistry + in fiery death. + type: Tweak + id: 6417 + time: '2024-04-22T08:44:14.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27187 +- author: Potato1234_x + changes: + - message: Added Psicodine, Mannitol, Lipolicide and Happiness. + type: Add + id: 6418 + time: '2024-04-22T08:45:39.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27134 +- author: Terraspark4941 + changes: + - message: Updated the engineering section of the guidebook! + type: Tweak + id: 6419 + time: '2024-04-22T08:58:54.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26851 +- author: eclips_e + changes: + - message: Slimepeople can now morph into a "geras"--a smaller slime form that can + pass under grilles, at the cost of dropping all of their inventory. They can + also be picked up with two hands and placed into duffelbags. + type: Add + - message: Slimepeople now have an internal 2x2 storage that they (and anyone around + them) can access. It is not dropped when morphing into a geras! + type: Add + - message: Slimepeople now have slightly increased regeneration and a slightly meatier + punch, but slower attacks. + type: Add + id: 6420 + time: '2024-04-22T10:03:03.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/23425 +- author: FungiFellow + changes: + - message: Syndi-Cats are now 6TC, Insulated, Available to Syndies, Can Move in + Space, Open Doors, and Hit Harder + type: Tweak + id: 6421 + time: '2024-04-22T12:18:28.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27222 +- author: Tayrtahn + changes: + - message: Ghosts can no longer trigger artifacts by examining them. + type: Fix + id: 6422 + time: '2024-04-22T22:46:22.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27249 +- author: Tyzemol + changes: + - message: Slimes no longer absorb all items used on them + type: Fix + id: 6423 + time: '2024-04-23T08:48:26.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27260 +- author: Rainbeon + changes: + - message: Suit sensor vitals now function again. + type: Fix + id: 6424 + time: '2024-04-23T08:57:09.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27259 +- author: Ghagliiarghii + changes: + - message: Security can now find replacements for their trusty Combat Knife in the + SecVend, or craft them with the Sec Lathe. + type: Tweak + id: 6425 + time: '2024-04-23T11:24:58.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27224 +- author: Whisper + changes: + - message: Fire stack limit reduced from 20 to 10. Fire transfers will be less effective, + and fires will not last as long. + type: Tweak + id: 6426 + time: '2024-04-23T11:30:01.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27159 +- author: brainfood1183 + changes: + - message: Directional Exit signs for maints! + type: Add + id: 6427 + time: '2024-04-23T11:31:48.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26831 +- author: pigeonpeas + changes: + - message: Adds the ability to purchase emitters in cargo. + type: Add + id: 6428 + time: '2024-04-23T11:34:09.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27229 +- author: EmoGarbage404 + changes: + - message: Fixed cargo telepads not teleporting in orders from linked consoles. + type: Fix + id: 6429 + time: '2024-04-23T12:07:12.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27255 +- author: Plykiya + changes: + - message: Do-after bars of other players are now shaded and harder to see in the + dark. + type: Tweak + id: 6430 + time: '2024-04-24T02:42:34.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27273 +- author: Blackern5000 + changes: + - message: The cargo telepad is now tier 2 technology rather than tier 3. + type: Tweak + id: 6431 + time: '2024-04-24T13:21:29.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26270 +- author: FungiFellow + changes: + - message: Truncheon now fits in SecBelt + type: Tweak + id: 6432 + time: '2024-04-24T13:43:56.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27281 +- author: pigeonpeas + changes: + - message: Adds a trash bag to the advanced cleaning module. + type: Add + id: 6433 + time: '2024-04-24T21:27:34.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27226 +- author: Beck Thompson + changes: + - message: Radio jammer now has 3 selectable power operating levels and a battery + level led indicator! + type: Tweak + id: 6434 + time: '2024-04-25T02:19:17.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25912 diff --git a/Resources/Locale/en-US/accent/italian.ftl b/Resources/Locale/en-US/accent/italian.ftl index d0ef4e8f72..cc8641417f 100644 --- a/Resources/Locale/en-US/accent/italian.ftl +++ b/Resources/Locale/en-US/accent/italian.ftl @@ -78,9 +78,6 @@ accent-italian-words-replace-23 = greek accent-italian-words-24 = operatives accent-italian-words-replace-24 = greeks -accent-italian-words-24 = ops -accent-italian-words-replace-24 = greeks - accent-italian-words-25 = sec accent-italian-words-replace-25 = polizia diff --git a/Resources/Locale/en-US/accent/pirate.ftl b/Resources/Locale/en-US/accent/pirate.ftl index 8da975df40..b6db7c803b 100644 --- a/Resources/Locale/en-US/accent/pirate.ftl +++ b/Resources/Locale/en-US/accent/pirate.ftl @@ -1,7 +1,7 @@ accent-pirate-prefix-1 = Arrgh accent-pirate-prefix-2 = Garr accent-pirate-prefix-3 = Yarr -accent-pirate-prefix-3 = Yarrgh +accent-pirate-prefix-4 = Yarrgh accent-pirate-replaced-1 = my accent-pirate-replacement-1 = me diff --git a/Resources/Locale/en-US/administration/ui/admin-logs.ftl b/Resources/Locale/en-US/administration/ui/admin-logs.ftl index 549e9587d7..377bea6e84 100644 --- a/Resources/Locale/en-US/administration/ui/admin-logs.ftl +++ b/Resources/Locale/en-US/administration/ui/admin-logs.ftl @@ -14,7 +14,6 @@ admin-logs-select-none = None # Players admin-logs-search-players-placeholder = Search Players (OR) -admin-logs-select-none = None admin-logs-include-non-player = Include Non-players # Logs diff --git a/Resources/Locale/en-US/administration/ui/admin-notes.ftl b/Resources/Locale/en-US/administration/ui/admin-notes.ftl index ca5348a940..03e1290257 100644 --- a/Resources/Locale/en-US/administration/ui/admin-notes.ftl +++ b/Resources/Locale/en-US/administration/ui/admin-notes.ftl @@ -35,7 +35,6 @@ admin-notes-message-seen = Seen admin-notes-banned-from = Banned from admin-notes-the-server = the server admin-notes-permanently = permanently -admin-notes-for = for {$player} admin-notes-days = {$days} days admin-notes-hours = {$hours} hours admin-notes-minutes = {$minutes} minutes diff --git a/Resources/Locale/en-US/alerts/alerts.ftl b/Resources/Locale/en-US/alerts/alerts.ftl index 795d740141..319809da40 100644 --- a/Resources/Locale/en-US/alerts/alerts.ftl +++ b/Resources/Locale/en-US/alerts/alerts.ftl @@ -57,9 +57,6 @@ alerts-no-battery-desc = You don't have a battery, rendering you unable to charg alerts-internals-name = Toggle internals alerts-internals-desc = Toggles your gas tank internals on or off. -alerts-internals-name = Toggle internals -alerts-internals-desc = Toggles your gas tank internals on or off. - alerts-piloting-name = Piloting Shuttle alerts-piloting-desc = You are piloting a shuttle. Click the alert to stop. diff --git a/Resources/Locale/en-US/ame/components/ame-controller-component.ftl b/Resources/Locale/en-US/ame/components/ame-controller-component.ftl index ee1f7f42e7..f15141ebcc 100644 --- a/Resources/Locale/en-US/ame/components/ame-controller-component.ftl +++ b/Resources/Locale/en-US/ame/components/ame-controller-component.ftl @@ -16,7 +16,6 @@ ame-window-refresh-parts-button = Refresh Parts ame-window-core-count-label = Core count: ame-window-power-currentsupply-label = Current power supply: ame-window-power-targetsupply-label = Targeted power supply: -ame-window-toggle-injection-button = Toggle Injection ame-window-eject-button = Eject ame-window-increase-fuel-button = Increase ame-window-decrease-fuel-button = Decrease diff --git a/Resources/Locale/en-US/arcade/components/space-villain-game-component.ftl b/Resources/Locale/en-US/arcade/components/space-villain-game-component.ftl index af005ae62d..75e18c8d71 100644 --- a/Resources/Locale/en-US/arcade/components/space-villain-game-component.ftl +++ b/Resources/Locale/en-US/arcade/components/space-villain-game-component.ftl @@ -11,5 +11,4 @@ space-villain-game-enemy-dies-with-player-message = {$enemyName} dies, but takes space-villain-game-enemy-throws-bomb-message = {$enemyName} throws a bomb, exploding you for {$damageReceived} damage! space-villain-game-enemy-steals-player-power-message = {$enemyName} steals {$stolenAmount} of your power! space-villain-game-enemy-heals-message = {$enemyName} heals for {$healedAmount} health! -space-villain-game-enemy-steals-player-power-message = {$enemyName} steals {$stolenAmount} of your power! -space-villain-game-enemy-attacks-message = {$enemyName} attacks you for {$damageDealt} damage! \ No newline at end of file +space-villain-game-enemy-attacks-message = {$enemyName} attacks you for {$damageDealt} damage! diff --git a/Resources/Locale/en-US/components/storage-component.ftl b/Resources/Locale/en-US/components/storage-component.ftl index 29c858891a..e742c83f6a 100644 --- a/Resources/Locale/en-US/components/storage-component.ftl +++ b/Resources/Locale/en-US/components/storage-component.ftl @@ -8,3 +8,5 @@ comp-storage-cant-drop = You can't let go of { THE($entity) }! comp-storage-window-title = Storage Item comp-storage-window-weight = { $weight }/{ $maxWeight }, Max Size: {$size} comp-storage-window-slots = Slots: { $itemCount }/{ $maxCount }, Max Size: {$size} +comp-storage-verb-open-storage = Open Storage +comp-storage-verb-close-storage = Close Storage diff --git a/Resources/Locale/en-US/construction/ui/construction-menu.ftl b/Resources/Locale/en-US/construction/ui/construction-menu.ftl index f4b7f3559a..82ebc01bc9 100644 --- a/Resources/Locale/en-US/construction/ui/construction-menu.ftl +++ b/Resources/Locale/en-US/construction/ui/construction-menu.ftl @@ -4,5 +4,4 @@ construction-menu-title = Construction construction-menu-place-ghost = Place construction ghost construction-menu-clear-all = Clear All construction-menu-eraser-mode = Eraser Mode -construction-menu-title = Construction -construction-menu-craft = Craft \ No newline at end of file +construction-menu-craft = Craft diff --git a/Resources/Locale/en-US/devices/network-configurator.ftl b/Resources/Locale/en-US/devices/network-configurator.ftl index e1bcbc4c94..cd4955ed36 100644 --- a/Resources/Locale/en-US/devices/network-configurator.ftl +++ b/Resources/Locale/en-US/devices/network-configurator.ftl @@ -41,5 +41,5 @@ network-configurator-examine-current-mode = Current mode: {$mode} network-configurator-examine-switch-modes = Press {$key} to switch modes # item status -network-configurator-item-status-label = Current mode: {$mode} -{$keybinding} to switch mode +network-configurator-item-status-label = Mode: {$mode} + Switch: {$keybinding} diff --git a/Resources/Locale/en-US/disposal/tube/components/disposal-router-component.ftl b/Resources/Locale/en-US/disposal/tube/components/disposal-router-component.ftl index 64fbfdf66f..4fe24b7853 100644 --- a/Resources/Locale/en-US/disposal/tube/components/disposal-router-component.ftl +++ b/Resources/Locale/en-US/disposal/tube/components/disposal-router-component.ftl @@ -4,7 +4,3 @@ disposal-router-window-title = Disposal Router disposal-router-window-tags-label = Tags: disposal-router-window-tag-input-tooltip = A comma separated list of tags disposal-router-window-tag-input-confirm-button = Confirm - -## ConfigureVerb - -configure-verb-get-data-text = Open Configuration diff --git a/Resources/Locale/en-US/disposal/tube/components/disposal-tagger-window.ftl b/Resources/Locale/en-US/disposal/tube/components/disposal-tagger-window.ftl index dc4b40fc7f..55523c4b95 100644 --- a/Resources/Locale/en-US/disposal/tube/components/disposal-tagger-window.ftl +++ b/Resources/Locale/en-US/disposal/tube/components/disposal-tagger-window.ftl @@ -1,6 +1,3 @@ disposal-tagger-window-title = Disposal Tagger disposal-tagger-window-tag-input-label = Tag: disposal-tagger-window-tag-confirm-button = Confirm - -## ConfigureVerb -configure-verb-get-data-text = Open Configuration diff --git a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl index 6173977285..416d87f7c5 100644 --- a/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl +++ b/Resources/Locale/en-US/escape-menu/ui/options-menu.ftl @@ -196,7 +196,6 @@ ui-options-function-editor-rotate-object = Rotate ui-options-function-editor-flip-object = Flip ui-options-function-editor-copy-object = Copy -ui-options-function-open-abilities-menu = Open action menu ui-options-function-show-debug-console = Open Console ui-options-function-show-debug-monitors = Show Debug Monitors ui-options-function-inspect-entity = Inspect Entity diff --git a/Resources/Locale/en-US/flavors/flavor-profiles.ftl b/Resources/Locale/en-US/flavors/flavor-profiles.ftl index 61567d8695..41b575b7d6 100644 --- a/Resources/Locale/en-US/flavors/flavor-profiles.ftl +++ b/Resources/Locale/en-US/flavors/flavor-profiles.ftl @@ -168,6 +168,8 @@ flavor-complex-light = like a light gone out flavor-complex-profits = like profits flavor-complex-fishops = like the dreaded fishops flavor-complex-violets = like violets +flavor-complex-mothballs = like mothballs +flavor-complex-paint-thinner = like paint thinner # Drink-specific flavors. diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-pirates.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-pirates.ftl deleted file mode 100644 index 941643dd9a..0000000000 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-pirates.ftl +++ /dev/null @@ -1,10 +0,0 @@ -pirates-title = Privateers -pirates-description = A group of privateers has approached your lowly station. Hostile or not, their sole goal is to end the round with as many knicknacks on their ship as they can get. - -pirates-no-ship = Through unknown circumstances, the privateer's ship was completely and utterly destroyed. No score. -pirates-final-score = The privateers successfully obtained {$score} spesos worth -pirates-final-score-2 = of knicknacks, with a total of {$finalPrice} spesos. -pirates-list-start = The privateers were: -pirates-most-valuable = The most valuable stolen items were: -pirates-stolen-item-entry = {$entity} ({$credits} spesos) -pirates-stole-nothing = - The pirates stole absolutely nothing at all. Point and laugh. diff --git a/Resources/Locale/en-US/geras/geras.ftl b/Resources/Locale/en-US/geras/geras.ftl new file mode 100644 index 0000000000..3cd3f101ff --- /dev/null +++ b/Resources/Locale/en-US/geras/geras.ftl @@ -0,0 +1,2 @@ +geras-popup-morph-message-user = You shift and morph into a small version of you! +geras-popup-morph-message-others = {CAPITALIZE(THE($entity))} shifts and morphs into a blob of slime! diff --git a/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl b/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl index 807b5591a8..7748ab9893 100644 --- a/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl +++ b/Resources/Locale/en-US/guidebook/chemistry/conditions.ftl @@ -7,6 +7,15 @@ } } +reagent-effect-condition-guidebook-total-hunger = + { $max -> + [2147483648] the target has at least {NATURALFIXED($min, 2)} total hunger + *[other] { $min -> + [0] the target has at most {NATURALFIXED($max, 2)} total hunger + *[other] the target has between {NATURALFIXED($min, 2)} and {NATURALFIXED($max, 2)} total hunger + } + } + reagent-effect-condition-guidebook-reagent-threshold = { $max -> [2147483648] there's at least {NATURALFIXED($min, 2)}u of {$reagent} diff --git a/Resources/Locale/en-US/guidebook/chemistry/effects.ftl b/Resources/Locale/en-US/guidebook/chemistry/effects.ftl index b6f45d2386..94c1376083 100644 --- a/Resources/Locale/en-US/guidebook/chemistry/effects.ftl +++ b/Resources/Locale/en-US/guidebook/chemistry/effects.ftl @@ -43,7 +43,7 @@ reagent-effect-guidebook-foam-area-reaction-effect = *[other] create } large quantities of foam -reagent-effect-guidebook-foam-area-reaction-effect = +reagent-effect-guidebook-smoke-area-reaction-effect = { $chance -> [1] Creates *[other] create diff --git a/Resources/Locale/en-US/guidebook/guides.ftl b/Resources/Locale/en-US/guidebook/guides.ftl index 496e38b9a0..ff1ffbf5ea 100644 --- a/Resources/Locale/en-US/guidebook/guides.ftl +++ b/Resources/Locale/en-US/guidebook/guides.ftl @@ -11,7 +11,7 @@ guide-entry-access-configurator = Access Configurator guide-entry-power = Power guide-entry-portable-generator = Portable Generators guide-entry-ame = Antimatter Engine (AME) -guide-entry-singularity = Singularity +guide-entry-singularity = Singularity / Tesla guide-entry-teg = Thermo-electric Generator (TEG) guide-entry-rtg = RTG guide-entry-controls = Controls diff --git a/Resources/Locale/en-US/implant/implant.ftl b/Resources/Locale/en-US/implant/implant.ftl index b93d43105a..c3002a73ae 100644 --- a/Resources/Locale/en-US/implant/implant.ftl +++ b/Resources/Locale/en-US/implant/implant.ftl @@ -10,9 +10,10 @@ implanter-component-implant-already = {$target} already has the {$implant}! implanter-draw-text = Draw implanter-inject-text = Inject -implanter-empty-text = None +implanter-empty-text = Empty -implanter-label = Implant: [color=green]{$implantName}[/color] | [color=white]{$modeString}[/color]{$lineBreak}{$implantDescription} +implanter-label = [color=green]{$implantName}[/color] + Mode: [color=white]{$modeString}[/color] implanter-contained-implant-text = [color=green]{$desc}[/color] diff --git a/Resources/Locale/en-US/interaction/interaction-popup-component.ftl b/Resources/Locale/en-US/interaction/interaction-popup-component.ftl index bb56233ff1..4929b11b1c 100644 --- a/Resources/Locale/en-US/interaction/interaction-popup-component.ftl +++ b/Resources/Locale/en-US/interaction/interaction-popup-component.ftl @@ -42,7 +42,6 @@ petting-failure-crab = You reach out to pet {THE($target)}, but {SUBJECT($target petting-failure-dehydrated-carp = You pet {THE($target)} on {POSS-ADJ($target)} dry little head. petting-failure-goat = You reach out to pet {THE($target)}, but {SUBJECT($target)} stubbornly refuses! petting-failure-goose = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} too horrible! -petting-failure-goose = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} too australian! petting-failure-possum = You reach out to pet {THE($target)}, but are met with hisses and snarls! petting-failure-pig = You reach out to pet {THE($target)}, but are met with irritated oinks and squeals! petting-failure-raccoon = You reach out to pet {THE($target)}, but {THE($target)} is busy raccooning around. @@ -68,11 +67,6 @@ petting-failure-cleanbot = You reach out to pet {THE($target)}, but {SUBJECT($ta petting-failure-mimebot = You reach out to pet {THE($target)}, but {SUBJECT($target)} {CONJUGATE-BE($target)} busy miming! petting-failure-medibot = You reach out to pet {THE($target)}, but {POSS-ADJ($target)} syringe nearly stabs your hand! -## Knocking on windows - -# Shown when knocking on a window -comp-window-knock = *knock knock* - ## Rattling fences fence-rattle-success = *rattle* diff --git a/Resources/Locale/en-US/inventory/item-status.ftl b/Resources/Locale/en-US/inventory/item-status.ftl new file mode 100644 index 0000000000..a53ba8be7d --- /dev/null +++ b/Resources/Locale/en-US/inventory/item-status.ftl @@ -0,0 +1 @@ +item-status-not-held = No held item diff --git a/Resources/Locale/en-US/nutrition/components/drink-component.ftl b/Resources/Locale/en-US/nutrition/components/drink-component.ftl index 9a388744b0..e80787c8d5 100644 --- a/Resources/Locale/en-US/nutrition/components/drink-component.ftl +++ b/Resources/Locale/en-US/nutrition/components/drink-component.ftl @@ -1,4 +1,4 @@ -drink-component-on-use-is-empty = {$owner} is empty! +drink-component-on-use-is-empty = {CAPITALIZE(THE($owner))} is empty! drink-component-on-examine-is-empty = [color=gray]Empty[/color] drink-component-on-examine-is-opened = [color=yellow]Opened[/color] drink-component-on-examine-is-sealed = The seal is intact. @@ -10,7 +10,7 @@ drink-component-on-examine-is-half-empty = Halfway Empty drink-component-on-examine-is-mostly-empty = Mostly Empty drink-component-on-examine-exact-volume = It contains {$amount}u. drink-component-try-use-drink-not-open = Open {$owner} first! -drink-component-try-use-drink-is-empty = {$entity} is empty! +drink-component-try-use-drink-is-empty = {CAPITALIZE(THE($entity))} is empty! drink-component-try-use-drink-cannot-drink = You can't drink anything! drink-component-try-use-drink-had-enough = You can't drink more! drink-component-try-use-drink-cannot-drink-other = They can't drink anything! diff --git a/Resources/Locale/en-US/radiation/geiger-component.ftl b/Resources/Locale/en-US/radiation/geiger-component.ftl index 0e7d2a8a35..726c7190f2 100644 --- a/Resources/Locale/en-US/radiation/geiger-component.ftl +++ b/Resources/Locale/en-US/radiation/geiger-component.ftl @@ -1,3 +1,3 @@ -geiger-item-control-status = Radiation: [color={$color}]{$rads} rads[/color] +geiger-item-control-status = [color={$color}]{$rads} rads[/color] geiger-item-control-disabled = Disabled geiger-component-examine = Current radiation: [color={$color}]{$rads} rads[/color] diff --git a/Resources/Locale/en-US/radio/components/radio-jammer-component.ftl b/Resources/Locale/en-US/radio/components/radio-jammer-component.ftl index 68efbf8d4e..eb540ee971 100644 --- a/Resources/Locale/en-US/radio/components/radio-jammer-component.ftl +++ b/Resources/Locale/en-US/radio/components/radio-jammer-component.ftl @@ -4,3 +4,13 @@ radio-jammer-component-off-state = off radio-jammer-component-examine-on-state = The light is currently [color=darkgreen]on[/color]. radio-jammer-component-examine-off-state = The light is currently [color=darkred]off[/color]. + +radio-jammer-component-setting-high = High +radio-jammer-component-setting-medium = Medium +radio-jammer-component-setting-low = Low + +radio-jammer-component-set-message-high = The jammer is now operating at high power. +radio-jammer-component-set-message-medium = The jammer is now operating at medium power. +radio-jammer-component-set-message-low = The jammer is now operating at low power. + +radio-jammer-component-switch-setting = The power level switch is set to "[color=yellow]{$powerLevel}[/color]". diff --git a/Resources/Locale/en-US/reagents/mannitol.ftl b/Resources/Locale/en-US/reagents/mannitol.ftl new file mode 100644 index 0000000000..1d35aff587 --- /dev/null +++ b/Resources/Locale/en-US/reagents/mannitol.ftl @@ -0,0 +1 @@ +mannitol-effect-enlightened = You feel ENLIGHTENED! diff --git a/Resources/Locale/en-US/reagents/meta/medicine.ftl b/Resources/Locale/en-US/reagents/meta/medicine.ftl index e02d428082..a0b557e28f 100644 --- a/Resources/Locale/en-US/reagents/meta/medicine.ftl +++ b/Resources/Locale/en-US/reagents/meta/medicine.ftl @@ -132,3 +132,9 @@ reagent-desc-necrosol = A necrotic substance that seems to be able to heal froze reagent-name-aloxadone = aloxadone reagent-desc-aloxadone = A cryogenics chemical. Used to treat severe third degree burns via regeneration of the burnt tissue. Works regardless of the patient being alive or dead. + +reagent-name-mannitol = mannitol +reagent-desc-mannitol = Efficiently restores brain damage. + +reagent-name-psicodine = psicodine +reagent-desc-psicodine = Suppresses anxiety and other various forms of mental distress. Overdose causes hallucinations and minor toxin damage. diff --git a/Resources/Locale/en-US/reagents/meta/narcotics.ftl b/Resources/Locale/en-US/reagents/meta/narcotics.ftl index ea115bf962..a7cffb7f6b 100644 --- a/Resources/Locale/en-US/reagents/meta/narcotics.ftl +++ b/Resources/Locale/en-US/reagents/meta/narcotics.ftl @@ -39,3 +39,6 @@ reagent-desc-norepinephric-acid = A smooth chemical that blocks the optical rece reagent-name-tear-gas = tear gas reagent-desc-tear-gas = A chemical that causes severe irritation and crying, commonly used in riot control. + +reagent-name-happiness = happiness +reagent-desc-happiness = Fills you with ecstatic numbness and causes minor brain damage. Highly addictive. If overdosed causes sudden mood swings. diff --git a/Resources/Locale/en-US/reagents/meta/physical-desc.ftl b/Resources/Locale/en-US/reagents/meta/physical-desc.ftl index 2e959bf14e..355b76c2a4 100644 --- a/Resources/Locale/en-US/reagents/meta/physical-desc.ftl +++ b/Resources/Locale/en-US/reagents/meta/physical-desc.ftl @@ -64,7 +64,6 @@ reagent-physical-desc-sticky = sticky reagent-physical-desc-bubbly = bubbly reagent-physical-desc-rocky = rocky reagent-physical-desc-lemony-fresh = lemony fresh -reagent-physical-desc-soapy = soapy reagent-physical-desc-crisp = crisp reagent-physical-desc-citric = citric reagent-physical-desc-acidic = acidic @@ -76,7 +75,6 @@ reagent-physical-desc-overpowering = overpowering reagent-physical-desc-sour = sour reagent-physical-desc-pungent = pungent reagent-physical-desc-clumpy = clumpy -reagent-physical-desc-strong-smelling = strong-smelling reagent-physical-desc-odorless = odorless reagent-physical-desc-gloopy = gloopy reagent-physical-desc-cloudy = cloudy diff --git a/Resources/Locale/en-US/reagents/meta/toxins.ftl b/Resources/Locale/en-US/reagents/meta/toxins.ftl index 660da9c271..09b135e7f5 100644 --- a/Resources/Locale/en-US/reagents/meta/toxins.ftl +++ b/Resources/Locale/en-US/reagents/meta/toxins.ftl @@ -75,3 +75,6 @@ reagent-desc-vestine = Has an adverse reaction within the body causing major jit reagent-name-tazinide = tazinide reagent-desc-tazinide = A highly dangerous metallic mixture which can interfere with most movement through an electrifying current. + +reagent-name-lipolicide = lipolicide +reagent-desc-lipolicide = A powerful toxin that will destroy fat cells, massively reducing body weight in a short time. Deadly to those without nutriment in their body. diff --git a/Resources/Locale/en-US/reagents/psicodine.ftl b/Resources/Locale/en-US/reagents/psicodine.ftl new file mode 100644 index 0000000000..c9795b11a9 --- /dev/null +++ b/Resources/Locale/en-US/reagents/psicodine.ftl @@ -0,0 +1,3 @@ +psicodine-effect-fearless = You feel totally fearless! +psicodine-effect-anxieties-wash-away = All of your anxieties wash away! +psicodine-effect-at-peace = You feel completely at peace. diff --git a/Resources/Locale/en-US/ui/verbs.ftl b/Resources/Locale/en-US/ui/verbs.ftl deleted file mode 100644 index 1471261dcb..0000000000 --- a/Resources/Locale/en-US/ui/verbs.ftl +++ /dev/null @@ -1,3 +0,0 @@ -### Loc for the various UI-related verbs -ui-verb-toggle-open = Toggle UI -verb-instrument-openui = Play Music diff --git a/Resources/Locale/en-US/verbs/verb-system.ftl b/Resources/Locale/en-US/verbs/verb-system.ftl index 2bebddca61..c626e41ce1 100644 --- a/Resources/Locale/en-US/verbs/verb-system.ftl +++ b/Resources/Locale/en-US/verbs/verb-system.ftl @@ -28,6 +28,7 @@ verb-categories-timer = Set Delay verb-categories-lever = Lever verb-categories-select-type = Select Type verb-categories-fax = Set Destination +verb-categories-power-level = Power Level verb-common-toggle-light = Toggle light verb-common-close = Close diff --git a/Resources/Maps/Shuttles/striker.yml b/Resources/Maps/Shuttles/striker.yml index 6a450f5266..88b113d7fd 100644 --- a/Resources/Maps/Shuttles/striker.yml +++ b/Resources/Maps/Shuttles/striker.yml @@ -1771,7 +1771,7 @@ entities: - type: Transform pos: 0.5436061,-7.5129323 parent: 325 -- proto: SpawnPointLoneNukeOperative +- proto: SpawnPointNukies entities: - uid: 322 components: diff --git a/Resources/Maps/saltern.yml b/Resources/Maps/saltern.yml index 22b08b60b6..b622f69799 100644 --- a/Resources/Maps/saltern.yml +++ b/Resources/Maps/saltern.yml @@ -3273,12 +3273,13 @@ entities: - type: MetaData - type: Transform - type: Map + mapPaused: True - type: PhysicsMap + - type: GridTree + - type: MovedGrids - type: Broadphase - type: OccluderTree - type: LoadedMap - - type: GridTree - - type: MovedGrids - proto: AcousticGuitarInstrument entities: - uid: 3146 @@ -6119,8 +6120,6 @@ entities: - data: null ReagentId: Leporazine Quantity: 40 - - type: MixableSolution - solution: beaker - uid: 10800 components: - type: Transform @@ -32842,9 +32841,6 @@ entities: - type: Transform pos: 38.5,-5.5 parent: 31 - - type: Door - secondsUntilStateChange: -9978.97 - state: Closing - uid: 4019 components: - type: Transform @@ -33106,9 +33102,6 @@ entities: - type: Transform pos: 1.5,-2.5 parent: 31 - - type: Door - secondsUntilStateChange: -80.957985 - state: Closing - uid: 3944 components: - type: Transform @@ -58376,6 +58369,11 @@ entities: - type: Transform pos: 42.5,-7.5 parent: 31 + - uid: 11452 + components: + - type: Transform + pos: -4.5,-3.5 + parent: 31 - proto: StorageCanister entities: - uid: 1108 diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index b91b26e357..be66691564 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -92,7 +92,6 @@ state: gib event: !type:ActivateImplantEvent - - type: entity id: ActionActivateFreedomImplant name: Break Free @@ -171,6 +170,21 @@ state: icon event: !type:UseDnaScramblerImplantEvent +- type: entity + id: ActionMorphGeras + name: Morph into Geras + description: Morphs you into a Geras - a miniature version of you which allows you to move fast, at the cost of your inventory. + noSpawn: true + components: + - type: InstantAction + itemIconStyle: BigAction + useDelay: 10 # prevent spam + priority: -20 + icon: + sprite: Mobs/Aliens/slimes.rsi + state: blue_adult_slime + event: !type:MorphIntoGeras + - type: entity id: ActionToggleSuitPiece name: Toggle Suit Piece diff --git a/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml b/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml index bd00b0c2d4..8d3bea5075 100644 --- a/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml +++ b/Resources/Prototypes/Catalog/Cargo/cargo_engines.yml @@ -28,17 +28,17 @@ category: cargoproduct-category-name-engineering group: market -#- type: cargoProduct -# name: "emitter crate" -# id: EngineSingularityEmitter -# description: "Contains an emitter. Used only for dangerous applications." -# icon: -# sprite: Structures/Power/Generation/Singularity/emitter.rsi -# state: emitter2 -# product: CrateEngineeringSingularityEmitter -# cost: 3000 -# category: cargoproduct-category-name-engineering -# group: market +- type: cargoProduct + name: "emitter crate" + id: EngineSingularityEmitter + description: "Contains an emitter. Used only for dangerous applications." + icon: + sprite: Structures/Power/Generation/Singularity/emitter.rsi + state: emitter2 + product: CrateEngineeringSingularityEmitter + cost: 3000 + category: cargoproduct-category-name-engineering + group: market - type: cargoProduct id: EngineSingularityCollector diff --git a/Resources/Prototypes/Catalog/Fills/Crates/engines.yml b/Resources/Prototypes/Catalog/Fills/Crates/engines.yml index 9b47036b01..79698b550a 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/engines.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/engines.yml @@ -42,7 +42,7 @@ components: - type: StorageFill contents: - - id: Emitter # TODO change to flatpack + - id: EmitterFlatpack # TODO change to flatpack - type: entity id: CrateEngineeringSingularityCollector diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml index 0aa814196a..b54c2cdbcf 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/sec.yml @@ -14,6 +14,7 @@ ClothingEyesHudSecurity: 2 ClothingEyesEyepatchHudSecurity: 2 ClothingBeltSecurityWebbing: 5 + CombatKnife: 3 Zipties: 12 RiotShield: 2 RiotLaserShield: 2 diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index 6375ac37f7..a7c2dbc7a4 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -122,7 +122,7 @@ name: uplink-sniper-bundle-name description: uplink-sniper-bundle-desc icon: { sprite: /Textures/Objects/Weapons/Guns/Snipers/heavy_sniper.rsi, state: base } - productEntity: BriefcaseSyndieSniperBundleFilled + productEntity: BriefcaseSyndieSniperBundleFilled cost: Telecrystal: 12 categories: @@ -990,15 +990,10 @@ icon: { sprite: /Textures/Mobs/Pets/cat.rsi, state: syndicat } productEntity: MobCatSyndy cost: - Telecrystal: 10 + Telecrystal: 6 categories: - UplinkAllies - conditions: - - !type:StoreWhitelistCondition - whitelist: - tags: - - NukeOpsUplink - + - type: listing id: UplinkSyndicatePersonalAI name: uplink-syndicate-pai-name diff --git a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml index 1f90b42152..1bae86a022 100644 --- a/Resources/Prototypes/Entities/Clothing/Belt/belts.yml +++ b/Resources/Prototypes/Entities/Clothing/Belt/belts.yml @@ -472,6 +472,8 @@ - Sidearm - MagazinePistol - MagazineMagnum + - CombatKnife + - Truncheon components: - Stunbaton - FlashOnTrigger diff --git a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml index 850050b2d3..3a86464048 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml @@ -278,7 +278,7 @@ sprite: Clothing/Mask/sterile.rsi - type: IngestionBlocker - type: Item - storedRotation: -90 + size: Tiny - type: IdentityBlocker coverage: MOUTH diff --git a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml index ddbccfe686..727b55eb4e 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml @@ -84,8 +84,7 @@ name: ghost-role-information-loneop-name description: ghost-role-information-loneop-description rules: ghost-role-information-loneop-rules - - type: GhostRoleMobSpawner - prototype: MobHumanLoneNuclearOperative + - type: GhostRoleAntagSpawner - type: Sprite sprite: Markers/jobs.rsi layers: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index aeae35efbf..819c7beacf 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -2731,6 +2731,16 @@ - type: NpcFactionMember factions: - Syndicate + - type: MeleeWeapon + damage: + types: + Piercing: 10 + Structural: 10 + - type: Insulated + - type: Tag + tags: + - DoorBumpOpener + - type: MovementAlwaysTouching - type: entity name: space cat diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index 10e9218d6e..95c30a174f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -1,16 +1,10 @@ - type: entity name: basic slime - id: MobAdultSlimes + id: BaseMobAdultSlimes parent: [ SimpleMobBase, MobCombat ] abstract: true description: It looks so much like jelly. I wonder what it tastes like? components: - - type: NpcFactionMember - factions: - - SimpleNeutral - - type: HTN - rootTask: - task: SimpleHostileCompound - type: Sprite drawdepth: Mobs sprite: Mobs/Aliens/slimes.rsi @@ -111,6 +105,19 @@ successChance: 0.5 interactSuccessString: petting-success-slimes interactFailureString: petting-failure-generic + - type: Speech + speechVerb: Slime + speechSounds: Slime + - type: TypingIndicator + proto: slime + +- type: entity + name: basic slime + id: MobAdultSlimes + parent: BaseMobAdultSlimes + abstract: true + description: It looks so much like jelly. I wonder what it tastes like? + components: - type: ReplacementAccent accent: slimes - type: GhostTakeoverAvailable @@ -118,11 +125,37 @@ makeSentient: true name: ghost-role-information-slimes-name description: ghost-role-information-slimes-description - - type: Speech - speechVerb: Slime - speechSounds: Slime - - type: TypingIndicator - proto: slime + - type: NpcFactionMember + factions: + - SimpleNeutral + - type: HTN + rootTask: + task: SimpleHostileCompound + +- type: entity + name: geras + description: A geras of a slime - the name is ironic, isn't it? + id: MobSlimesGeras + parent: BaseMobAdultSlimes + noSpawn: true + components: + # they portable... + - type: MultiHandedItem + - type: Item + size: Huge + - type: Sprite + color: "#FFFFFF55" + - type: MeleeWeapon + attackRate: 2 + damage: + types: + Blunt: 4 + - type: DamageStateVisuals + states: + Alive: + Base: blue_adult_slime + Dead: + Base: blue_adult_slime_dead - type: entity name: blue slime diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml index 3eabb7dc07..de2f88e59d 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml @@ -12,6 +12,31 @@ - type: Body prototype: Slime requiredLegs: 2 + # they like eat it idk lol + - type: Storage + clickInsert: false + grid: + - 0,0,1,2 + maxItemSize: Large + storageInsertSound: + path: /Audio/Voice/Slime/slime_squish.ogg + - type: ContainerContainer + containers: + storagebase: !type:Container + ents: [] + - type: UserInterface + interfaces: + - key: enum.StorageUiKey.Key + type: StorageBoundUserInterface + - key: enum.VoiceMaskUIKey.Key + type: VoiceMaskBoundUserInterface + - key: enum.HumanoidMarkingModifierKey.Key + type: HumanoidMarkingModifierBoundUserInterface + - key: enum.StrippingUiKey.Key + type: StrippableBoundUserInterface + # to prevent bag open/honk spam + - type: UseDelay + delay: 0.5 - type: HumanoidAppearance species: SlimePerson - type: Speech @@ -27,6 +52,16 @@ - type: Damageable damageContainer: Biological damageModifierSet: Slime + - type: Geras + - type: PassiveDamage # Around 8 damage a minute healed + allowedStates: + - Alive + damageCap: 65 + damage: + types: + Heat: -0.14 + groups: + Brute: -0.14 - type: DamageVisuals damageOverlayGroups: Brute: diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml index 065d62c748..0a2b68d0a1 100644 --- a/Resources/Prototypes/Entities/Mobs/base.yml +++ b/Resources/Prototypes/Entities/Mobs/base.yml @@ -192,7 +192,7 @@ canResistFire: true damage: #per second, scales with number of fire 'stacks' types: - Heat: 3 + Heat: 1.5 - type: FireVisuals sprite: Mobs/Effects/onfire.rsi normalState: Generic_mob_burning diff --git a/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml b/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml index 5499348bd1..2aecd13288 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/flatpack.yml @@ -110,6 +110,20 @@ - type: GuideHelp guides: [ Singularity, Power ] +- type: entity + parent: BaseFlatpack + id: EmitterFlatpack + name: emitter flatpack + description: A flatpack used for constructing an emitter. + components: + - type: Flatpack + entity: Emitter + - type: Sprite + layers: + - state: emitter + - type: GuideHelp + guides: [ Singularity, Power ] + - type: entity parent: BaseFlatpack id: TeslaGeneratorFlatpack diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml index d37523bd73..daa0d9bc20 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml @@ -318,6 +318,7 @@ - HoloprojectorBorg - SprayBottleSpaceCleaner - Dropper + - TrashBag # medical modules - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Tools/jammer.yml b/Resources/Prototypes/Entities/Objects/Tools/jammer.yml index beb3695627..b456a23f1f 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/jammer.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/jammer.yml @@ -6,8 +6,26 @@ components: - type: Sprite sprite: Objects/Devices/jammer.rsi - state: jammer + layers: + - state: jammer + - state: jammer_high_charge + map: ["enum.RadioJammerLayers.LED"] + shader: unshaded + visible: false - type: RadioJammer + settings: + - wattage: 1 + range: 2.5 + message: radio-jammer-component-set-message-low + name: radio-jammer-component-setting-low + - wattage: 2 + range: 6 + message: radio-jammer-component-set-message-medium + name: radio-jammer-component-setting-medium + - wattage: 12 + range: 12 + message: radio-jammer-component-set-message-high + name: radio-jammer-component-setting-high - type: PowerCellSlot cellSlotId: cell_slot - type: ContainerContainer @@ -18,3 +36,15 @@ cell_slot: name: power-cell-slot-component-slot-name-default startingItem: PowerCellMedium + - type: Appearance + - type: GenericVisualizer + visuals: + enum.RadioJammerVisuals.LEDOn: + RadioJammerLayers.LED: + True: { visible: True } + False: { visible: False } + enum.RadioJammerVisuals.ChargeLevel: + RadioJammerLayers.LED: + Low: {state: jammer_low_charge} + Medium: {state: jammer_medium_charge} + High: {state: jammer_high_charge} diff --git a/Resources/Prototypes/Entities/Objects/Weapons/security.yml b/Resources/Prototypes/Entities/Objects/Weapons/security.yml index b9d409fb3d..101314a1fb 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/security.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/security.yml @@ -102,6 +102,9 @@ bluntStaminaDamageFactor: 1.5 - type: Item size: Normal + - type: Tag + tags: + - Truncheon - type: Clothing sprite: Objects/Weapons/Melee/truncheon.rsi quickEquip: false diff --git a/Resources/Prototypes/Entities/Structures/cargo_telepad.yml b/Resources/Prototypes/Entities/Structures/cargo_telepad.yml index d395235a53..9dc9f77cff 100644 --- a/Resources/Prototypes/Entities/Structures/cargo_telepad.yml +++ b/Resources/Prototypes/Entities/Structures/cargo_telepad.yml @@ -47,3 +47,5 @@ board: CargoTelepadMachineCircuitboard - type: Appearance - type: CollideOnAnchor + - type: NameIdentifier + group: CargoTelepads diff --git a/Resources/Prototypes/Flavors/flavors.yml b/Resources/Prototypes/Flavors/flavors.yml index 2b55efc21b..25ed9d3372 100644 --- a/Resources/Prototypes/Flavors/flavors.yml +++ b/Resources/Prototypes/Flavors/flavors.yml @@ -1058,3 +1058,13 @@ id: violets flavorType: Complex description: flavor-complex-violets + +- type: flavor + id: mothballs + flavorType: Complex + description: flavor-complex-mothballs + +- type: flavor + id: paintthinner + flavorType: Complex + description: flavor-complex-paint-thinner diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 32cfd69cb0..45519e840d 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -405,11 +405,28 @@ weight: 3 duration: 1 - type: ZombieRule - minStartDelay: 0 #let them know immediately - maxStartDelay: 10 - maxInitialInfected: 3 #fewer zombies - minInitialInfectedGrace: 300 #less time to prepare - maxInitialInfectedGrace: 450 + - type: AntagSelection + definitions: + - prefRoles: [ InitialInfected ] + max: 3 + playerRatio: 10 + blacklist: + components: + - ZombieImmune + - InitialInfectedExempt + briefing: + text: zombie-patientzero-role-greeting + color: Plum + sound: "/Audio/Ambience/Antag/zombie_start.ogg" + components: + - type: PendingZombie #less time to prepare than normal + minInitialInfectedGrace: 300 + maxInitialInfectedGrace: 450 + - type: ZombifyOnDeath + - type: IncurableZombie + mindComponents: + - type: InitialInfectedRole + prototype: InitialInfected - type: entity id: LoneOpsSpawn @@ -422,7 +439,29 @@ minimumPlayers: 20 reoccurrenceDelay: 30 duration: 1 - - type: LoneOpsSpawnRule + - type: LoadMapRule + mapPath: /Maps/Shuttles/striker.yml + - type: NukeopsRule + roundEndBehavior: Nothing + - type: AntagSelection + definitions: + - spawnerPrototype: SpawnPointLoneNukeOperative + min: 1 + max: 1 + pickPlayer: false + startingGear: SyndicateLoneOperativeGearFull + components: + - type: NukeOperative + - type: RandomMetadata + nameSegments: + - SyndicateNamesPrefix + - SyndicateNamesNormal + - type: NpcFactionMember + factions: + - Syndicate + mindComponents: + - type: NukeopsRole + prototype: Nukeops - type: entity id: MassHallucinations diff --git a/Resources/Prototypes/GameRules/midround.yml b/Resources/Prototypes/GameRules/midround.yml index 37fc4b44cd..bb870f6007 100644 --- a/Resources/Prototypes/GameRules/midround.yml +++ b/Resources/Prototypes/GameRules/midround.yml @@ -34,6 +34,23 @@ id: Thief components: - type: ThiefRule + - type: AntagSelection + definitions: + - prefRoles: [ Thief ] + maxRange: + min: 1 + max: 3 + playerRatio: 1 + allowNonHumans: true + multiAntagSetting: All + startingGear: ThiefGear + components: + - type: Pacified + mindComponents: + - type: ThiefRole + prototype: Thief + briefing: + sound: "/Audio/Misc/thief_greeting.ogg" - type: entity noSpawn: true diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 6d2b1f29d1..8218e1bdd1 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -70,29 +70,114 @@ components: - type: GameRule minPlayers: 20 + - type: RandomMetadata #this generates the random operation name cuz it's cool. + nameSegments: + - operationPrefix + - operationSuffix - type: NukeopsRule - faction: Syndicate - -- type: entity - id: Pirates - parent: BaseGameRule - noSpawn: true - components: - - type: PiratesRule + - type: LoadMapRule + gameMap: NukieOutpost + - type: AntagSelection + selectionTime: PrePlayerSpawn + definitions: + - prefRoles: [ NukeopsCommander ] + fallbackRoles: [ Nukeops, NukeopsMedic ] + max: 1 + playerRatio: 10 + startingGear: SyndicateCommanderGearFull + components: + - type: NukeOperative + - type: RandomMetadata + nameSegments: + - nukeops-role-commander + - SyndicateNamesElite + - type: NpcFactionMember + factions: + - Syndicate + mindComponents: + - type: NukeopsRole + prototype: NukeopsCommander + - prefRoles: [ NukeopsMedic ] + fallbackRoles: [ Nukeops, NukeopsCommander ] + max: 1 + playerRatio: 10 + startingGear: SyndicateOperativeMedicFull + components: + - type: NukeOperative + - type: RandomMetadata + nameSegments: + - nukeops-role-agent + - SyndicateNamesNormal + - type: NpcFactionMember + factions: + - Syndicate + mindComponents: + - type: NukeopsRole + prototype: NukeopsMedic + - prefRoles: [ Nukeops ] + fallbackRoles: [ NukeopsCommander, NukeopsMedic ] + min: 0 + max: 3 + playerRatio: 10 + startingGear: SyndicateOperativeGearFull + components: + - type: NukeOperative + - type: RandomMetadata + nameSegments: + - nukeops-role-operator + - SyndicateNamesNormal + - type: NpcFactionMember + factions: + - Syndicate + mindComponents: + - type: NukeopsRole + prototype: Nukeops - type: entity id: Traitor parent: BaseGameRule noSpawn: true components: + - type: GameRule + minPlayers: 5 + delay: + min: 240 + max: 420 - type: TraitorRule + - type: AntagSelection + definitions: + - prefRoles: [ Traitor ] + max: 12 + playerRatio: 10 + lateJoinAdditional: true + mindComponents: + - type: TraitorRole + prototype: Traitor - type: entity id: Revolutionary parent: BaseGameRule noSpawn: true components: + - type: GameRule + minPlayers: 15 - type: RevolutionaryRule + - type: AntagSelection + definitions: + - prefRoles: [ HeadRev ] + max: 3 + playerRatio: 15 + briefing: + text: head-rev-role-greeting + color: CornflowerBlue + sound: "/Audio/Ambience/Antag/headrev_start.ogg" + startingGear: HeadRevGear + components: + - type: Revolutionary + - type: HeadRevolutionary + mindComponents: + - type: RevolutionaryRole + prototype: HeadRev - type: entity id: Sandbox @@ -113,7 +198,32 @@ parent: BaseGameRule noSpawn: true components: + - type: GameRule + minPlayers: 20 + delay: + min: 600 + max: 900 - type: ZombieRule + - type: AntagSelection + definitions: + - prefRoles: [ InitialInfected ] + max: 6 + playerRatio: 10 + blacklist: + components: + - ZombieImmune + - InitialInfectedExempt + briefing: + text: zombie-patientzero-role-greeting + color: Plum + sound: "/Audio/Ambience/Antag/zombie_start.ogg" + components: + - type: PendingZombie + - type: ZombifyOnDeath + - type: IncurableZombie + mindComponents: + - type: InitialInfectedRole + prototype: InitialInfected # event schedulers - type: entity @@ -142,7 +252,6 @@ - id: BasicTrashVariationPass - id: SolidWallRustingVariationPass - id: ReinforcedWallRustingVariationPass - - id: CutWireVariationPass - id: BasicPuddleMessVariationPass prob: 0.99 orGroup: puddleMess diff --git a/Resources/Prototypes/Polymorphs/polymorph.yml b/Resources/Prototypes/Polymorphs/polymorph.yml index b4249f8a3e..ce89f41d37 100644 --- a/Resources/Prototypes/Polymorphs/polymorph.yml +++ b/Resources/Prototypes/Polymorphs/polymorph.yml @@ -75,6 +75,17 @@ inventory: Transfer revertOnDeath: true +- type: polymorph + id: SlimeMorphGeras + configuration: + entity: MobSlimesGeras + transferName: false + transferHumanoidAppearance: false + inventory: Drop + transferDamage: true + revertOnDeath: true + revertOnCrit: true + # this is a test for transferring some visual appearance stuff - type: polymorph id: TestHumanMorph diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml index 5df80543a7..aae367d9eb 100644 --- a/Resources/Prototypes/Reagents/medicine.yml +++ b/Resources/Prototypes/Reagents/medicine.yml @@ -1199,3 +1199,67 @@ Heat: -3.0 Shock: -3.0 Caustic: -1.0 + +- type: reagent + id : Mannitol # currently this is just a way to create psicodine + name: reagent-name-mannitol + group: Medicine + desc: reagent-desc-mannitol + physicalDesc: reagent-physical-desc-opaque + flavor: sweet + color: "#A0A0A0" + metabolisms: + Medicine: + effects: + - !type:PopupMessage + conditions: + - !type:ReagentThreshold + min: 15 + type: Local + visualType: Medium + messages: [ "mannitol-effect-enlightened" ] + probability: 0.2 + +- type: reagent + id: Psicodine + name: reagent-name-psicodine + group: Medicine + desc: reagent-desc-psicodine + physicalDesc: reagent-physical-desc-shiny + flavor: bitter + color: "#07E79E" + metabolisms: + Medicine: + effects: + - !type:HealthChange + conditions: + - !type:ReagentThreshold + min: 30 + damage: + types: + Poison: 2 + - !type:GenericStatusEffect + conditions: + - !type:ReagentThreshold + min: 30 + key: SeeingRainbows + component: SeeingRainbows + type: Add + time: 8 + refresh: false + - !type:GenericStatusEffect + key: Jitter + time: 2.0 + type: Remove + - !type:GenericStatusEffect + key: Drunk + time: 6.0 + type: Remove + - !type:PopupMessage # we dont have sanity/mood so this will have to do + type: Local + visualType: Medium + messages: + - "psicodine-effect-fearless" + - "psicodine-effect-anxieties-wash-away" + - "psicodine-effect-at-peace" + probability: 0.2 diff --git a/Resources/Prototypes/Reagents/narcotics.yml b/Resources/Prototypes/Reagents/narcotics.yml index cefc8043b0..9b14fa2bc8 100644 --- a/Resources/Prototypes/Reagents/narcotics.yml +++ b/Resources/Prototypes/Reagents/narcotics.yml @@ -407,3 +407,53 @@ conditions: - !type:ReagentThreshold min: 20 + +- type: reagent + id: Happiness + name: reagent-name-happiness + group: Narcotics + desc: reagent-desc-happiness + physicalDesc: reagent-physical-desc-soothing + flavor: paintthinner + color: "#EE35FF" + metabolisms: + Narcotic: + effects: + - !type:Emote + emote: Laugh + showInChat: true + probability: 0.1 + conditions: + - !type:ReagentThreshold + max: 20 + - !type:Emote + emote: Whistle + showInChat: true + probability: 0.1 + conditions: + - !type:ReagentThreshold + max: 20 + - !type:Emote + emote: Crying + showInChat: true + probability: 0.1 + conditions: + - !type:ReagentThreshold + min: 20 + - !type:PopupMessage # we dont have sanity/mood so this will have to do + type: Local + visualType: Medium + messages: + - "psicodine-effect-fearless" + - "psicodine-effect-anxieties-wash-away" + - "psicodine-effect-at-peace" + probability: 0.2 + conditions: + - !type:ReagentThreshold + max: 20 + - !type:GenericStatusEffect + key: SeeingRainbows + component: SeeingRainbows + type: Add + time: 5 + refresh: false diff --git a/Resources/Prototypes/Reagents/toxins.yml b/Resources/Prototypes/Reagents/toxins.yml index 8c91c5f226..f5b196acf6 100644 --- a/Resources/Prototypes/Reagents/toxins.yml +++ b/Resources/Prototypes/Reagents/toxins.yml @@ -641,3 +641,23 @@ - !type:Electrocute probability: 0.8 +- type: reagent + id: Lipolicide + name: reagent-name-lipolicide + group: Toxins + desc: reagent-desc-lipolicide + physicalDesc: reagent-physical-desc-strong-smelling + flavor: mothballs #why does weightloss juice taste like mothballs + color: "#F0FFF0" + metabolisms: + Poison: + effects: + - !type:HealthChange + conditions: + - !type:Hunger + max: 50 + damage: + types: + Poison: 2 + - !type:SatiateHunger + factor: -6 diff --git a/Resources/Prototypes/Recipes/Lathes/security.yml b/Resources/Prototypes/Recipes/Lathes/security.yml index b3100ed70b..f536242238 100644 --- a/Resources/Prototypes/Recipes/Lathes/security.yml +++ b/Resources/Prototypes/Recipes/Lathes/security.yml @@ -30,6 +30,15 @@ Steel: 300 Plastic: 300 +- type: latheRecipe + id: CombatKnife + result: CombatKnife + category: Weapons + completetime: 2 + materials: + Steel: 250 + Plastic: 100 + - type: latheRecipe id: WeaponLaserCarbine result: WeaponLaserCarbine diff --git a/Resources/Prototypes/Recipes/Reactions/medicine.yml b/Resources/Prototypes/Recipes/Reactions/medicine.yml index 60cb8a21f3..b13c6bb71b 100644 --- a/Resources/Prototypes/Recipes/Reactions/medicine.yml +++ b/Resources/Prototypes/Recipes/Reactions/medicine.yml @@ -298,6 +298,18 @@ products: Lipozine: 3 +- type: reaction + id: Mannitol + reactants: + Hydrogen: + amount: 1 + Water: + amount: 1 + Sugar: + amount: 1 + products: + Mannitol: 3 + - type: reaction id: MindbreakerToxin minTemp: 370 @@ -571,3 +583,43 @@ amount: 2 products: Aloxadone: 4 + +- type: reaction + id: Psicodine + impact: Medium + reactants: + Mannitol: + amount: 2 + Impedrezene: + amount: 1 + Water: + amount: 2 + products: + Psicodine: 4 + +- type: reaction + id: Lipolicide + reactants: + Ephedrine: + amount: 1 + Diethylamine: + amount: 1 + Mercury: + amount: 1 + products: + Lipolicide: 3 + +- type: reaction + id: Happiness + reactants: + Laughter: + amount: 2 + Epinephrine: + amount: 1 + Ethanol: + amount: 1 + Plasma: + amount: 5 + catalyst: true + products: + Happiness: 4 diff --git a/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml b/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml index 318490931f..3591ce7008 100644 --- a/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml +++ b/Resources/Prototypes/Recipes/Reactions/pyrotechnic.yml @@ -31,6 +31,7 @@ - type: reaction id: ChlorineTrifluoride + minTemp: 370 priority: 20 reactants: Chlorine: @@ -38,7 +39,6 @@ Fluorine: amount: 3 effects: - # TODO solution temperature!! - !type:ExplosionReactionEffect explosionType: Default # 15 damage per intensity. maxIntensity: 200 diff --git a/Resources/Prototypes/Research/civilianservices.yml b/Resources/Prototypes/Research/civilianservices.yml index afb1f0ff50..b990eb6ae4 100644 --- a/Resources/Prototypes/Research/civilianservices.yml +++ b/Resources/Prototypes/Research/civilianservices.yml @@ -191,8 +191,6 @@ - WeaponSprayNozzle - ClothingBackpackWaterTank -# Tier 3 - - type: technology id: BluespaceCargoTransport name: research-technology-bluespace-cargo-transport @@ -200,11 +198,13 @@ sprite: Structures/cargo_telepad.rsi state: display discipline: CivilianServices - tier: 3 + tier: 2 cost: 15000 recipeUnlocks: - CargoTelepadMachineCircuitboard +# Tier 3 + - type: technology id: QuantumFiberWeaving name: research-technology-quantum-fiber-weaving diff --git a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml index 70d74b932a..946100fb96 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml @@ -273,8 +273,18 @@ #Head Rev Gear - type: startingGear id: HeadRevGear - equipment: - pocket2: Flash + storage: + back: + - Flash + - ClothingEyesGlassesSunglasses + +#Thief Gear +- type: startingGear + id: ThiefGear + storage: + back: + - ToolboxThief + - ClothingHandsChameleonThief #Gladiator with spear - type: startingGear diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml index 3bbb15ec1a..9b931c41ee 100644 --- a/Resources/Prototypes/game_presets.yml +++ b/Resources/Prototypes/game_presets.yml @@ -153,15 +153,3 @@ - Zombie - BasicStationEventScheduler - BasicRoundstartVariation - -- type: gamePreset - id: Pirates - alias: - - pirates - name: pirates-title - description: pirates-description - showInVote: false - rules: - - Pirates - - BasicStationEventScheduler - - BasicRoundstartVariation diff --git a/Resources/Prototypes/name_identifier_groups.yml b/Resources/Prototypes/name_identifier_groups.yml index 82c2f3bce9..4823e31f55 100644 --- a/Resources/Prototypes/name_identifier_groups.yml +++ b/Resources/Prototypes/name_identifier_groups.yml @@ -37,3 +37,9 @@ id: Bounty minValue: 0 maxValue: 999 + +- type: nameIdentifierGroup + id: CargoTelepads + prefix: TELE + minValue: 0 + maxValue: 999 diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index f6cedeb937..91b79f3d8e 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -1235,6 +1235,9 @@ - type: Tag id: TrashBag +- type: Tag + id: Truncheon + - type: Tag id: Unimplantable diff --git a/Resources/ServerInfo/Guidebook/Engineering/AME.xml b/Resources/ServerInfo/Guidebook/Engineering/AME.xml index 202ed16d3d..4b55ce85c5 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/AME.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/AME.xml @@ -1,26 +1,25 @@ -# Antimatter Engine (AME) + # Antimatter Engine (AME) -The AME is one of the simplest engines available. You put together the multi-tile structure, stick some fuel into it, and you're all set. This doesn't mean it isn't potentially dangerous with overheating though. + The AME is one of the simplest engines available. You put together the multi-tile structure, stick some fuel into it, and you're all set. This doesn't mean it is perfectly safe though; you may need to deal with the AME overheating. -## Construction -Required parts: - - - - - + ## Construction + Required parts: + + + + + -To assemble an AME, start by wrenching down the controller on the far end of a HV wire. On most stations, there's catwalks to assist with this. From there, start putting down a 3x3 or larger square of AME parts in preparation for construction, making sure to maximize the number of "center" pieces that are surrounded on all 8 sides. + To assemble an AME, start by wrenching down the controller on the near end of a HV wire. On most stations, there's catwalks to assist with this. From there, start putting down a 3x3 or larger square of AME parts in preparation for construction, making sure to maximize the number of "center" pieces that are surrounded on all 8 sides. -Once this is done, you can use a multitool to convert each AME part into shielding, which should form a finished AME configuration. From there, insert a fuel jar, set the fuel rate to [color=#a4885c]twice the core count or less[/color], and turn on injection. + Once this is done, you can use a multitool to convert each AME part into shielding, which should form a finished AME configuration. From there, insert a fuel jar, set the fuel rate to [color=#a4885c]twice the core count or less[/color], and turn on injection. Any more than this ratio will eventually result in the engine [color=#ff0000]overheating and[/color], shortly afterwards, [color=#ff0000]exploding[/color]. -## Fuel Economy -The closer you are to the perfect ratio of [color=#a4885c]1:2[/color] (1 AME core to 2 fuel rate) the more efficient you'll be. You're cutting fuel efficiency to [color=#a4885c]50% and less[/color] if you're using more cores, but less fuel injection rate. -For an example [color=#76db91]3 core and 6 fuel rate[/color] will generate [color=#76db91]240kW[/color], while [color=#f0684d]8 core 8 fuel rate[/color] will generate [color=#f0684d]160kW[/color]. Generating 80kW less while spending 2 more fuel each injection. + ## Fuel Economy + The closer you are to the perfect ratio of [color=#a4885c]1:2[/color] AME cores to fuel injection rate, the more efficient you'll be. You're cutting fuel efficiency to [color=#a4885c]50% and less[/color] if you're using more cores, but a lower fuel injection rate. + For an example, [color=#76db91]3 cores and 6 fuel injected[/color] will generate [color=#76db91]240kW[/color], while [color=#f0684d]8 cores and 8 fuel injected[/color] will generate [color=#f0684d]160kW[/color]; you'd be generating 80kW less while spending 2 more fuel per injection. -## Upgrading the AME + ## Upgrading the AME -You can generally only upgrade the AME by getting more cores, which can be done by ordering more AME packages from [color=#a4885c]cargo[/color]. - - \ No newline at end of file + You can generally only upgrade the AME by installing more cores, which can be done by ordering more AME flatpacks from [color=#a4885c]Cargo[/color]. + diff --git a/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml b/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml index f22ea40a0d..6399521baf 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/AccessConfigurator.xml @@ -1,32 +1,34 @@ -# Access Configurator -The access configurator is a tool used to specify what type of personnel may use certain devices. + # Access Configurator + The access configurator is a tool used to specify what type of personnel may use certain devices. - - - + + + -Configurable devices can include airlocks, secure crates and lockers, as well as access restricted machines. + Configurable devices can include airlocks, secure crates and lockers, as well as access restricted machines. + Note: Airlocks can have their accesses configured by the [color=#a4885c]Network Configurator[/color] (or multitool), for convenience. -## Where to find access configurators -Each station is equipped with up to two access configurators. The first is in the possession of the Chief Engineer, while the second can be found with the Head of Personnel. + ## Where to find Access Configurators + Each station is equipped with up to two access configurators. The first is in the possession of the Chief Engineer, while the second can be found with the Head of Personnel. -## How to use the access configurator -To modify a device using the access configurator -- First, use the access configurator on the chosen device to link them together. This will automatically open the configurator UI. -- Next, insert an ID card into the access configurator. -- Set the access requirements of the connected device. What requirements can be added or removed will depend upon the access privileges of the inserted ID card. -- Any changes made will be applied immediately - simply eject the ID card from the access configurator and close the UI when you are done. + ## How to use the access configurator + To modify a device using the access configurator: + - First, use the access configurator on the chosen device to link them together. This will automatically open the configurator UI. + - Next, insert an ID card into the access configurator. + - Set the access requirements of the connected device. What requirements can be added or removed will depend upon the access privileges of the inserted ID card. + - Any changes made will be applied [color=#a4885c]immediately[/color] - simply eject the ID card from the access configurator and close the UI when you are done. -## Restrictions on changing access -As a safety precaution, the inserted ID must possess *all* of the access requirements that are currently active on the connected device in order to modify it. + ## Restrictions on changing access + As a safety precaution, the inserted ID must possess [bold]all[/bold] of the access requirements that are currently active on the connected device in order to modify it. -For example, a device which can be access by both 'Science' and 'Medical' personnel can only by modified using an ID card that has access to both of these departments. The access configurator will warn the user if the inserted ID card does not have sufficient privileges to modify a device. + For example, a device which can be access by both 'Science' and 'Medical' personnel can only by modified using an ID card that has access to [color=#a4885c]both[/color] of these departments. + The access configurator will warn the user if the inserted ID card does not have sufficient privileges to modify a device. -A device with no access requirements set, like a public access airlock, can be modified using any valid station ID card. + A device with no access requirements set, like a public access airlock, can be modified using any valid station ID card. -## Repairing damaged ID card readers -Syndicate agents may attempt to hack access restricted devices through the use of a Cryptographic Sequencer (EMAG). This nefarious tool will completely short out any ID card readers that are attached to the device. + ## Repairing damaged ID card readers + Syndicate agents may attempt to hack access restricted devices through the use of a [color=#a4885c]Cryptographic Sequencer (EMAG)[/color]. This nefarious tool will completely short out any ID card readers that are attached to the device. -Crew members will need to partially de/reconstruct affected devices, and then set appropriate access permissions afterwards using the access configurator, to re-establish access restrictions. - \ No newline at end of file + Engineers will need to partially de/reconstruct affected devices, and then set appropriate access permissions afterwards using the access configurator (or network configurator, for airlocks), to re-establish access restrictions. + diff --git a/Resources/ServerInfo/Guidebook/Engineering/AirlockSecurity.xml b/Resources/ServerInfo/Guidebook/Engineering/AirlockSecurity.xml index 125833a0a1..8bfd3902cc 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/AirlockSecurity.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/AirlockSecurity.xml @@ -1,76 +1,76 @@ -# Airlock Upgrades -It is not uncommon for plucky individuals to try and bypass an airlock by meddling with its internal wiring. + # Airlock Upgrades + It is not uncommon for plucky individuals to try and bypass an airlock by meddling with its internal wiring. -Fortunately, certain countermeasures can installed into airlocks to inconvenience any would be trespassers. + Fortunately, certain countermeasures can installed into airlocks to inconvenience any would-be trespassers. -## Medium security airlocks -The most basic form of intrusion deterrence is to install internal steel plating that will prevent access to internal wiring of the airlock. + ## Medium security airlocks + The most basic form of intrusion deterrence is to install a secured steel plating that will prevent access to internal wiring of the airlock. -To upgrade a basic airlock to a medium security airlock, you will require the following materials - - - - - - - - - - - + To upgrade a basic airlock to a medium security airlock, you will require the following materials: + + + + + + + + + + + -To upgrade the basic airlock, -- Use the screwdriver to open the airlock maintenance panel. -- Add the steel sheets to the airlock. -- Weld the steel sheets into place. -- Close the maintenance panel using the screwdriver. + To upgrade a basic airlock: + - Use the screwdriver to open the airlock maintenance panel. + - Add the steel sheets to the airlock. + - Weld the steel sheets into place. + - Close the maintenance panel using the screwdriver. -## High security airlocks -For airlocks leading to the more sensitive areas of the space station, the use of stronger deterrents are advised. High security airlocks have improved armor plating to protect its internal wiring, along with an electrified security grille. + ## High security airlocks + For airlocks leading to the more sensitive areas of the space station, the use of stronger deterrents are advised. High security airlocks have improved armor plating to protect its internal wiring, along with an electrified security grille. -To upgrade a basic airlock to a high security airlock, you will require the following materials - - - - - - - - - - - - - - + To upgrade a medium security airlock to a high security airlock, you will require the following materials: + + + + + + + + + + + + + + -To upgrade the basic airlock, -- Use the screwdriver to open the airlock maintenance panel. -- Add the plasteel sheets to the airlock. -- Weld the plasteel sheets into place. -- Add the metal rods to the airlock. -- Close the maintenance panel using the screwdriver. + To upgrade a medium security airlock: + - Use the screwdriver to open the airlock maintenance panel. + - Add the plasteel sheets to the airlock. + - Weld the plasteel sheets into place. + - Add the metal rods to the airlock. + - Close the maintenance panel using the screwdriver. -## Maximum security airlocks -You can optionally upgrade a high security airlock to a maximum security airlock. Maximum security airlocks possess an additional layer of plasteel plating on top of its other protections. + ## Maximum security airlocks + You can optionally upgrade a high security airlock to a maximum security airlock. Maximum security airlocks possess an additional layer of plasteel plating on top of its other protections. -To upgrade a high security airlock to a maximum security airlock, you will require the following materials - - - - - - - - - - - + To upgrade a high security airlock to a maximum security airlock, you will require the following materials: + + + + + + + + + + + -To upgrade the high security airlock, -- Use the screwdriver to open the airlock maintenance panel. -- Add the plasteel sheets to the airlock. -- Weld the plasteel sheets into place. -- Close the maintenance panel using the screwdriver. - \ No newline at end of file + To upgrade a high security airlock: + - Use the screwdriver to open the airlock maintenance panel. + - Add the plasteel sheets to the airlock. + - Weld the plasteel sheets into place. + - Close the maintenance panel using the screwdriver. + diff --git a/Resources/ServerInfo/Guidebook/Engineering/Atmospherics.xml b/Resources/ServerInfo/Guidebook/Engineering/Atmospherics.xml index 693e3a0209..48d0d9415e 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Atmospherics.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Atmospherics.xml @@ -1,61 +1,67 @@ - -# Atmospherics + + # Atmospherics -Atmospherics setups are a necessity for your long-term comfort but are generally undocumented, resulting in them being a bit tricky to set up. The following attempts to cover the basics. + Atmospherics setups are a necessity for your long-term comfort, but are generally underdocumented, resulting in them being a bit tricky to set up. The following attempts to cover the basics. -## Standard Mix -Breathing pure O2 or pure N2 is generally bad for the health of your crew, and it is recommended to instead aim for a mix of [color=#a4885c]78% N2 and 22% O2 at 101.24kPa.[/color] It's recommended that your gas mixer setup be set to output at least 1000kPa for faster re-pressurization of rooms. - - - - - -Variations on this mix may be necessary for the long-term comfort of atypical crew, for example crew who require a plasma gas mix to survive. For atypical crew, it is recommended to try and give them their own personal space, isolated by either airlock or disposals section. Keep in mind both methods are leaky and you will need scrubbers on both sides of the lock to clean up any leaked gasses. - - - - -## Vents and Scrubbers -Vents and scrubbers are core atmospherics devices that fill and cleanse rooms, respectively. By default, they are configured for filling rooms to standard pressure (101.24kPa) and to remove all non-O2/N2 gasses from a room. They can be reconfigured from their default settings, allowing you to configure how they respond to various types of gasses or pressure levels. This can be done by interacting with an existing air alarm nearby, or installing and connecting them to a new one. + ## Standard Mix + Breathing pure O2 or pure N2 is generally bad for the health of your crew, and it is recommended to instead aim for a mix of [color=#a4885c]78% N2 and 22% O2 at 101.24kPa.[/color] It's recommended that your gas mixer setup be set to output at least 300kPA for faster re-pressurization of rooms, without posing too much of an overpressurization risk, should traitors sabotage the distro. + + + + + + Variations on this mix may be necessary for the long-term comfort of atypical crew, (for example, Voxes, who are poisoned by Oxygen and breathe Nitrogen). For atypical crew (to be implemented), it is recommended to try and give them their own personal space, isolated by either an airlock or disposals section. Keep in mind that both methods are leaky and you will need scrubbers on both sides of the lock to clean up any leaked gasses. + + + + + ## Vents and Scrubbers + Vents and scrubbers are core atmospherics devices that fill and cleanse rooms, respectively. By default, they are configured for filling rooms to standard pressure (101.24kPa) and to remove all non-O2/N2 gasses from a room. They can be reconfigured from their default settings by linking them to an Air Alarm, allowing you to configure how they respond to various types of gasses or pressure levels. - - - - -During standard operation, if a vent detects that the outside environment is space, it will automatically cease operation until a minimum pressure is reached to avoid destruction of necessary gasses. This can be fixed by pressurizing the room up to that minimum pressure by refilling it with gas canister (potentially multiple, if the room is of significant size). + + + + + During standard operation, if a normal vent detects that the outside environment is space, it will automatically cease operation until a minimum pressure is reached to avoid destruction of useful gasses. This can be fixed by pressurizing the room up to that minimum pressure by refilling it with gas canister (potentially multiple, if the room is of significant size). -Should you encounter a situation where scrubbers aren't cleaning a room fast enough, employ portable scrubbers by dragging them to the affected location and wrenching them down. They work much faster than typical scrubbers and can clean up a room quite quickly. Large spills may require you to employ multiple. - - - -# Gas mixes and Burn chambers -In the event you finish all the tasks at hand, you can make some extra power or money by creating new chemical gasses. + Should you encounter a situation where scrubbers aren't cleaning a room fast enough (and the "Siphon" functionality still cannot keep up), employ portable scrubbers by dragging them to the affected location and wrenching them down. They work much faster than typical scrubbers and can clean up a room quite quickly. Large spills may require you to employ multiple. + + + + # Gas mixes and Burn chambers + In the event you finish all the tasks at hand, you can make some extra money by creating new chemical gasses. -##Tritium -Tritium is a clear, green gas that is highly flammable, radioactive, and combusts when in contact with oxygen making it very helpful when running the [color=#a4885c]TEG.[/color] -It can be made by burning 1% Plasma and 96% or more Oxygen in the Burn Chamber. You can extract this gas through scrubbers. + ##Tritium + Tritium is a clear, green gas that is highly flammable, radioactive, and combusts when in contact with oxygen, making it very helpful when running the [color=#a4885c]TEG[/color]. + It can be made by burning 1% Plasma and 96% or more Oxygen in the Burn Chamber (Ideal ratio is 3% Plasma to 97% Oxygen). You can extract this gas through scrubbers. - - - - - - + + + + + + -##Frezon -Frezon is a bluish-green gas that is very complex and very dangerous. To obtain frezon, you must mix Tritium, Oxygen, and Nitrogen in a 70K room to start the reaction, as well as prevent the Tritium from combusting with the oxygen. + ##Frezon + Frezon is a bluish-green gas that is very complex and very dangerous. To obtain frezon, you must mix Tritium, Oxygen, and Nitrogen in a 70K room to start the reaction, and prevent the Tritium from combusting with the oxygen. - - - - - - + + + + + + -It is critical to understand that a frezon leak can devastate the station, causing a wintery hell filled with itchy sweaters and cold burns. Frezon is very cold, and can freeze the station to death if even a few moles get out, so make sure that you lock your canisters or just move your Frezon straight into a storage room. + It is critical to understand that a frezon leak can devastate the station, causing a wintery hell filled with itchy sweaters and cold burns. Frezon is very cold, and can freeze the station to death if even a few moles get out, so make sure that you lock your canisters or just move your Frezon straight into a storage room. -## Reference Sheet -- Standard atmospheric mix is [color=#a4885c]78% N2 and 22% O2 at 101.24kPa.[/color] -- Gas obeys real math. You can use the equation PV = nRT (Pressure kPa * Volume L = Moles * R * Temperature K) to derive information you might need to know about a gas. R is approximately 8.31446 + ## Reference Sheet + - Standard atmospheric mix is [color=#a4885c]78% N2 and 22% O2 at 101.24kPa.[/color] + - Gas obeys real math. You can use the equation: + + [color=cyan]PV = nRT[/color] + + + ([color=#a4885c]Pressure kPa * Volume L = Moles * R * Temperature K[/color]) + to derive information you might need to know about a gas. R is approximately 8.31446. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Construction.xml b/Resources/ServerInfo/Guidebook/Engineering/Construction.xml index 832b831d8e..15f2f15539 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Construction.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Construction.xml @@ -1,11 +1,11 @@ - -# Construction + + # Construction -By pressing [color=#a4885c]G[/color], one can open the construction menu, which allows you to craft and build a variety of objects. + By pressing [color=#a4885c][keybind="OpenCraftingMenu"][/color], one can open the construction menu, which allows you to craft and build a variety of objects. -When placing objects that "snap" to the grid, you can hold [color=#a4885c]shift[/color] to place an entire line at a time, and [color=#a4885c]ctrl[/color] to place an entire square at a time. + When placing objects that "snap" to the grid, you can hold [color=#a4885c]Shift[/color] to place an entire line at a time, and [color=#a4885c]Ctrl[/color] to place an entire grid at a time. -When crafting objects with a lot of ingredients, keep in mind you don't have to hold everything at once, you can simply place the ingredients on the floor or on a table near you and they'll be used up during crafting like normal. + When crafting objects with a lot of ingredients, keep in mind you don't have to hold everything at once; you can simply place the ingredients on the floor, in your backpack or on a table near you, and they'll be used up during crafting like normal. -When placing a "building ghost" somewhere in the world press [color=#a4885c]Middle Mouse Button[/color] to rotate the ghost clockwise. + When placing a "building ghost" somewhere in the world, press [color=#a4885c][keybind="EditorRotateObject"][/color] to rotate the ghost clockwise. If you are building a mirrorable component (think: Gas Mixers/Filters), you can press [color=#a4885c][keybind="EditorFlipObject"][/color] to flip the ghost. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Engineering.xml b/Resources/ServerInfo/Guidebook/Engineering/Engineering.xml index 0f53ea3042..ab48ed1d82 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Engineering.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Engineering.xml @@ -1,22 +1,21 @@ -# Engineering + # Engineering -Engineering is a combination of construction work, repair work, maintaining a death machine that happens to produce power, and making sure the station contains breathable air. + Engineering is a combination of construction work, repair work, maintaining a death machine that happens to produce power, and making sure the station contains breathable air. -## Tools - - - - - - - - - - - -Your core toolset is a small variety of tools. If you're an engineer, then you should have a belt on your waist containing one of each, if not you can likely find them in maintenance and tool storage within assorted toolboxes and vending machines. - -Most tasks will have explainers for how to perform them on examination, for example if you're constructing a wall, it'll tell you the next step if you look at it a bit closer. + ## Tools + + + + + + + + + + + + Your core toolset is a small variety of tools. If you're an engineer, then you should have a belt on your waist containing one of each; if not, you can likely find them in maintenance shafts and in tool storage within assorted toolboxes and vending machines. + Most tasks will have explainers for how to perform them on examination; for example, if you're constructing a wall, it'll tell you the next step if you look at it a bit closer. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Fires.xml b/Resources/ServerInfo/Guidebook/Engineering/Fires.xml index a1c54059b7..e2c83956cc 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Fires.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Fires.xml @@ -1,15 +1,15 @@ - -# Fires & Space + + # Fires & Space -Fires and spacings are an inevitability due to the highly flammable plasma gas and endless vacuum of space present in and around the station, so it's important to know how to manage them. + Fires and spacings are an inevitability due to the highly flammable plasma gas and the endless vacuum of space present in and around the station, so it's important to know how to manage them. -## Spacing -Space is arguably the easier of the two to handle. -While it does render an area uninhabitable, it can be trivially solved by simply sealing the hole that resulted in the vacuum. After that, assuming distro vents and pipes have not been destroyed in some unfortunate accident, the room will slowly begin to repressurize. -Be aware, that active spacings will slowly siphon the air out of the station's air reserves. If you find it impossible to fix structural damage due to some other hazard - make sure to limit the airflow to that room. + ## Spacing + Space is arguably the easier of the two to handle. + While it does render an area uninhabitable, it can be trivially solved by simply sealing the hole that resulted in the vacuum. After that, assuming distro vents and pipes have not been destroyed in some unfortunate accident, the room will slowly begin to repressurize. + Be aware; active spacings will slowly siphon the air out of the station's air reserves. If you find it impossible to fix structural damage due to some other hazard, make sure to limit the airflow to that room. (Currently only half-valid due to the Gas Miners infinitely replenishing most of the useful gases) -## Fires -Fires can be dealt with through a multitude of ways, but some of the most effective methods include: - - Spacing the enflamed area if possible. This will destroy all of the gasses in the room, which may be a problem if you're already straining life support. - - Dumping a Frezon canister into the enflamed area. This will ice over the flames and halt any ongoing reaction, provided you use enough Frezon. Additionally does not result in destruction of material, so you can simply scrub the room afterwards. + ## Fires + Fires can be dealt with through a multitude of ways, but some of the most effective methods include: + - Spacing the enflamed area if possible. This will destroy all of the gasses in the room, which may be a problem if you're already straining life support. + - Dumping a Frezon canister into the enflamed area. This will ice over the flames and halt any ongoing reaction, provided you use enough Frezon. Additionally, this does not result in destruction of material, so you can simply scrub the room afterwards. diff --git a/Resources/ServerInfo/Guidebook/Engineering/NetworkConfigurator.xml b/Resources/ServerInfo/Guidebook/Engineering/NetworkConfigurator.xml index 445d182ab8..ab95dd2e29 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/NetworkConfigurator.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/NetworkConfigurator.xml @@ -1,39 +1,40 @@ -# Network Configurator -The network configurator allows you to manipulate device lists and link devices together. - - - -The configurator has two modes: List and Link. You can press [color=gray]Alt+Z[/color] or [color=gray]Alt+Y[/color] to switch between them. + # Network Configurator + The network configurator allows you to manipulate device lists, link devices together and configure accesses for airlocks through door electronics. + + + + The configurator has two modes: List and Link. You can press [color=gray]Alt+Z[/color] or [color=gray]Alt+Y[/color] to switch between them. -## List Mode -In list mode you can click on network devices to save them on the configurator and then on a network device that has a device list like the [color=#a4885c]Air Alarm[/color]. + ## List Mode + In list mode you can click on network devices to save them on the configurator and then on a network device that has a device list like the [color=#a4885c]Air Alarm[/color]. -When clicking on a device like the Air Alarm, a UI will open displaying the list currently saved on the device and buttons to manipulate that list. + When clicking on a device like the Air Alarm, a UI will open displaying the list currently saved on the device and buttons to manipulate that list. -You can: -- Replace the current list with the one saved on the configurator -- Add the list on the configurator to the current one -- Clear the current list -- Copy the current list to the configurator -- Visualize the connections to the devices on the current list + You can: + - Replace the current list with the one saved on the configurator + - Add the list on the configurator to the current one + - Clear the current list + - Copy the current list to the configurator + - Visualize the connections to the devices on the current list -Pressing [color=gray]z[/color] or [color=gray]y[/color] opens the list saved on the configurator where you can remove saved devices. + Pressing [color=gray][keybind="ActivateItemInHand"][/color] opens the list saved on the configurator where you can remove saved devices. -## Link Mode -With link mode you can click on a device that is capable of device linking and click on any other device that is either -a sink or source. + ## Link Mode + With link mode, you can click on a device that is capable of device linking and then click on any other device that is either a sink or source. -For example, first clicking on a source like a [color=#a4885c]signal button[/color] and then on sink like a -[color=#a4885c]small light[/color] opens a UI that displays the source ports on the left side and the sink ports on the right. + For example, first clicking on a source, like a [color=#a4885c]signal button[/color], and then on sink, like a [color=#a4885c]small light[/color], opens a UI that displays the source ports on the left side and the sink ports on the right. -Now you can eiter click [color=gray]link defaults[/color] to link the default ports for a source + sink combination or press on a source and then a sink port to connect them. + Now, you can either click [color=gray]Link Defaults[/color] to link the default ports for a source + sink combination, or press on a source port and then a sink port to connect them. -An example of a default link for the aformentioned combinaton of devices would be: - - [color=cyan]Pressed 🠒 Toggle[/color] - -When you're done connecting the ports you want you can click on [color=gray]ok[/color] to close the UI. + An example of a default link for the aformentioned combinaton of devices would be: + + [color=cyan]Pressed 🠒 Toggle[/color] + + When you're done connecting the ports you want you can click on [color=gray]OK[/color] to close the UI. -You can quickly link multiple devices to their default port by first clicking on a device that can be linked and then using [color=gray]alt+left mouse button[/color] on the devices you want to link together. + You can quickly link multiple devices to their default port by first clicking on a device that can be linked and then using [color=gray]Alt+Left Mouse button[/color] on the devices you want to link together. + + ## Airlock Access + To configure an airlock's access, simply take the airlock's door electronics and interact with it using a network configurator (or multitool). Select the accesses you want, insert the door electronics into an airlock frame, and construct to finish! diff --git a/Resources/ServerInfo/Guidebook/Engineering/Networking.xml b/Resources/ServerInfo/Guidebook/Engineering/Networking.xml index 03576c789a..90d1f0891b 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Networking.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Networking.xml @@ -1,25 +1,24 @@ -# Networking -Some devices on the station need to communicate with each other, and they do this by utilizing device networking. -With networking machines and devices can send arbitrary data between each other. -There are multiple networks that get used, such as the wireless and wired network. -Each network device has a frequency it receives on. PDAs for example, use the frequency: [color=green]220.2[/color] + # Networking + Some devices on the station need to communicate with each other, and they do this by utilizing device networking. + With networking, machines and devices can send arbitrary data between each other. + There are multiple networks that get used, such as the wireless and wired network. + Each network device has a frequency it receives on. PDAs for example, use the frequency: [color=green]220.2[/color] -## Device Lists -Some devices need to know what other devices to communicate with specifically. - - - -Air alarms for example require you to tell it which vents, scrubbers, sensors, and firelocks to interact with. -You do that by using the Network Configurator. + Note: The following operations will require use of the Network Configurator to be performed: -## Linking -If devices basic or still more advanced but need finer control of how and what connects to each other they will generally use device linking. + ## Device Lists + Some devices need to know which other devices to communicate with specifically. - - + -With linking you can connect the outputs of a device like [color=gray]On[/color] or [color=gray]Off[/color] with the inputs of a device like the airlocks -[color=gray]Open[/color] or [color=gray]Close[/color] inputs. -The Network Configurator is also used for linking devices together. + Air alarms, for example, require you to tell it which vents, scrubbers, sensors, and firelocks to interact with. + + ## Linking + If a device, basic or advanced, needs finer controls of how and which devices it connects to, it will generally use device linking. + + + + + With linking, you can connect the outputs of a device, like [color=gray]On[/color] or [color=gray]Off[/color], with the inputs of a device, like the airlocks [color=gray]Open[/color] or [color=gray]Close[/color] inputs. diff --git a/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml b/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml index 2cf1fa44ea..b946bf041c 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/PortableGenerator.xml @@ -1,7 +1,7 @@ - + # Portable Generators - Need power? No engine running? The "P.A.C.M.A.N." line of portable generators has you covered. + Need power? No engines running? The "P.A.C.M.A.N." line of portable generators has you covered. @@ -16,10 +16,10 @@ - The J.R.P.A.C.M.A.N. can be found across the station in maintenance areas, and is ideal for crew to set up themselves whenever there are power issues. + The J.R.P.A.C.M.A.N. can be found across the station in maintenance shafts, and is ideal for crew to set up themselves whenever there are power issues. Setup is incredibly easy: wrench it down above an [color=green]LV[/color] power cable, give it some welding fuel, and start it up. - Welding fuel should be plentiful to find around the station. In a pinch, you can even transfer some from the big tanks with a soda can. Just remember to empty the soda can first, I don't think it likes soda as fuel. + Welding fuel should be plentiful to find around the station. In a pinch, you can even transfer some from the big tanks with a soda can or water bottle. Just remember to empty the soda can first, I don't think it likes soda as fuel. # The Big Ones @@ -33,10 +33,11 @@ - The (S.U.P.E.R.)P.A.C.M.A.N. is intended for usage by engineering for advanced power scenarios. Bootstrapping the engine, powering departments, and so on. + The (S.U.P.E.R.)P.A.C.M.A.N. is intended for usage by engineering for advanced power scenarios. Bootstrapping larger engines, powering departments, and so on. - The S.U.P.E.R.P.A.C.M.A.N. boasts larger power output and longer runtime at maximum output, but scales down to lower outputs less efficiently. + The S.U.P.E.R.P.A.C.M.A.N. boasts a larger power output and longer runtime at maximum output, but scales down to lower outputs less efficiently. - They connect directly to [color=yellow]MV[/color] or [color=orange]HV[/color] power cables, able to switch between them for flexibility. + They connect directly to [color=yellow]MV[/color] or [color=orange]HV[/color] power cables, and are able to switch between them for flexibility. + The S.U.P.E.R.P.A.C.M.A.N and P.A.C.M.A.N require uranium sheets and plasma sheets as fuel, respectively. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Power.xml b/Resources/ServerInfo/Guidebook/Engineering/Power.xml index 62b38e397d..7dd227ee9b 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Power.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Power.xml @@ -1,53 +1,53 @@ -# Power + # Power -SS14 has a fairly in-depth power system through which all devices on the station receive electricity. It's divided into three main powernets; HV, LV, and MV. - - - - - + SS14 has a fairly in-depth power system through which all devices on the station receive electricity. It's divided into three main powernets; High Voltage, Medium Voltage, and Low Voltage. + + + + + -## Cabling -The three major cable types (HV, MV, and LV) can be used to form independent powernets. Examine them for a description of their uses. - - - - - + ## Cabling + The three major cable types (HV, MV, and LV) can be used to form independent powernets. Examine them for a description of their uses. + + + + + -## Power storage -Each power storage device presented functions as the transformer for its respective power level (HV, MV, and LV) and also provides a fairly sizable backup battery to help flatten out spikes and dips in power usage. - - - - - + ## Power storage + Each power storage device presented functions as the transformer for its respective power level (HV, MV, and LV), and also provides a fairly sizable backup battery to help flatten out spikes and dips in power usage. + + + + + -## Ramping -Contrary to what one might expect from a video game electrical simulation, power is not instantly provided upon request. Generators and batteries take time to ramp up to match the draw imposed on them, which leads to brownouts when there are large changes in current draw all at once, for example when batteries run out. + ## Ramping + Contrary to what one might expect from a video game electrical simulation, power is not instantly provided upon request. Generators and batteries take time to ramp up to match the draw imposed on them, which leads to brownouts when there are large changes in current draw all at once; for example, when batteries run out. -## Installing power storage -Substations are the most self-explanatory. Simply install the machine on top of an MV and HV cable, it will draw power from the HV cable to provide to MV. + ## Installing power storage + Substations are the most self-explanatory. Simply install the machine on top of an MV and HV cable; it will draw power from the HV cable to provide to MV. -Installing APCs is similarly simple, except APCs are exclusively wallmounted machinery and cannot be installed on the floor. Make sure it has both MV and LV connections. + Installing APCs is similarly simple, except APCs are exclusively wallmounted machinery and cannot be installed on the floor. Make sure it has both MV and LV connections. -Installing a SMES requires you construct a cable terminal to use as the input. The SMES will draw power from the terminal and send power out from underneath. The terminal will ensure that the HV input and HV output do not connect. Avoid connecting a SMES to itself, this will result in a short circuit which can result in power flickering or outages depending on severity. + Installing a SMES requires you construct a cable terminal to use as the input. The SMES will draw power from the terminal and send power out from underneath. The terminal will ensure that the HV input and HV output do not connect. Avoid connecting a SMES to itself; this will result in a short circuit, which can result in power flickering or outages depending on severity. -## APC breaking -Currently the only power storage device that has a limit to its power network is APC. As soon as all connected devices and machinery demand more than [color=#a4885c]24kW[/color] it's breaker will pop and everything will turn off. - - - + ## APC breaking + Currently the only power storage device that has a limit to its power to the network is the APC. As soon as all connected devices and machinery demand more than [color=#a4885c]24kW[/color] of power, its breaker will pop and everything will turn off. In the case that you are not an engineer, call an engineer (or cyborg) to re-enable it, after reducing the load back down to [color=#a4885c]below[/color] 24kW. + + + -## Checking power grid -1. Use the [color=#a4885c]t-ray scanner[/color] in order to locate cables that are hidden under tiles. (skip this step if cables aren't hidden) -2. Pry open the tile that is blocking your access to the cable with a [color=#a4885c]crowbar[/color]. (skip this step if cables aren't hidden) -3. Equip your trusty [color=#a4885c]Multitool[/color] and click on any cable to see powergrid stats. - - - - - + ## Checking the power grid + 1. Use the [color=#a4885c]t-ray scanner[/color] in order to locate cables that are hidden under tiles. (skip this step if cables aren't hidden) + 2. Pry open the tile that is blocking your access to the cable with a [color=#a4885c]crowbar[/color]. (skip this step if cables aren't hidden) + 3. Equip your trusty [color=#a4885c]Multitool[/color] and click on any cable to see the power-grid stats. + + + + + diff --git a/Resources/ServerInfo/Guidebook/Engineering/RTG.xml b/Resources/ServerInfo/Guidebook/Engineering/RTG.xml index 1d71ee9144..6149b58049 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/RTG.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/RTG.xml @@ -1,17 +1,20 @@ -# Radioisotope Thermoelectric Generator (RTG) + # Radioisotope Thermoelectric Generator (RTG) - - - - + + + -Making power using a Radioisotope Thermoelectric Generator (RTG) is similar to making power using solar. -RTGs only provide 10 kW of power, but they provide it for free and for the entire round. -Basically, if you connect an RTG to your power grid, it'll give you free power. + Making power using a Radioisotope Thermoelectric Generator (RTG) is similar to making power using solars. + RTGs only provide [color=#a4885c]10kW[/color] of power, but they provide it for free and for the entire round. + Basically, if you connect an RTG to your power grid, it'll give you [color=#a4885c]free power[/color]. + However, they're only accessible through salvage finding one on an expedition. Should they bring some in, make sure to thank them! -Sometimes, RTGs are damaged. -Damaged RTGs behave just like regular ones, but they're radioactive. -That means they're more dangerous, but on the bright side, you can put radiation collectors next to them to turn that radiation into more power. - - \ No newline at end of file + + + + Sometimes, RTGs appear damaged. + Damaged RTGs behave just like regular ones, but they're [color=yellow]radioactive[/color]. + That means they're more dangerous, but on the bright side, you can put radiation collectors next to them to turn that radiation into more power. + This is usually more worthwhile, considering the power is still free, so long as you can find a safe spot to put the RTG(s) in. + diff --git a/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml b/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml index 7e743ddd68..21956d600c 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Shuttlecraft.xml @@ -1,40 +1,39 @@ -# Shuttle-craft + # Shuttle-craft -Shuttle construction is simple and easy, albeit rather expensive and hard to pull off within an hour. It's a good activity if you have a significant amount of spare time on your hands and want a bit of a challenge. + Shuttle construction is simple and easy, albeit rather expensive and hard to pull off within an hour. It's a good activity if you have a significant amount of spare time on your hands and want a bit of a challenge. -## Getting started -Required parts: - - - - - - - - - - - - - + ## Getting started + Required parts: + + + + + + + + + + + + + -Optional parts: - - - - - - - - - - + Optional parts: + + + + + + + + + + -Head out into space with steel sheets and metal rods in hand, and once you're three or more meters away from the station, click near or under you with the rods in hand. This will place some lattice, which can then be turned into plating with the steel sheets. Expand your lattice platform by clicking just off the edge with rods in hand. + Head out into space with steel sheets and metal rods in hand, and once you're three or more tiles away from the station, click near or under you with the rods in hand. This will place some lattice, which can then be turned into plating with steel sheets or floor tiles. Expand your lattice platform by clicking just off the edge with some rods in hand. -From there, once you have the shape you want, bring out and install thrusters at the edges. They must be pointing outward into space to function and will not fire if there's a tile in the way of the nozzle. Install a gyroscope where convenient, and use your substation and generator to set up power. Construct a wall on top of an MV cable and then install an APC on that to power the devices onboard. - -Finally, install the shuttle computer wherever is convenient and ensure all your thrusters and gyroscopes are receiving power. If they are, congratulations, you should have a functional shuttle! Making it livable and good looking is left as an exercise to the reader. + From there, once you have the shape you want, bring out and install thrusters at the edges. They must be pointing outward into space to function and will not fire if there's a tile in the way of the nozzle. Install a gyroscope where convenient, and use your substation and generator to set up power. Construct a wall on top of an MV cable and then install an APC on that to power the devices onboard. + Finally, install the shuttle computer wherever is convenient and ensure all your thrusters and gyroscopes are receiving power (remember to wire the MV and LV networks!). If they are; congratulations, you should have a functional shuttle! Making it livable and good looking is left as an exercise to the reader. diff --git a/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml b/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml index 3553d43e6f..3c0dd665e2 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/Singularity.xml @@ -1,109 +1,141 @@ -# Gravitational Singularity Engine + # Singularity / Tesla Engine -The Gravitational Singularity Engine can yield infinite power, with no fueling required. It can also destroy the whole station with equal ease. It uses a Particle Accelerator to fire high energy particles at a Singularity Generator to form a singularity. The singularity then pulses radiation which is absorbed by Radiation Collectors. + The Singularity Engine / Tesla Engine can yield [color=#a4885c]infinite power[/color], with no fueling required. It can also [color=red]destroy the whole station[/color] with equal ease. It uses a Particle Accelerator to fire high energy particles at a Singularity Generator to form a singularity or ball lightning. + The singularity then pulses radiation which is absorbed by Radiation Collectors, or the ball lightning then zaps nearby tesla coils and grounding rods to provide power. -## Setting it up + # Setting it up -The Gravitational Singularity Engine requires 4 subsystems to work properly: + Both engines requires 4 subsystems to work properly; two are shared between both engines: -## Gravitational singularity generator - - - -The generator should be anchored at the center of the containment area since this is where the singularity will appear at. + ## Containment field generators and Emitters + + + + + + The emitters connect to MV cables and fire lasers as long as they have power and are turned on. + Fire the emitters at enabled containment field generators to activate them. + If two containment field generators are active, in range and are in the same cardinal axis, a containment field will appear. + The containment field will repel the singularity or tesla, keeping it from escaping, and yield a little bit of power every time anything bounces off of them. -## Containment field generators and emitters - - - - - -The emitters connect to MV cables and fire lasers as long as they have power and are turned on. -Fire emitters at containment field generators to activate them. -If two containment field generators are active, in range and in the same cardinal axis, a containment field will appear. -The containment field will repell the singularity, keeping it from escaping, and yield a little bit of power every time anything bounces off of them. -Emitter lasers and containment field can cause damage, avoid touching them when active. + The emitter lasers and the containment fields can also cause damage and/or cause you to be sent flying into deep space; [color=#a4885c]avoid touching them[/color] when active. + It is recommended to [color=#a4885c]lock the emitters[/color] with [keybind="AltActivateItemInWorld"/], to prevent any break-in no-gooders from loosing the singularity or tesla by simply switching off the field. -## Radiation collectors - - - - -They connect to HV cables and generate power from nearby radiation sources when turned on. -Radiation collectors require a tank full of gaseous plasma in order to operate. -Continous radiation exposure will gradually consume the stored plasma, so replace depleted tanks with fresh ones to maintain a high power output. + Teslas can have significantly smaller containment fields than singularity containment fields; adjusting field size is recommended, as the tesla becomes easier to keep watch on in a simply 3x3 field setup. -## Particle accelerator + ## Particle accelerator - - - + + + - - - - - + + + + + - - - + + + - - - - - + + + + + -The Particle Accelerator (PA) is a multi-tile structure that launches accelerated particles from its emitters. Its emitters should always face the gravitational singularity generator. -Some stations already have an unfinished PA. To complete, first ensure there is MV cable beneath the PA power box, anchor all parts, then add LV cable to each part. - - - -Then use a screwdriver to screw back the panels. -Scan parts using the PA control computer to check if it's operational. If it shows up as incomplete, examine for what's missing. - - - + The Particle Accelerator (PA) is a multi-tile structure that launches accelerated particles from its emitters. Its emitters should always face the generator. + Some stations already have an unfinished PA. To complete it, first ensure there is a MV cable beneath the PA power box, anchor all the parts, and then add an LV cable to each part. + + + + Then use a screwdriver to screw back the panels. + [color=#a4885c]Scan parts[/color] using the PA control computer to check if it's operational (the PA will not function if you do not scan it!). If it shows up as incomplete, examine what's missing. + + + -## Turing on the Gravitational Singularity Engine + The other two subsystems are unique to each other: -[color=#a4885c]Do not[/color] turn the PA on unless all other subsystems are working properly. + ## Gravitational singularity generator or Ball lightning generator + + + + + The generator should be anchored at the center of the containment area, since this is where the singularity/tesla should appear at. -Turn power on using the PA control computer. Set strength to an appropiate level. Currently the only appropriate level is [color=#f0684d]1[/color], anything above that will ensure that singularity grows too strong to handle. -The higher the output stength is set on PA control computer, the bigger the singularity will be. + ## Radiation collectors or Tesla coils + + + + + The radition collectors connect to HV cables and generate power from nearby radiation sources when turned on. + Radiation collectors require a tank full of gaseous plasma in order to operate. + Continous radiation exposure will gradually convert the stored plasma into tritium, so replace depleted plasma tanks with fresh ones regularly to maintain a high power output. -The PA will now draw power from the power net and start firing particles at the Gravitational singularity generator. + + + + + The tesla coils connect to HV cables and provide a stream of power after being zapped by the ball lightning. + However, tesla coils usually do not fully absorb the lightning strike, and the grounding rods are required to prevent lighting from arcing to and obliterating nearby machines. + Do note that one grounding rod is not a foolproof solution; get [color=#a4885c]atleast 4 rods[/color] around the containment field to make it mathematically unlikely for the tesla to escape. + As the ball lightning zaps tesla coils, they will degrade from wear; make sure to [color=#a4885c]weld them[/color] every now and then to keep generating power. - - - - - + ## Turing on the Engines -A singularity will soon appear at the position of the Gravitational singularity generator. - - - + [color=red]Do not[/color] turn the PA on unless all the other subsystems are working properly and there is enough power to start the engine. -If no particle is hitting the singularity generator, the singularity will start to slowly decay until it disappear. + Turn power on using the PA control computer. Set the strength to an appropiate level. Currently the only appropriate level is [color=#f0684d]1[/color]; anything above that will ensure that singularity grows too strong to handle. + The higher the output stength is set on PA control computer, the bigger the singularity will be. -## Safety -Singularity emits radiation around it, so always keep a distance. Consider getting radiation shielding gear beforehand. Seek medical attention if experiencing health issues. + Currently, the output power does not affect the ball lightning, beyond giving the ball lightning extra orbs around it. - - - + The PA will now draw power from the power net and start firing particles at the generators. - - - - - + + + + + -A singularity might move around, but the containment field will repel it. -If a singularity escapes its containment field, often referred to as a "singuloose," it will attract and then consume everything in its way. + A singularity or ball lightning will soon appear at the position of the Gravitational singularity generator. + + + or + + -In such circumstances, there is little to be done other than running in the opposite direction. + If no particles are hitting the singularity, the singularity will start to slowly decay until it disappears. + This is not the case for the tesla; feel free to disconnect the PA after the tesla has been set up. + + ## Safety + The singularity emits a large amount of radiation around it, so always keep a distance from it. Consider getting [color=yellow]radiation shielding gear[/color] beforehand. Seek medical attention if you are experiencing health issues. + + + + + + + + + + + + The singularity might move around, but the containment field will repel it. + + The tesla creates large bolts of lightning around it, so make sure to wear insuls before approaching it. If you aren't, and it zaps you, pray that the ball lightning doesn't stunlock you and eventually send you into crit. + + + + If a singularity or tesla escapes its containment field, often referred to as a "singuloose" or "tesloose" respectively, it will attract and then consume everything in its way, growing larger as it does so, or it will begin to obliterate every machine in its path, and shock all crew personnel. + + In such circumstances, there is little to be done other than running in the opposite direction. + + + + However, if science has happened to research [color=#D381C9]Portable Particle Decelerators[/color], or if cargo can order them in time, you may be able to stop the singularity from eating the whole station. + Good luck on the tesla, though; it is merely too powerful to recontain after breaching. diff --git a/Resources/ServerInfo/Guidebook/Engineering/TEG.xml b/Resources/ServerInfo/Guidebook/Engineering/TEG.xml index 9e8697a9e1..7739181f94 100644 --- a/Resources/ServerInfo/Guidebook/Engineering/TEG.xml +++ b/Resources/ServerInfo/Guidebook/Engineering/TEG.xml @@ -19,17 +19,17 @@ Note that the circulators are [color=#a4885c]directional[/color]: they will only let gas through one way. You can see this direction in-game by examining the circulator itself. A pressure difference is required across the input and output, so pumps are generally provided and must be turned on. - There is no preference for which side must be hot or cold, there need only be a difference in temperature between them. The gases in the two "loops" are never mixed, only energy is exchanged between them. The hot side will cool down, the cold side will heat up. + There is no preference for which side must be hot or cold, there need only be a difference in temperature between them. The gases in the two "loops" are never mixed, [color=#a4885c]only energy is exchanged between them[/color]. The hot side will cool down, the cold side will heat up. ## The Pipes - There are 2 major pipenets to worry about here: The Hot Loop (where gas will be burnt for heat), and The Cold Loop (where circulated, heated waste gas will either be removed into space or cooled back down). Make sure that [bold]both pipenets do NOT mix[/bold], as only heat should be transferred between the two through the TEG. + There are 2 major pipenets to worry about here: [color=red]The Hot Loop[/color] (where gas will be burnt for heat), and [color=cyan]The Cold Loop[/color] (where circulated, heated waste gas will either be removed into space or cooled back down). Make sure that [color=#a4885c][bold]both pipenets do NOT mix[/bold][/color], as only heat should be transferred between the two through the TEG. # The Hot Loop As I'm sure a wise person once said: the best way to make something hot is to light it on fire. Well, depending on context, that may not be very wise, but luckily your engineering department has just what's needed to do it wisely after all. - As stated above, there are many different layouts one can follow to heat up (or cool down) gases; this part of the guide will cover 2 common methods one will often see for the hot loop when the TEG is setup: The Pipe Burn, and the Burn chamber. + As stated above, there are many different layouts one can follow to heat up (or cool down) gases; this part of the guide will cover 2 common methods one will often see for the hot loop when the TEG is setup: [color=#a4885c]The Pipe Burn[/color], and [color=red]the Burn Chamber[/color]. Side note: Plasma fires burn relatively cool compared to, for example, Tritium fires. It may be viable to extract Tritium from an extraction setup (using a 97/3 ratio of O2/Plasma) and react it with Oxygen to get truly hellish temperatures for power. Although, this is just a recommendation; I'm not ya mum. @@ -37,7 +37,7 @@ Also known as the naive method, this is generally discouraged when working for efficiency. However, if all you need is a smidge of power to run the station, and you don't feel like setting up the burn chamber, this method will do. - TODO: Remove this section when atmos pipes are updated to have pressure/temperature limits in a future atmos refactor. + [color=#444444]TODO: Remove this section when atmos pipes are updated to have pressure/temperature limits in a future atmos refactor.[/color] Most (if not all) pipe burns follow this general layout: @@ -55,8 +55,8 @@ - The Gas input is pretty self-explanatory; this is where you will input the O2-Plasma mix to be burnt. A 2:1 (67/33) ratio of Oxygen to Plasma is recommended for the hottest burn. - The High-pressure pump serves 2 purposes; first, it prevents the burn from backwashing into the supply pipe, which would be.. bad, for many reasons. Second, it maintains a positive pressure in the following pipe segment, which is important to allow the burn to continue, especially since hot gases expand. - - The Pipe segment is where the burn actually occurs; to start it off, one can use a heater to increase the temperature up to the ignition temperature of Plasma. Afterwards, the reaction should be self-sustaining, so long as the Pressure and Moles supplied remains high enough. Be warned; if you wish to remove the heater, it will carry some of this superheated gas with it, transferring it to the next pipenet you connect it to. Best to space the gas through a space vent, if you must. - - The Low-pressure pump (whose pressure should be [italics]slightly lower[/italics] than the input pump) prevents [italics]all[/italics] the gas from passing through the circulator, which could result in the loss of the Moles required to sustain a burn. + - The Pipe segment is where the burn actually occurs; to start it off, one can use a heater to increase the temperature up to the ignition temperature of Plasma. Afterwards, the reaction should be self-sustaining, so long as the Pressure and Moles supplied remains high enough. [color=#a4885c]Be warned[/color]; if you wish to remove the heater, it will carry some of this superheated gas with it, transferring it to the next pipenet you connect it to. Best to space the gas through a space vent, if you must. + - The Low-pressure pump (whose pressure should be [italic]slightly lower[/italic] than the input pump) prevents [italic]all[/italic] the gas from passing through the circulator, which could result in the loss of the Moles required to sustain a burn. - The Circulator is where this generated heat will flow to the cold loop; afterwards, feel free to space the waste gases. Note: Pressure pumps are used here as, while they pump on pressure (not flow-rate, which is comparatively faster), they are a bit easier to control when it comes to the limited Plasma supply on-station. However, the steps shown can be followed with volumetric pumps too. @@ -68,25 +68,53 @@ Most (if not all) stations have the burn chamber separated from the main atmospherics block by a 1-wide spaced grid, presumably to prevent conduction. The chambers consist of 3(+1) important parts: - The Air Injector/Passive Vent - The Space Vent - - The Radiator Loop + - The Scrubber Array + + Here is one layer of an example setup: (pipes can and do need to be layered under the scrubbers below to connect them!) + + + + + + + + + + + + + + + + + + + + + + + + + + + + - Most normal burn chambers don't come with Heat-Exchangers; instead, they have air scrubbers (and optionally, an air alarm) to help filter for Tritium, which is a highly reactive, hot-burning gas that can also be used to heat the TEG efficiently. - However, this is a slightly more advanced setup than just burning plasma, as it needs 2 burn chambers instead of 1 (one for tritium production, one for burning said tritium), so remove the scrubbers and retrofit the burn chamber with a parallel array of heat-exchangers instead. - The air injector (or Passive Vent) injects air (or allows air to flow) into the burn chamber. Either should be supplemented with a pump before it, to keep pressures high. - There is a notable difference between the passive vent and the air injector; the air injector can only keep injecting air up to 9MPa, which can be reached very easily with a good burn. Ideally, switch out the air injector for a passive vent connected to a volume pump. + There is a notable difference between the passive vent and the air injector; the air injector can only keep injecting air up to [color=#a4885c]9MPa[/color], which can be reached very easily with a good burn. Ideally, switch out the air injector for a passive vent connected to a volume pump. - The space vent (designated as a blast door to space on one side of the burn chamber) allows waste gases to be expelled and destroyed. Open this to keep the pressure under control. + The space vent (designated as a blast door to space on one side of the burn chamber) allows waste gases to be expelled and destroyed. Open this every now and then to keep the pressure under control, or to space excess input gas. - The radiator loop collects heat from the burnt gases and brings it to the TEG. To maximize efficiency, hook up the heat-exchangers [italics]in parallel to each other[/italics], with a pressure pump at max pressure after the array and a volumetric pump before the array. - The pressure of the volumetric pump should be set to ( 200 / number of heat-exchangers ) L/s. For example, having 2 heat-exchangers would mean you should set the pressure to 100 L/s. - Finally, fill the whole loop with (ideally) a high heat capacity gas, like Frezon or Plasma. (Yes, Frezon =/= Cold. Frezon has one of the highest heat capacities in the game; so long as it isn't reacting with Nitrogen, it can actually be heated and can store heat really well!) + The scrubber array filters out all the burnt gasses and sends them through the TEG. Note that using default settings on the scrubbers is a bad idea, as valuable plasma will be filtered out too. + Instead, use a network configurator to connect all the scrubbers to a nearby air alarm, and set the air alarm's scrubber settings to scrub everything except Oxygen and Plasma, and to siphon air aswell. This ensures that as much heat as available can be collected and sent to the TEG. + + Note that these are just two of many ways you can setup the hot loop; [color=#a4885c]feel free to mix and match setups as needed![/color] Volume pumps in replacement of pressure pumps, radiator loops for heat collection, or even a Pyroclastic anomaly to provide said heat! The stars are the limit! # The Cold Loop As with the Hot Loop, the Cold Loop must also be setup in order to operate the TEG. However, the Cold Loop is usually a lot more low-tech than the Hot Loop; in reality, the Cold Loop only has to be "relatively" cooler -- hey, room temperature is technically cooler than the surface of the sun, right? - There are 3 main methods you will see used for the Cold Loop: The Water Cooler (see: Liltenhead's video on the TEG), the Coolant Array and the Freezer Loop. + There are 3 main methods you will see used for the Cold Loop: [color=#a4885c]The Water Cooler[/color] (see: Liltenhead's video on the TEG), [color=cyan]the Coolant Array[/color] and [color=#a4885c]the Freezer Loop[/color]. ## The Water Cooler @@ -103,14 +131,17 @@ - TODO: Remove this section when gas miners are removed in a future atmos refactor. + [color=#444444]TODO: Remove this section when gas miners are removed in a future atmos refactor.[/color] ## Coolant Array - This is the default method for the Cold Loop you will see on a variety of stations. Being of moderate complexity and having no losses of any resource, this [italics]should[/italics] be the main method of cooling down the TEG. However, every station at the moment somehow has their heat exchangers hooked up wrong, reducing efficiency greatly. (Thanks a bunch, NT!) + This is the default method for the Cold Loop you will see on a variety of stations. Being of moderate complexity and having no losses of any resource, this [color=#a4885c]should[/color] be the main method of cooling down the TEG. However, most stations at the moment somehow have their heat exchangers hooked up wrong (or suggest incorrect piping), reducing efficiency greatly. [color=#444444](Thanks a bunch, NT!)[/color] - To use heat-exchangers properly, they must be setup in [italics]parallel[/italics], not in series (like what you see on most stations). A gas pump at max pressure should be placed after, and a volumetric pump before the heat-exchangers. - The flow-rate of the volumetric pump should be set to ( 200 / number of heat-exchangers ) L/s. + To use heat-exchangers properly, they must be setup in [color=#a4885c]parallel[/color], not in series (like what you see on most stations). A gas pump at max pressure should be placed after, and a volumetric pump before the heat-exchangers. + The flow-rate of the volumetric pump should be set using the following formula: + + [color=cyan]( 200 / number of heat-exchangers )[/color] L/s. + Simply speaking, the Coolant Array consists of 3 major parts: An input connector port, a few pumps and the heat-exchanger array out in space. It can be setup like so: @@ -161,7 +192,7 @@ - Connector Port: Use this to input a gas with high heat capacity; most of the time, Plasma or Frezon is used to do so, as they both have very high specific heat capacities (although most any gas will do). (Yes, Plasma =/= Hot. You can cool it down, and it acts as a really good heat exchange medium.) - Input/Output Pumps: Used to make sure gas keeps flowing through both the Circulator and the Heat-Exchanger array. As the gas cools down and heats up (and as it flows through the Exchanger), pressure must be applied for it to keep flowing. - - Heat-Exchanger: Basically, just a bunch of heat-exchanger pipes in space. Not much to say, besides the fact that it cools down the gas inside it. Make sure the heat-exchangers are placed on lattice, not plating! Otherwise, the heat-exchange efficiency will be greatly reduced, as the heat-exchangers aren't directly exposed to space below them. + - Heat-Exchanger: Basically, just a bunch of heat-exchanger pipes in space. Not much to say, besides the fact that it cools down the gas inside it. Make sure the heat-exchangers are [color=#a4885c]placed on lattice, not plating[/color]! Otherwise, the heat-exchange efficiency will be greatly reduced, as the heat-exchangers aren't directly exposed to space below them. ## The Freezer Loop diff --git a/Resources/ServerInfo/Guidebook/Mobs/SlimePerson.xml b/Resources/ServerInfo/Guidebook/Mobs/SlimePerson.xml index fc72c60dbf..0374d4cb95 100644 --- a/Resources/ServerInfo/Guidebook/Mobs/SlimePerson.xml +++ b/Resources/ServerInfo/Guidebook/Mobs/SlimePerson.xml @@ -10,7 +10,21 @@ They exhale nitrous oxide and are unaffected by it. Their body can process 6 reagents at the same time instead of just 2. - Their Slime "blood" can not be regenerated from Iron. Slime Blood is technically a source of + Slimepeople can morph into a [bold]"geras"[/bold] (an archaic slimefolk term), which is a smaller slime form that can [bold]pass through grilles[/bold], + but forces them to drop their inventory and held items. It's handy for a quick getaway. A geras is small enough to pick up (with two hands) + and fits in a duffelbag. + + + + + + Slimepeople have an [bold]internal 2x2 storage inventory[/bold] inside of their slime membrane. Anyone can see what's inside and take it out of you without asking, + so be careful. They [bold]don't drop their internal storage when they morph into a geras, however![/bold] + + Slimepeople have slight accelerated regeneration compared to other humanoids. They're also capable of hardening their fists, and as such have stronger punches, + although they punch a little slower. + + Their slime "blood" can not be regenerated from Iron. Slime Blood is technically a source of moderately filling food for other species, although drinking the blood of your coworkers is usually frowned upon. They suffocate 80% slower, but take pressure damage 9% faster. This makes them by far the species most capable to survive in hard vacuum. For a while. diff --git a/Resources/Textures/Objects/Devices/flatpack.rsi/emitter.png b/Resources/Textures/Objects/Devices/flatpack.rsi/emitter.png new file mode 100644 index 0000000000..c663886f9c Binary files /dev/null and b/Resources/Textures/Objects/Devices/flatpack.rsi/emitter.png differ diff --git a/Resources/Textures/Objects/Devices/flatpack.rsi/meta.json b/Resources/Textures/Objects/Devices/flatpack.rsi/meta.json index 2d1ca37141..8f46a0ca53 100644 --- a/Resources/Textures/Objects/Devices/flatpack.rsi/meta.json +++ b/Resources/Textures/Objects/Devices/flatpack.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC0-1.0", - "copyright": "Created by EmoGarbage404 (github) for SS14, solar-assembly-part taken from tgstation and modified at https://tgstation13.org/wiki/Guide_to_construction#Solar_Panels_and_Trackers, ame-part taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1b7952787c06c21ef1623e494dcfe7cb1f46e041; singularity-generator, tesla-generator, radiation-collector, containment-field-generator, tesla-coil, grounding-rod inner icons made by lzk228", + "copyright": "Created by EmoGarbage404 (github) for SS14, solar-assembly-part taken from tgstation and modified at https://tgstation13.org/wiki/Guide_to_construction#Solar_Panels_and_Trackers, ame-part taken from vgstation at https://github.com/vgstation-coders/vgstation13/commit/1b7952787c06c21ef1623e494dcfe7cb1f46e041; singularity-generator, tesla-generator, radiation-collector, containment-field-generator, tesla-coil, grounding-rod inner icons made by lzk228; emitter made by pigeonpeas", "size": { "x": 32, "y": 32 @@ -39,6 +39,9 @@ }, { "name": "containment-field-generator" + }, + { + "name": "emitter" } ] } diff --git a/Resources/Textures/Objects/Devices/jammer.rsi/jammer.png b/Resources/Textures/Objects/Devices/jammer.rsi/jammer.png index 6de27ba924..e1db2d05b6 100644 Binary files a/Resources/Textures/Objects/Devices/jammer.rsi/jammer.png and b/Resources/Textures/Objects/Devices/jammer.rsi/jammer.png differ diff --git a/Resources/Textures/Objects/Devices/jammer.rsi/jammer_high_charge.png b/Resources/Textures/Objects/Devices/jammer.rsi/jammer_high_charge.png new file mode 100644 index 0000000000..e288427e71 Binary files /dev/null and b/Resources/Textures/Objects/Devices/jammer.rsi/jammer_high_charge.png differ diff --git a/Resources/Textures/Objects/Devices/jammer.rsi/jammer_low_charge.png b/Resources/Textures/Objects/Devices/jammer.rsi/jammer_low_charge.png new file mode 100644 index 0000000000..0950a95df7 Binary files /dev/null and b/Resources/Textures/Objects/Devices/jammer.rsi/jammer_low_charge.png differ diff --git a/Resources/Textures/Objects/Devices/jammer.rsi/jammer_medium_charge.png b/Resources/Textures/Objects/Devices/jammer.rsi/jammer_medium_charge.png new file mode 100644 index 0000000000..7c12da8606 Binary files /dev/null and b/Resources/Textures/Objects/Devices/jammer.rsi/jammer_medium_charge.png differ diff --git a/Resources/Textures/Objects/Devices/jammer.rsi/meta.json b/Resources/Textures/Objects/Devices/jammer.rsi/meta.json index 2923d9ac63..d837374a87 100644 --- a/Resources/Textures/Objects/Devices/jammer.rsi/meta.json +++ b/Resources/Textures/Objects/Devices/jammer.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from https://github.com/tgstation/tgstation/commit/c65da5a49477413310c81c460ea4b243a9f864dd", + "copyright": "Taken from https://github.com/tgstation/tgstation/commit/c65da5a49477413310c81c460ea4b243a9f864dd with minor edits.", "size": { "x": 32, "y": 32 @@ -10,6 +10,24 @@ { "name": "jammer", "directions": 1 + }, + { + "name": "jammer_high_charge", + "directions": 1 + }, + { + "name": "jammer_medium_charge", + "directions": 1 + }, + { + "name": "jammer_low_charge", + "directions": 1, + "delays": [ + [ + 0.3, + 0.3 + ] + ] } ] -} \ No newline at end of file +} diff --git a/RobustToolbox b/RobustToolbox index 73da147b88..eb63809999 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit 73da147b8811c8d032e0f119ef507a1c11ff4073 +Subproject commit eb638099999dce3a43d90772ca976ae010d649c0