Merge remote-tracking branch 'space-station-14/master' into ed-26-04-2024-upstream

# Conflicts:
#	Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs
This commit is contained in:
Ed
2024-04-26 00:33:04 +03:00
254 changed files with 4595 additions and 3363 deletions

View File

@@ -0,0 +1,8 @@
using Content.Shared.Radio.EntitySystems;
namespace Content.Client.DeviceNetwork;
public sealed class JammerSystem : SharedJammerSystem
{
}

View File

@@ -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;
/// <summary>
/// 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<SpriteSystem>().Frame0(sprite);
_shader = protoManager.Index<ShaderPrototype>("unshaded").Instance();
_unshadedShader = protoManager.Index<ShaderPrototype>("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

View File

@@ -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<GhostSystem>();
if (args.Length != 0 && bool.TryParse(args[0], out var visibility))
{
ghostSystem.ToggleGhostVisibility(visibility);
}
else
{
ghostSystem.ToggleGhostVisibility();
}
}
}

View File

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

View File

@@ -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.
/// </remarks>
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;

View File

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

View File

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

View File

@@ -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<RichTextLabel>()
.Class(StyleClassItemStatus)
.Prop(nameof(RichTextLabel.LineHeightScale), 0.7f)

View File

@@ -0,0 +1,55 @@
using System.Numerics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// Pretends to child controls that there's infinite space.
/// This can be used to make something like a <see cref="RichTextLabel"/> clip instead of wrapping.
/// </summary>
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;
}
}

View File

@@ -200,7 +200,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
var coords = args.Coordinates;
if (!_actionsSystem.ValidateWorldTarget(user, coords, action))
if (!_actionsSystem.ValidateWorldTarget(user, coords, (actionId, action)))
{
// Invalid target.
if (action.DeselectOnMiss)
@@ -235,7 +235,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
var entity = args.EntityUid;
if (!_actionsSystem.ValidateEntityTarget(user, entity, action))
if (!_actionsSystem.ValidateEntityTarget(user, entity, (actionId, action)))
{
if (action.DeselectOnMiss)
StopTargeting();

View File

@@ -5,8 +5,11 @@ namespace Content.Client.UserInterface.Systems.Hands.Controls;
public sealed class HandButton : SlotControl
{
public HandLocation HandLocation { get; }
public HandButton(string handName, HandLocation handLocation)
{
HandLocation = handLocation;
Name = "hand_" + handName;
SlotName = handName;
SetBackground(handLocation);

View File

@@ -256,7 +256,8 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_player.LocalSession?.AttachedEntity is { } playerEntity &&
_handsSystem.TryGetHand(playerEntity, handName, out var hand, _playerHandsComponent))
{
if (hand.Location == HandLocation.Left)
var foldedLocation = hand.Location.GetUILocation();
if (foldedLocation == HandUILocation.Left)
{
_statusHandLeft = handControl;
HandsGui.UpdatePanelEntityLeft(hand.HeldEntity);
@@ -268,7 +269,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
HandsGui.UpdatePanelEntityRight(hand.HeldEntity);
}
HandsGui.SetHighlightHand(hand.Location);
HandsGui.SetHighlightHand(foldedLocation);
}
}
@@ -299,11 +300,13 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
// If we don't have a status for this hand type yet, set it.
// This means we have status filled by default in most scenarios,
// otherwise the user'd need to switch hands to "activate" the hands the first time.
if (location == HandLocation.Left)
if (location.GetUILocation() == HandUILocation.Left)
_statusHandLeft ??= button;
else
_statusHandRight ??= button;
UpdateVisibleStatusPanels();
return button;
}
@@ -369,9 +372,30 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
_handLookup.Remove(handName);
handButton.Dispose();
UpdateVisibleStatusPanels();
return true;
}
private void UpdateVisibleStatusPanels()
{
var leftVisible = false;
var rightVisible = false;
foreach (var hand in _handLookup.Values)
{
if (hand.HandLocation.GetUILocation() == HandUILocation.Left)
{
leftVisible = true;
}
else
{
rightVisible = true;
}
}
HandsGui?.UpdateStatusVisibility(leftVisible, rightVisible);
}
public string RegisterHandContainer(HandsContainer handContainer)
{
var name = "HandContainer_" + _backupSuffix;

View File

@@ -32,7 +32,7 @@
Name="StatusPanelRight"
HorizontalAlignment="Center" Margin="0 0 -2 2"
SetWidth="125"
MaxHeight="60"/>
SetHeight="60"/>
<hands:HandsContainer
Name="HandContainer"
Access="Public"
@@ -43,7 +43,7 @@
Name="StatusPanelLeft"
HorizontalAlignment="Center" Margin="-2 0 0 2"
SetWidth="125"
MaxHeight="60"/>
SetHeight="60"/>
<inventory:ItemSlotButtonContainer
Name="MainHotbar"
SlotGroup="MainHotbar"

View File

@@ -11,8 +11,8 @@ public sealed partial class HotbarGui : UIWidget
public HotbarGui()
{
RobustXamlLoader.Load(this);
StatusPanelRight.SetSide(HandLocation.Right);
StatusPanelLeft.SetSide(HandLocation.Left);
StatusPanelRight.SetSide(HandUILocation.Right);
StatusPanelLeft.SetSide(HandUILocation.Left);
var hotbarController = UserInterfaceManager.GetUIController<HotbarUIController>();
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;
}
}

View File

@@ -4,25 +4,26 @@
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
VerticalAlignment="Bottom"
HorizontalAlignment="Center">
<Control Name="VisWrapper" Visible="False">
<PanelContainer Name="Panel">
<PanelContainer.PanelOverride>
<graphics:StyleBoxTexture
PatchMarginBottom="4"
PatchMarginTop="6"
TextureScale="2 2"
Mode="Tile"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<PanelContainer Name="HighlightPanel">
<PanelContainer.PanelOverride>
<graphics:StyleBoxTexture PatchMarginBottom="4" PatchMarginTop="6" TextureScale="2 2">
</graphics:StyleBoxTexture>
</PanelContainer.PanelOverride>
</PanelContainer>
<BoxContainer Name="Contents" Orientation="Vertical" Margin="0 6 0 4">
<BoxContainer Name="StatusContents" Orientation="Vertical" />
<Label Name="ItemNameLabel" ClipText="True" StyleClasses="ItemStatus" Align="Left" />
</BoxContainer>
</Control>
<PanelContainer Name="Panel">
<PanelContainer.PanelOverride>
<graphics:StyleBoxTexture
PatchMarginBottom="4"
PatchMarginTop="6"
TextureScale="2 2"
Mode="Tile"/>
</PanelContainer.PanelOverride>
</PanelContainer>
<PanelContainer Name="HighlightPanel">
<PanelContainer.PanelOverride>
<graphics:StyleBoxTexture PatchMarginBottom="4" PatchMarginTop="6" TextureScale="2 2">
</graphics:StyleBoxTexture>
</PanelContainer.PanelOverride>
</PanelContainer>
<BoxContainer Name="Contents" Orientation="Vertical" Margin="0 6 0 4" RectClipContent="True">
<BoxContainer Name="StatusContents" Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Bottom" />
<Control>
<Label Name="NoItemLabel" ClipText="True" StyleClasses="ItemStatusNotHeld" Align="Left" Text="{Loc 'item-status-not-held'}" />
<Label Name="ItemNameLabel" ClipText="True" StyleClasses="ItemStatus" Align="Left" Visible="False" />
</Control>
</BoxContainer>
</controls:ItemStatusPanel>

View File

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

View File

@@ -6,40 +6,10 @@ using Robust.Client.UserInterface;
namespace Content.Client.Weapons.Ranged.ItemStatus;
/// <summary>
/// Renders one or more rows of bullets for item status.
/// </summary>
/// <remarks>
/// This is a custom control to allow complex responsive layout logic.
/// </remarks>
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");
/// <summary>
/// Try to ensure there's at least this many bullets on one row.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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<IResourceCache>();
_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;
/// <summary>
/// Try to ensure there's at least this many bullets on one row.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public int MinCountPerRow;
}
}
/// <summary>
/// Renders one or more rows of bullets for item status.
/// </summary>
/// <remarks>
/// This is a custom control to allow complex responsive layout logic.
/// </remarks>
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<IResourceCache>();
_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);
}
}

View File

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

View File

@@ -22,11 +22,6 @@
ToolTip="{Loc 'analysis-console-print-tooltip-info'}">
</Button>
<BoxContainer MinHeight="5"></BoxContainer>
<Button Name="ExtractButton"
Text="{Loc 'analysis-console-extract-button'}"
ToolTip="{Loc 'analysis-console-extract-button-info'}">
</Button>
<BoxContainer MinHeight="5"></BoxContainer>
<BoxContainer Orientation="Horizontal">
<Button Name="UpBiasButton"
Text="{Loc 'analysis-console-bias-up'}"
@@ -41,6 +36,11 @@
StyleClasses="OpenLeft">
</Button>
</BoxContainer>
<BoxContainer MinHeight="15"></BoxContainer>
<Button Name="ExtractButton"
Text="{Loc 'analysis-console-extract-button'}"
ToolTip="{Loc 'analysis-console-extract-button-info'}">
</Button>
</BoxContainer>
<BoxContainer Orientation="Vertical">
<Label Name="ProgressLabel"></Label>

View File

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

View File

@@ -1,5 +1,4 @@
#nullable enable
using Robust.Shared.Timing;
namespace Content.IntegrationTests.Pair;
@@ -19,6 +18,22 @@ public sealed partial class TestPair
}
}
/// <summary>
/// Convert a time interval to some number of ticks.
/// </summary>
public int SecondsToTicks(float seconds)
{
return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds);
}
/// <summary>
/// Run the server & client in sync for some amount of time
/// </summary>
public async Task RunSeconds(float seconds)
{
await RunTicksSync(SecondsToTicks(seconds));
}
/// <summary>
/// Runs the server-client pair in sync, but also ensures they are both idle each tick.
/// </summary>
@@ -59,4 +74,4 @@ public sealed partial class TestPair
delta = cTick - sTick;
Assert.That(delta, Is.EqualTo(targetDelta));
}
}
}

View File

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

View File

@@ -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
{
/// <summary>
/// Check that a nuke ops game mode can start without issue. I.e., that the nuke station and such all get loaded.
/// </summary>
[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<MapSystem>();
var ticker = server.System<GameTicker>();
var mindSys = server.System<MindSystem>();
var roleSys = server.System<RoleSystem>();
var invSys = server.System<InventorySystem>();
var factionSys = server.System<NpcFactionSystem>();
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<MapComponent>(), Is.Zero);
Assert.That(entMan.Count<MapGridComponent>(), Is.Zero);
Assert.That(entMan.Count<StationMapComponent>(), Is.Zero);
Assert.That(entMan.Count<StationMemberComponent>(), Is.Zero);
Assert.That(entMan.Count<StationCentcommComponent>(), Is.Zero);
// And no nukie related components
Assert.That(entMan.Count<NukeopsRuleComponent>(), Is.Zero);
Assert.That(entMan.Count<NukeopsRoleComponent>(), Is.Zero);
Assert.That(entMan.Count<NukeOperativeComponent>(), Is.Zero);
Assert.That(entMan.Count<NukeOpsShuttleComponent>(), Is.Zero);
Assert.That(entMan.Count<NukeOperativeSpawnerComponent>(), 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<MapComponent>(), Is.GreaterThan(0));
Assert.That(entMan.Count<MapGridComponent>(), Is.GreaterThan(0));
Assert.That(entMan.Count<StationDataComponent>(), Is.EqualTo(2)); // The main station & nukie station
Assert.That(entMan.Count<StationMemberComponent>(), Is.GreaterThan(3)); // Each station has at least 1 grid, plus some shuttles
Assert.That(entMan.Count<StationCentcommComponent>(), Is.EqualTo(1));
// And we now have nukie related components
Assert.That(entMan.Count<NukeopsRuleComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<NukeopsRoleComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<NukeOperativeComponent>(), Is.EqualTo(1));
Assert.That(entMan.Count<NukeOpsShuttleComponent>(), Is.EqualTo(1));
// The player entity should be the nukie commander
var mind = mindSys.GetMind(player)!.Value;
Assert.That(entMan.HasComponent<NukeOperativeComponent>(player));
Assert.That(roleSys.MindIsAntagonist(mind));
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(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<NukeopsRuleComponent>().Single().Component;
var mapRule = entMan.AllComponents<LoadMapRuleComponent>().Single().Component;
foreach (var grid in mapRule.MapGrids)
{
Assert.That(entMan.EntityExists(grid));
Assert.That(entMan.HasComponent<MapGridComponent>(grid));
Assert.That(entMan.HasComponent<StationMemberComponent>(grid));
}
Assert.That(entMan.EntityExists(rule.TargetStation));
Assert.That(entMan.HasComponent<StationDataComponent>(rule.TargetStation));
var nukieShuttlEnt = entMan.AllComponents<NukeOpsShuttleComponent>().FirstOrDefault().Uid;
Assert.That(entMan.EntityExists(nukieShuttlEnt));
EntityUid? nukieStationEnt = null;
foreach (var grid in mapRule.MapGrids)
{
if (entMan.HasComponent<StationMemberComponent>(grid))
{
nukieStationEnt = grid;
break;
}
}
Assert.That(entMan.EntityExists(nukieStationEnt));
var nukieStation = entMan.GetComponent<StationMemberComponent>(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<StationDataComponent>(rule.TargetStation!.Value);
var targetGrid = targetStation.Grids.First();
var targetMap = entMan.GetComponent<TransformComponent>(targetGrid).MapUid!.Value;
Assert.That(targetMap, Is.Not.EqualTo(nukieMap));
Assert.That(entMan.GetComponent<TransformComponent>(player).MapUid, Is.EqualTo(nukieMap));
Assert.That(entMan.GetComponent<TransformComponent>(nukieStationEnt.Value).MapUid, Is.EqualTo(nukieMap));
Assert.That(entMan.GetComponent<TransformComponent>(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<MetaDataComponent>(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<HandsComponent>(player));
Assert.That(entMan.GetComponent<HandsComponent>(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<RespiratorComponent>(player);
var damage = entMan.GetComponent<DamageableComponent>(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();
}
}

View File

@@ -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<GameRuleComponent>(), Is.Zero);
Assert.That(server.EntMan.Count<ActiveGameRuleComponent>(), Is.Zero);
var entityManager = server.ResolveDependency<IEntityManager>();
var sGameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
var sGameTiming = server.ResolveDependency<IGameTiming>();
@@ -26,6 +30,9 @@ namespace Content.IntegrationTests.Tests.GameRules
sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity);
Assert.That(entityManager.TryGetComponent<MaxTimeRestartRuleComponent>(ruleEntity, out var maxTime));
Assert.That(server.EntMan.Count<GameRuleComponent>(), Is.EqualTo(1));
Assert.That(server.EntMan.Count<ActiveGameRuleComponent>(), 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<GameRuleComponent>(), Is.EqualTo(1));
Assert.That(server.EntMan.Count<ActiveGameRuleComponent>(), Is.EqualTo(1));
await server.WaitAssertion(() =>
{
Assert.That(sGameTicker.RunLevel, Is.EqualTo(GameRunLevel.InRound));

View File

@@ -17,6 +17,7 @@ public sealed class SecretStartsTest
var server = pair.Server;
await server.WaitIdleAsync();
var entMan = server.ResolveDependency<IEntityManager>();
var gameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
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();

View File

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

View File

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

View File

@@ -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;
/// <summary>
/// Verify that the yaml linter successfully validates static fields
/// </summary>
[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<Type, HashSet<string>>();
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<TagPrototype>] public static string Tag = "StaticFieldTestTag";
}
[Reflect(false)] private sealed class StringInvalid
{
[ValidatePrototypeId<TagPrototype>] public static string Tag = string.Empty;
}
[Reflect(false)] private sealed class StringArrayValid
{
[ValidatePrototypeId<TagPrototype>] public static string[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"};
}
[Reflect(false)] private sealed class StringArrayInvalid
{
[ValidatePrototypeId<TagPrototype>] 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<TagPrototype> Tag = "StaticFieldTestTag";
}
[Reflect(false)] private sealed class ProtoIdTestInvalid
{
public static ProtoId<TagPrototype> Tag = string.Empty;
}
[Reflect(false)] private sealed class ProtoIdArrayValid
{
public static ProtoId<TagPrototype>[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"};
}
[Reflect(false)] private sealed class ProtoIdArrayInvalid
{
public static ProtoId<TagPrototype>[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty};
}
[Reflect(false)] private sealed class ProtoIdListValid
{
public static List<ProtoId<TagPrototype>> Tag = new() {"StaticFieldTestTag", "StaticFieldTestTag"};
}
[Reflect(false)] private sealed class ProtoIdListInvalid
{
public static List<ProtoId<TagPrototype>> Tag = new() {string.Empty, "StaticFieldTestTag", string.Empty};
}
[Reflect(false)] private sealed class ProtoIdSetValid
{
public static HashSet<ProtoId<TagPrototype>> Tag = new() {"StaticFieldTestTag", "StaticFieldTestTag"};
}
[Reflect(false)] private sealed class ProtoIdSetInvalid
{
public static HashSet<ProtoId<TagPrototype>> Tag = new() {string.Empty, "StaticFieldTestTag", string.Empty, " "};
}
[Reflect(false)] private sealed class PrivateProtoIdArrayValid
{
private static ProtoId<TagPrototype>[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"};
}
[Reflect(false)] private sealed class PrivateProtoIdArrayInvalid
{
private static ProtoId<TagPrototype>[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty};
}
}

View File

@@ -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
{
/// <summary>
/// Checks that the mapping command creates paused & uninitialized maps.
/// </summary>
[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<MapSystem>();
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();
}
}

View File

@@ -28,10 +28,11 @@ public sealed class EvacShuttleTest
// Dummy ticker tests should not have centcomm
Assert.That(entMan.Count<StationCentcommComponent>(), 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<SharedMapSystem>();
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<MetaDataComponent>(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<EmergencyShuttleSystem>();
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<FTLMapComponent>(), 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();
}
}

View File

@@ -64,7 +64,7 @@ public sealed class ActionOnInteractSystem : EntitySystem
var entOptions = GetValidActions<EntityTargetActionComponent>(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<WorldTargetActionComponent>(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);
}

View File

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

View File

@@ -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<EntityPrototype>]
private const string DefaultTraitorRule = "Traitor";
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultNukeOpRule = "LoneOpsSpawn";
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultRevsRule = "Revolutionary";
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultThiefRule = "Thief";
[ValidatePrototypeId<StartingGearPrototype>]
private const string PirateGearId = "PirateGear";
// All antag verbs have names so invokeverb works.
private void AddAntagVerbs(GetVerbsEvent<Verb> 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<HumanoidAppearanceComponent>(args.Target);
_traitorRule.MakeTraitorAdmin(args.Target, giveUplink: isHuman, giveObjectives: isHuman);
_antag.ForceMakeAntag<TraitorRuleComponent>(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<NukeopsRuleComponent>(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<RevolutionaryRuleComponent>(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<ThiefRuleComponent>(player, DefaultThiefRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-thief"),

View File

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

View File

@@ -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<ICommonSession>[] sessions)
{
private readonly List<List<ICommonSession>> _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);
}

View File

@@ -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
{
/// <summary>
/// Tries to get the next non-filled definition based on the current amount of selected minds and other factors.
/// </summary>
public bool TryGetNextAvailableDefinition(Entity<AntagSelectionComponent> 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;
}
/// <summary>
/// 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.
/// </summary>
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool = null)
{
var count = 0;
foreach (var def in ent.Comp.Definitions)
{
count += GetTargetAntagCount(ent, pool, def);
}
return count;
}
/// <summary>
/// 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.
/// </summary>
public int GetTargetAntagCount(Entity<AntagSelectionComponent> 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);
}
/// <summary>
/// Returns identifiable information for all antagonists to be used in a round end summary.
/// </summary>
/// <returns>
/// A list containing, in order, the antag's mind, the session data, and the original name stored as a string.
/// </returns>
public List<(EntityUid, SessionData, string)> GetAntagIdentifiers(Entity<AntagSelectionComponent?> 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<MindComponent>(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;
}
/// <summary>
/// Returns all the minds of antagonists.
/// </summary>
public List<Entity<MindComponent>> GetAntagMinds(Entity<AntagSelectionComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return new();
var output = new List<Entity<MindComponent>>();
foreach (var (mind, _) in ent.Comp.SelectedMinds)
{
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
continue;
output.Add((mind, mindComp));
}
return output;
}
/// <remarks>
/// Helper specifically for <see cref="ObjectivesTextGetInfoEvent"/>
/// </remarks>
public List<EntityUid> GetAntagMindEntityUids(Entity<AntagSelectionComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return new();
return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList();
}
/// <summary>
/// Returns all the antagonists for this rule who are currently alive
/// </summary>
public IEnumerable<EntityUid> GetAliveAntags(Entity<AntagSelectionComponent?> 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);
}
}
/// <summary>
/// Returns the number of alive antagonists for this rule.
/// </summary>
public int GetAliveAntagCount(Entity<AntagSelectionComponent?> 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;
}
/// <summary>
/// Returns if there are any remaining antagonists alive for this rule.
/// </summary>
public bool AnyAliveAntags(Entity<AntagSelectionComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
return GetAliveAntags(ent).Any();
}
/// <summary>
/// Checks if all the antagonists for this rule are alive.
/// </summary>
public bool AllAntagsAlive(Entity<AntagSelectionComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count;
}
/// <summary>
/// Helper method to send the briefing text and sound to a player entity
/// </summary>
/// <param name="entity">The entity chosen to be antag</param>
/// <param name="briefing">The briefing text to send</param>
/// <param name="briefingColor">The color the briefing should be, null for default</param>
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
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);
}
/// <summary>
/// Helper method to send the briefing text and sound to a list of sessions
/// </summary>
/// <param name="sessions">The sessions that will be sent the briefing</param>
/// <param name="briefing">The briefing text to send</param>
/// <param name="briefingColor">The color the briefing should be, null for default</param>
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
[PublicAPI]
public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
{
foreach (var session in sessions)
{
SendBriefing(session, briefing, briefingColor, briefingSound);
}
}
/// <summary>
/// Helper method to send the briefing text and sound to a session
/// </summary>
/// <param name="session">The player chosen to be an antag</param>
/// <param name="data">The briefing data</param>
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);
}
/// <summary>
/// Helper method to send the briefing text and sound to a session
/// </summary>
/// <param name="session">The player chosen to be an antag</param>
/// <param name="briefing">The briefing text to send</param>
/// <param name="briefingColor">The color the briefing should be, null for default</param>
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
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);
}
}
/// <summary>
/// This technically is a gamerule-ent-less way to make an entity an antag.
/// You should almost never be using this.
/// </summary>
public void ForceMakeAntag<T>(ICommonSession? player, string defaultRule) where T : Component
{
var rule = ForceGetGameRuleEnt<T>(defaultRule);
if (!TryGetNextAvailableDefinition(rule, out var def))
def = rule.Comp.Definitions.Last();
MakeAntag(rule, player, def.Value);
}
/// <summary>
/// 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.
/// </summary>
public Entity<AntagSelectionComponent> ForceGetGameRuleEnt<T>(string id) where T : Component
{
var query = EntityQueryEnumerator<T, AntagSelectionComponent>();
while (query.MoveNext(out var uid, out _, out var comp))
{
return (uid, comp);
}
var ruleEnt = GameTicker.AddGameRule(id);
RemComp<LoadMapRuleComponent>(ruleEnt);
var antag = Comp<AntagSelectionComponent>(ruleEnt);
antag.SelectionsComplete = true; // don't do normal selection.
GameTicker.StartGameRule(ruleEnt);
return (ruleEnt, antag);
}
}

View File

@@ -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<GameRuleComponent>
public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
{
[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
/// <summary>
/// Get all players that are eligible for an antag role
/// </summary>
/// <param name="playerSessions">All sessions from which to select eligible players</param>
/// <param name="antagPrototype">The prototype to get eligible players for</param>
/// <param name="includeAllJobs">Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included</param>
/// <param name="acceptableAntags">Should players already selected as antags be eligible</param>
/// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
/// <param name="customExcludeCondition">A custom condition that each player is tested against, if it returns true the player is excluded from eligibility</param>
/// <returns>List of all player entities that match the requirements</returns>
public List<EntityUid> GetEligiblePlayers(IEnumerable<ICommonSession> playerSessions,
ProtoId<AntagPrototype> antagPrototype,
bool includeAllJobs = false,
AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive,
bool ignorePreferences = false,
bool allowNonHumanoids = false,
Func<EntityUid?, bool>? customExcludeCondition = null)
// arbitrary random number to give late joining some mild interest.
public const float LateJoinRandomChance = 0.5f;
/// <inheritdoc/>
public override void Initialize()
{
var eligiblePlayers = new List<EntityUid>();
base.Initialize();
foreach (var player in playerSessions)
SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
}
private void OnTakeGhostRole(Entity<GhostRoleAntagSpawnerComponent> 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<AntagSelectionComponent>(rule, out var select))
return;
MakeAntag((rule, select), args.Player, def, ignoreSpawner: true);
args.TookRole = true;
_ghostRole.UnregisterGhostRole((ent, Comp<GhostRoleComponent>(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;
}
/// <summary>
/// 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
/// </summary>
/// <param name="playerSessions">All sessions from which to select eligible players</param>
/// <param name="antagPrototype">The prototype to get eligible players for</param>
/// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
/// <returns>List of all player sessions that match the requirements</returns>
public List<ICommonSession> GetEligibleSessions(IEnumerable<ICommonSession> playerSessions, ProtoId<AntagPrototype> antagPrototype, bool ignorePreferences = false)
{
var eligibleSessions = new List<ICommonSession>();
foreach (var session in playerSessions)
{
if (IsSessionEligible(session, antagPrototype, ignorePreferences))
eligibleSessions.Add(session);
}
return eligibleSessions;
}
/// <summary>
/// Test eligibility of the player for a specific antag role
/// </summary>
/// <param name="session">The player session to test</param>
/// <param name="antagPrototype">The prototype to get eligible players for</param>
/// <param name="includeAllJobs">Should jobs that prohibit antag roles (ie Heads, Sec, Interns) be included</param>
/// <param name="acceptableAntags">Should players already selected as antags be eligible</param>
/// <param name="ignorePreferences">Should we ignore if the player has enabled this specific role</param>
/// <param name="customExcludeCondition">A function, accepting an EntityUid and returning bool. Each player is tested against this, returning truw will exclude the player from eligibility</param>
/// <returns>True if the player session matches the requirements, false otherwise</returns>
public bool IsPlayerEligible(ICommonSession session,
ProtoId<AntagPrototype> antagPrototype,
bool includeAllJobs = false,
AntagAcceptability acceptableAntags = AntagAcceptability.NotExclusive,
bool ignorePreferences = false,
bool allowNonHumanoids = false,
Func<EntityUid?, bool>? 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<PendingClockInComponent>(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<HumanoidAppearanceComponent>(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;
}
/// <summary>
/// Check if the session is eligible for a role, can be run prior to the session being attached to an entity
/// </summary>
/// <param name="session">Player session to check</param>
/// <param name="antagPrototype">Which antag prototype to check for</param>
/// <param name="ignorePreferences">Ignore if the player has enabled this antag</param>
/// <returns>True if the session matches the requirements, false otherwise</returns>
public bool IsSessionEligible(ICommonSession session, ProtoId<AntagPrototype> 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
/// <summary>
/// Helper method to calculate the number of antags to select based upon the number of players
/// </summary>
/// <param name="playerCount">How many players there are on the server</param>
/// <param name="playersPerAntag">How many players should there be for an additional antag</param>
/// <param name="maxAntags">Maximum number of antags allowed</param>
/// <returns>The number of antags that should be chosen</returns>
public int CalculateAntagCount(int playerCount, int playersPerAntag, int maxAntags)
{
return Math.Clamp(playerCount / playersPerAntag, 1, maxAntags);
}
#region Antag Selection
/// <summary>
/// Selects a set number of entities from several lists, prioritising the first list till its empty, then second list etc
/// </summary>
/// <param name="eligiblePlayerLists">Array of lists, which are chosen from in order until the correct number of items are selected</param>
/// <param name="count">How many items to select</param>
/// <returns>Up to the specified count of elements from all provided lists</returns>
public List<EntityUid> ChooseAntags(int count, params List<EntityUid>[] eligiblePlayerLists)
{
var chosenPlayers = new List<EntityUid>();
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;
}
/// <summary>
/// Helper method to choose antags from a list
/// </summary>
/// <param name="eligiblePlayers">List of eligible players</param>
/// <param name="count">How many to choose</param>
/// <returns>Up to the specified count of elements from the provided list</returns>
public List<EntityUid> ChooseAntags(int count, List<EntityUid> eligiblePlayers)
private void OnJobsAssigned(RulePlayerJobsAssignedEvent args)
{
var chosenPlayers = new List<EntityUid>();
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;
}
/// <summary>
/// Chooses antagonists from the current selection of players
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent)
{
var sessions = _playerManager.Sessions.ToList();
ChooseAntags(ent, sessions);
}
/// <summary>
/// Chooses antagonists from the given selection of players
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> pool)
{
foreach (var def in ent.Comp.Definitions)
{
ChooseAntags(ent, pool, def);
}
}
/// <summary>
/// Chooses antagonists from the given selection of players for the given antag definition.
/// </summary>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, List<ICommonSession> 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;
}
/// <summary>
/// Selects a set number of sessions from several lists, prioritising the first list till its empty, then second list etc
/// </summary>
/// <param name="eligiblePlayerLists">Array of lists, which are chosen from in order until the correct number of items are selected</param>
/// <param name="count">How many items to select</param>
/// <returns>Up to the specified count of elements from all provided lists</returns>
public List<ICommonSession> ChooseAntags(int count, params List<ICommonSession>[] eligiblePlayerLists)
{
var chosenPlayers = new List<ICommonSession>();
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;
}
/// <summary>
/// Helper method to choose sessions from a list
/// </summary>
/// <param name="eligiblePlayers">List of eligible sessions</param>
/// <param name="count">How many to choose</param>
/// <returns>Up to the specified count of elements from the provided list</returns>
public List<ICommonSession> ChooseAntags(int count, List<ICommonSession> eligiblePlayers)
{
var chosenPlayers = new List<ICommonSession>();
for (int i = 0; i < count; i++)
/// <summary>
/// Makes a given player into the specified antagonist.
/// </summary>
public void MakeAntag(Entity<AntagSelectionComponent> 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<GhostComponent>(session.AttachedEntity))
antagEnt = session.AttachedEntity;
}
return chosenPlayers;
}
#endregion
#region Briefings
/// <summary>
/// Helper method to send the briefing text and sound to a list of entities
/// </summary>
/// <param name="entities">The players chosen to be antags</param>
/// <param name="briefing">The briefing text to send</param>
/// <param name="briefingColor">The color the briefing should be, null for default</param>
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
public void SendBriefing(List<EntityUid> 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;
}
}
/// <summary>
/// Helper method to send the briefing text and sound to a player entity
/// </summary>
/// <param name="entity">The entity chosen to be antag</param>
/// <param name="briefing">The briefing text to send</param>
/// <param name="briefingColor">The color the briefing should be, null for default</param>
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
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<GhostRoleAntagSpawnerComponent>(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);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sessions"></param>
/// <param name="briefing"></param>
/// <param name="briefingColor"></param>
/// <param name="briefingSound"></param>
public void SendBriefing(List<ICommonSession> sessions, string briefing, Color? briefingColor, SoundSpecifier? briefingSound)
public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, List<ICommonSession> sessions, AntagSelectionDefinition def)
{
var primaryList = new List<ICommonSession>();
var secondaryList = new List<ICommonSession>();
var fallbackList = new List<ICommonSession>();
var rawList = new List<ICommonSession>();
foreach (var session in sessions)
{
SendBriefing(session, briefing, briefingColor, briefingSound);
}
}
/// <summary>
/// Helper method to send the briefing text and sound to a session
/// </summary>
/// <param name="session">The player chosen to be an antag</param>
/// <param name="briefing">The briefing text to send</param>
/// <param name="briefingColor">The color the briefing should be, null for default</param>
/// <param name="briefingSound">The sound to briefing/greeting sound to play</param>
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);
}
/// <summary>
/// Checks if a given session is valid for an antagonist.
/// </summary>
public bool IsSessionValid(Entity<AntagSelectionComponent> 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;
}
/// <summary>
/// Checks if a given entity (mind/session not included) is valid for a given antagonist.
/// </summary>
private bool IsEntityValid(EntityUid? entity, AntagSelectionDefinition def)
{
if (entity == null)
return false;
if (HasComp<PendingClockInComponent>(entity))
return false;
if (!def.AllowNonHumans && !HasComp<HumanoidAppearanceComponent>(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
}
/// <summary>
/// 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.
/// </summary>
[ByRefEvent]
public record struct AntagSelectEntityEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
{
public readonly ICommonSession? Session = Session;
public bool Handled => Entity != null;
public EntityUid? Entity;
}
/// <summary>
/// Event raised on a game rule entity to determine the location for the antagonist.
/// </summary>
[ByRefEvent]
public record struct AntagSelectLocationEvent(ICommonSession? Session, Entity<AntagSelectionComponent> GameRule)
{
public readonly ICommonSession? Session = Session;
public bool Handled => Coordinates.Any();
public List<MapCoordinates> Coordinates = new();
}
/// <summary>
/// Event raised on a game rule entity after the setup logic for an antag is complete.
/// Used for applying additional more complex setup logic.
/// </summary>
[ByRefEvent]
public readonly record struct AfterAntagEntitySelectedEvent(ICommonSession? Session, EntityUid EntityUid, Entity<AntagSelectionComponent> GameRule, AntagSelectionDefinition Def);

View File

@@ -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
{
/// <summary>
/// Has the primary selection of antagonists finished yet?
/// </summary>
[DataField]
public bool SelectionsComplete;
/// <summary>
/// The definitions for the antagonists
/// </summary>
[DataField]
public List<AntagSelectionDefinition> Definitions = new();
/// <summary>
/// The minds and original names of the players selected to be antagonists.
/// </summary>
[DataField]
public List<(EntityUid, string)> SelectedMinds = new();
/// <summary>
/// When the antag selection will occur.
/// </summary>
[DataField]
public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
/// <summary>
/// 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.
/// </summary>
public HashSet<ICommonSession> SelectedSessions = new();
}
[DataDefinition]
public partial struct AntagSelectionDefinition()
{
/// <summary>
/// A list of antagonist roles that are used for selecting which players will be antagonists.
/// </summary>
[DataField]
public List<ProtoId<AntagPrototype>> PrefRoles = new();
/// <summary>
/// Fallback for <see cref="PrefRoles"/>. Useful if you need multiple role preferences for a team antagonist.
/// </summary>
[DataField]
public List<ProtoId<AntagPrototype>> FallbackRoles = new();
/// <summary>
/// Should we allow people who already have an antagonist role?
/// </summary>
[DataField]
public AntagAcceptability MultiAntagSetting = AntagAcceptability.None;
/// <summary>
/// The minimum number of this antag.
/// </summary>
[DataField]
public int Min = 1;
/// <summary>
/// The maximum number of this antag.
/// </summary>
[DataField]
public int Max = 1;
/// <summary>
/// A range used to randomly select <see cref="Min"/>
/// </summary>
[DataField]
public MinMax? MinRange;
/// <summary>
/// A range used to randomly select <see cref="Max"/>
/// </summary>
[DataField]
public MinMax? MaxRange;
/// <summary>
/// a player to antag ratio: used to determine the amount of antags that will be present.
/// </summary>
[DataField]
public int PlayerRatio = 10;
/// <summary>
/// Whether or not players should be picked to inhabit this antag or not.
/// </summary>
[DataField]
public bool PickPlayer = true;
/// <summary>
/// If true, players that latejoin into a round have a chance of being converted into antagonists.
/// </summary>
[DataField]
public bool LateJoinAdditional = false;
//todo: find out how to do this with minimal boilerplate: filler department, maybe?
//public HashSet<ProtoId<JobPrototype>> JobBlacklist = new()
/// <remarks>
/// Mostly just here for legacy compatibility and reducing boilerplate
/// </remarks>
[DataField]
public bool AllowNonHumans = false;
/// <summary>
/// A whitelist for selecting which players can become this antag.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// A blacklist for selecting which players can become this antag.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// Components added to the player.
/// </summary>
[DataField]
public ComponentRegistry Components = new();
/// <summary>
/// Components added to the player's mind.
/// </summary>
[DataField]
public ComponentRegistry MindComponents = new();
/// <summary>
/// A set of starting gear that's equipped to the player.
/// </summary>
[DataField]
public ProtoId<StartingGearPrototype>? StartingGear;
/// <summary>
/// A briefing shown to the player.
/// </summary>
[DataField]
public BriefingData? Briefing;
/// <summary>
/// A spawner used to defer the selection of this particular definition.
/// </summary>
/// <remarks>
/// Not the cleanest way of doing this code but it's just an odd specific behavior.
/// Sue me.
/// </remarks>
[DataField]
public EntProtoId? SpawnerPrototype;
}
/// <summary>
/// Contains data used to generate a briefing.
/// </summary>
[DataDefinition]
public partial struct BriefingData
{
/// <summary>
/// The text shown
/// </summary>
[DataField]
public LocId? Text;
/// <summary>
/// The color of the text.
/// </summary>
[DataField]
public Color? Color;
/// <summary>
/// The sound played.
/// </summary>
[DataField]
public SoundSpecifier? Sound;
}

View File

@@ -0,0 +1,14 @@
namespace Content.Server.Antag.Components;
/// <summary>
/// Ghost role spawner that creates an antag for the associated gamerule.
/// </summary>
[RegisterComponent, Access(typeof(AntagSelectionSystem))]
public sealed partial class GhostRoleAntagSpawnerComponent : Component
{
[DataField]
public EntityUid? Rule;
[DataField]
public AntagSelectionDefinition? Definition;
}

View File

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

View File

@@ -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.
/// <summary>
/// Used for the fixture created to handle passing firestacks when two flammable objects collide.
/// </summary>
[DataField("flammableCollisionShape")]
[DataField]
public IPhysShape FlammableCollisionShape = new PhysShapeCircle(0.35f);
/// <summary>
/// Should the component be set on fire by interactions with isHot entities
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("alwaysCombustible")]
[DataField]
public bool AlwaysCombustible = false;
/// <summary>
/// Can the component anyhow lose its FireStacks?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("canExtinguish")]
[DataField]
public bool CanExtinguish = true;
/// <summary>
/// How many firestacks should be applied to component when being set on fire?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("firestacksOnIgnite")]
[DataField]
public float FirestacksOnIgnite = 2.0f;
/// <summary>

View File

@@ -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<PhysicsComponent> _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<PhysicsComponent>();
SubscribeLocalEvent<FlammableComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<FlammableComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<FlammableComponent, StartCollideEvent>(OnCollide);
@@ -131,7 +131,7 @@ namespace Content.Server.Atmos.EntitySystems
if (!TryComp<PhysicsComponent>(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<FlammableComponent> 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<IgnitionSourceComponent>(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);

View File

@@ -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";
}
/// <summary>
/// Event broadcast before a cargo order is fulfilled, allowing alternate systems to fulfill the order.
/// </summary>
[ByRefEvent]
public record struct FulfillCargoOrderEvent(Entity<StationDataComponent> Station, CargoOrderData Order, Entity<CargoOrderConsoleComponent> OrderConsole)
{
public Entity<CargoOrderConsoleComponent> OrderConsole = OrderConsole;
public Entity<StationDataComponent> Station = Station;
public CargoOrderData Order = Order;
public EntityUid? FulfillmentEntity;
public bool Handled = false;
}

View File

@@ -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<StationDataComponent> 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.
/// </summary>
private void UpdateOrders(EntityUid dbUid, StationCargoOrderDatabaseComponent _)
private void UpdateOrders(EntityUid dbUid)
{
// Order added so all consoles need updating.
var orderQuery = AllEntityQuery<CargoOrderConsoleComponent>();
@@ -392,7 +399,7 @@ namespace Content.Server.Cargo.Systems
string description,
string dest,
StationCargoOrderDatabaseComponent component,
StationDataComponent stationData
Entity<StationDataComponent> stationData
)
{
DebugTools.Assert(_protoMan.HasIndex<EntityPrototype>(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)

View File

@@ -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<CargoTelepadComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<CargoTelepadComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<CargoTelepadComponent, PowerChangedEvent>(OnTelepadPowerChange);
// Shouldn't need re-anchored event
SubscribeLocalEvent<CargoTelepadComponent, AnchorStateChangedEvent>(OnTelepadAnchorChange);
SubscribeLocalEvent<FulfillCargoOrderEvent>(OnTelepadFulfillCargoOrder);
}
private void OnTelepadFulfillCargoOrder(ref FulfillCargoOrderEvent args)
{
var query = EntityQueryEnumerator<CargoTelepadComponent, TransformComponent>();
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<DeviceLinkSinkComponent>(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<CargoTelepadComponent>();
@@ -33,14 +71,6 @@ public sealed partial class CargoSystem
continue;
}
if (!TryComp<DeviceLinkSinkComponent>(uid, out var sinkComponent) ||
sinkComponent.LinkedSources.FirstOrNull() is not { } console ||
!HasComp<CargoOrderConsoleComponent>(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<StationCargoOrderDatabaseComponent>(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<CargoTelepadComponent> 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<StationCargoOrderDatabaseComponent>).ToList());
}
if (!TryComp<StationCargoOrderDatabaseComponent>(station, out var db) ||
!TryComp<StationDataComponent>(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)
{

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
namespace Content.Server.GameTicking.Rules.Components;
namespace Content.Server.GameTicking.Components;
/// <summary>
/// Added to game rules before <see cref="GameRuleStartedEvent"/> and removed before <see cref="GameRuleEndedEvent"/>.

View File

@@ -0,0 +1,16 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.GameTicking.Components;
/// <summary>
/// Generic component used to track a gamerule that's start has been delayed.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class DelayedStartRuleComponent : Component
{
/// <summary>
/// The time at which the rule will start properly.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan RuleStartTime;
}

View File

@@ -1,4 +1,4 @@
namespace Content.Server.GameTicking.Rules.Components;
namespace Content.Server.GameTicking.Components;
/// <summary>
/// Added to game rules before <see cref="GameRuleEndedEvent"/>.

View File

@@ -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;
/// <summary>
/// Component attached to all gamerule entities.
@@ -20,6 +21,12 @@ public sealed partial class GameRuleComponent : Component
/// </summary>
[DataField]
public int MinPlayers;
/// <summary>
/// A delay for when the rule the is started and when the starting logic actually runs.
/// </summary>
[DataField]
public MinMax? Delay;
}
/// <summary>

View File

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

View File

@@ -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<DelayedStartRuleComponent>(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<DelayedStartRuleComponent>(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<DelayedStartRuleComponent, GameRuleComponent>();
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
}
/*
/// <summary>
/// Raised broadcast when a game rule is selected, but not started yet.
/// </summary>
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;
}
}
*/

View File

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

View File

@@ -133,6 +133,7 @@ namespace Content.Server.GameTicking
return;
base.Update(frameTime);
UpdateRoundFlow(frameTime);
UpdateGameRules();
}
}
}

View File

@@ -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;
/// <summary>
/// This is used for a game rule that loads a map when activated.
/// </summary>
[RegisterComponent]
public sealed partial class LoadMapRuleComponent : Component
{
[DataField]
public MapId? Map;
[DataField]
public ProtoId<GameMapPrototype>? GameMap ;
[DataField]
public ResPath? MapPath;
[DataField]
public List<EntityUid> MapGrids = new();
[DataField]
public EntityWhitelist? SpawnerWhitelist;
}

View File

@@ -8,7 +8,7 @@ namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Stores some configuration used by the ninja system.
/// Objectives and roundend summary are handled by <see cref="GenericAntagRuleComponent/">.
/// Objectives and roundend summary are handled by <see cref="GenericAntagRuleComponent"/>.
/// </summary>
[RegisterComponent, Access(typeof(SpaceNinjaSystem))]
public sealed partial class NinjaRuleComponent : Component

View File

@@ -1,6 +1,3 @@
using Content.Shared.Roles;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
@@ -9,11 +6,5 @@ namespace Content.Server.GameTicking.Rules.Components;
/// TODO: Remove once systems can request spawns from the ghost role system directly.
/// </summary>
[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!;
}

View File

@@ -6,4 +6,6 @@
[RegisterComponent]
public sealed partial class NukeOpsShuttleComponent : Component
{
[DataField]
public EntityUid AssociatedRule;
}

View File

@@ -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
{
/// <summary>
/// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
/// </summary>
[DataField]
public int PlayersPerOperative = 10;
[DataField]
public int MaxOps = 5;
/// <summary>
/// What will happen if all of the nuclear operatives will die. Used by LoneOpsSpawn event.
/// </summary>
@@ -56,12 +41,6 @@ public sealed partial class NukeopsRuleComponent : Component
[DataField]
public TimeSpan EvacShuttleTime = TimeSpan.FromMinutes(3);
/// <summary>
/// Whether or not to spawn the nuclear operative outpost. Used by LoneOpsSpawn event.
/// </summary>
[DataField]
public bool SpawnOutpost = true;
/// <summary>
/// Whether or not nukie left their outpost
/// </summary>
@@ -84,7 +63,7 @@ public sealed partial class NukeopsRuleComponent : Component
/// This amount of TC will be given to each nukie
/// </summary>
[DataField]
public int WarTCAmountPerNukie = 40;
public int WarTcAmountPerNukie = 40;
/// <summary>
/// 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<GameMapPrototype> OutpostMapPrototype = "NukieOutpost";
[DataField]
public WinType WinType = WinType.Neutral;
[DataField]
public List<WinCondition> 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<NpcFactionPrototype> Faction = "Syndicate";
/// <summary>
/// Data to be used in <see cref="OnMindAdded"/> for an operative once the Mind has been added.
/// Path to antagonist alert sound.
/// </summary>
[DataField]
public Dictionary<EntityUid, string> OperativeMindPendingData = new();
[DataField(required: true)]
public ProtoId<NpcFactionPrototype> 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");
}
/// <summary>

View File

@@ -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<EntityUid> Pirates = new();
[ViewVariables]
public EntityUid PirateShip = EntityUid.Invalid;
[ViewVariables]
public HashSet<EntityUid> InitialItems = new();
[ViewVariables]
public double InitialShipValue;
/// <summary>
/// Path to antagonist alert sound.
/// </summary>
[DataField("pirateAlertSound")]
public SoundSpecifier PirateAlertSound = new SoundPathSpecifier(
"/Audio/Ambience/Antag/pirate_start.ogg",
AudioParams.Default.WithVolume(4));
}

View File

@@ -22,43 +22,6 @@ public sealed partial class RevolutionaryRuleComponent : Component
[DataField]
public TimeSpan TimerWait = TimeSpan.FromSeconds(20);
/// <summary>
/// Stores players minds
/// </summary>
[DataField]
public Dictionary<string, EntityUid> HeadRevs = new();
[DataField]
public ProtoId<AntagPrototype> HeadRevPrototypeId = "HeadRev";
/// <summary>
/// Min players needed for Revolutionary gamemode to start.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int MinPlayers = 15;
/// <summary>
/// Max Head Revs allowed during selection.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int MaxHeadRevs = 3;
/// <summary>
/// The amount of Head Revs that will spawn per this amount of players.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int PlayersPerHeadRev = 15;
/// <summary>
/// The gear head revolutionaries are given on spawn.
/// </summary>
[DataField]
public List<EntProtoId> StartingGear = new()
{
"Flash",
"ClothingEyesGlassesSunglasses"
};
/// <summary>
/// The time it takes after the last head is killed for the shuttle to arrive.
/// </summary>

View File

@@ -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;
/// <summary>
/// Stores data for <see cref="ThiefRuleSystem/">.
/// Stores data for <see cref="ThiefRuleSystem"/>.
/// </summary>
[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;
/// <summary>
/// Add a Pacified comp to thieves
/// </summary>
[DataField]
public bool PacifistThieves = true;
[DataField]
public ProtoId<AntagPrototype> ThiefPrototypeId = "Thief";
[DataField]
public float MaxObjectiveDifficulty = 2.5f;
[DataField]
public int MaxStealObjectives = 10;
/// <summary>
/// Things that will be given to thieves
/// </summary>
[DataField]
public List<EntProtoId> StarterItems = new() { "ToolboxThief", "ClothingHandsChameleonThief" };
/// <summary>
/// All Thieves created by this rule
/// </summary>
[DataField]
public List<EntityUid> ThievesMinds = new();
/// <summary>
/// Max Thiefs created by rule on roundstart
/// </summary>
[DataField]
public int MaxAllowThief = 3;
/// <summary>
/// Sound played when making the player a thief via antag control or ghost role
/// </summary>
[DataField]
public SoundSpecifier? GreetingSound = new SoundPathSpecifier("/Audio/Misc/thief_greeting.ogg");
}

View File

@@ -57,4 +57,19 @@ public sealed partial class TraitorRuleComponent : Component
/// </summary>
[DataField]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/traitor_start.ogg");
/// <summary>
/// The amount of codewords that are selected.
/// </summary>
[DataField]
public int CodewordCount = 4;
/// <summary>
/// The amount of TC traitors start with.
/// </summary>
[DataField]
public int StartingBalance = 20;
[DataField]
public int MaxDifficulty = 20;
}

View File

@@ -8,12 +8,6 @@ namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(ZombieRuleSystem))]
public sealed partial class ZombieRuleComponent : Component
{
[DataField]
public Dictionary<string, string> InitialInfectedNames = new();
[DataField]
public ProtoId<AntagPrototype> PatientZeroPrototypeId = "InitialInfected";
/// <summary>
/// When the round will next check for round end.
/// </summary>
@@ -26,61 +20,9 @@ public sealed partial class ZombieRuleComponent : Component
[DataField]
public TimeSpan EndCheckDelay = TimeSpan.FromSeconds(30);
/// <summary>
/// The time at which the initial infected will be chosen.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan? StartTime;
/// <summary>
/// The minimum amount of time after the round starts that the initial infected will be chosen.
/// </summary>
[DataField]
public TimeSpan MinStartDelay = TimeSpan.FromMinutes(10);
/// <summary>
/// The maximum amount of time after the round starts that the initial infected will be chosen.
/// </summary>
[DataField]
public TimeSpan MaxStartDelay = TimeSpan.FromMinutes(15);
/// <summary>
/// The sound that plays when someone becomes an initial infected.
/// todo: this should have a unique sound instead of reusing the zombie one.
/// </summary>
[DataField]
public SoundSpecifier InitialInfectedSound = new SoundPathSpecifier("/Audio/Ambience/Antag/zombie_start.ogg");
/// <summary>
/// The minimum amount of time initial infected have before they start taking infection damage.
/// </summary>
[DataField]
public TimeSpan MinInitialInfectedGrace = TimeSpan.FromMinutes(12.5f);
/// <summary>
/// The maximum amount of time initial infected have before they start taking damage.
/// </summary>
[DataField]
public TimeSpan MaxInitialInfectedGrace = TimeSpan.FromMinutes(15f);
/// <summary>
/// How many players for each initial infected.
/// </summary>
[DataField]
public int PlayersPerInfected = 10;
/// <summary>
/// The maximum number of initial infected.
/// </summary>
[DataField]
public int MaxInitialInfected = 6;
/// <summary>
/// After this amount of the crew become zombies, the shuttle will be automatically called.
/// </summary>
[DataField]
public float ZombieShuttleCallPercentage = 0.7f;
[DataField]
public EntProtoId ZombifySelfActionPrototype = "ActionTurnUndead";
}

View File

@@ -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<DeathMatchRuleComponen
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
SubscribeLocalEvent<KillReportedEvent>(OnKillReported);
SubscribeLocalEvent<DeathMatchRuleComponent, PlayerPointChangedEvent>(OnPointChanged);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextAppend);
}
private void OnBeforeSpawn(PlayerBeforeSpawnEvent ev)
@@ -113,21 +113,17 @@ public sealed class DeathMatchRuleSystem : GameRuleSystem<DeathMatchRuleComponen
_roundEnd.EndRound(component.RestartDelay);
}
private void OnRoundEndTextAppend(RoundEndTextAppendEvent ev)
protected override void AppendRoundEndText(EntityUid uid, DeathMatchRuleComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args)
{
var query = EntityQueryEnumerator<DeathMatchRuleComponent, PointManagerComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var dm, out var point, out var rule))
{
if (!GameTicker.IsGameRuleAdded(uid, rule))
continue;
if (!TryComp<PointManagerComponent>(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());
}
}

View File

@@ -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<T> where T: IComponent
return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
}
protected bool TryRoundStartAttempt(RoundStartAttemptEvent ev, string localizedPresetName)
{
var query = EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
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;
}
/// <summary>
/// Utility function for finding a random event-eligible station entity
/// </summary>

View File

@@ -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<T> : EntitySystem where T : ICompon
{
base.Initialize();
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
SubscribeLocalEvent<T, GameRuleAddedEvent>(OnGameRuleAdded);
SubscribeLocalEvent<T, GameRuleStartedEvent>(OnGameRuleStarted);
SubscribeLocalEvent<T, GameRuleEndedEvent>(OnGameRuleEnded);
SubscribeLocalEvent<T, RoundEndTextAppendEvent>(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<T> : EntitySystem where T : ICompon
Ended(uid, component, ruleData, args);
}
private void OnRoundEndTextAppend(Entity<T> ent, ref RoundEndTextAppendEvent args)
{
if (!TryComp<GameRuleComponent>(ent, out var ruleData))
return;
AppendRoundEndText(ent, ent, ruleData, ref args);
}
/// <summary>
/// Called when the gamerule is added
@@ -73,6 +101,14 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
}
/// <summary>
/// Called at the end of a round when text needs to be added for a game rule.
/// </summary>
protected virtual void AppendRoundEndText(EntityUid uid, T component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args)
{
}
/// <summary>
/// Called on an active gamerule entity in the Update function
/// </summary>

View File

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

View File

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

View File

@@ -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<LoadMapRuleComponent>
{
[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<LoadMapRuleComponent, AntagSelectLocationEvent>(OnSelectLocation);
SubscribeLocalEvent<GridSplitEvent>(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<LoadMapRuleComponent> ent, ref AntagSelectLocationEvent args)
{
var query = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
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));
}
}
}

View File

@@ -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<MaxTimeRestartRule
public void RestartTimer(MaxTimeRestartRuleComponent component)
{
// TODO FULL GAME SAVE
component.TimerCancel.Cancel();
component.TimerCancel = new CancellationTokenSource();
Timer.Spawn(component.RoundMaxTime, () => TimerFired(component), component.TimerCancel.Token);
@@ -49,6 +51,7 @@ public sealed class MaxTimeRestartRuleSystem : GameRuleSystem<MaxTimeRestartRule
_chatManager.DispatchServerAnnouncement(Loc.GetString("rule-restarting-in-seconds",("seconds", (int) component.RoundEndDelay.TotalSeconds)));
// TODO FULL GAME SAVE
Timer.Spawn(component.RoundEndDelay, () => GameTicker.RestartRound());
}

View File

@@ -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<NukeopsRuleComponent>
{
[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<CurrencyPrototype>]
private const string TelecrystalCurrencyPrototype = "Telecrystal";
@@ -79,141 +53,67 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
[ValidatePrototypeId<TagPrototype>]
private const string NukeOpsUplinkTagPrototype = "NukeOpsUplink";
[ValidatePrototypeId<AntagPrototype>]
public const string NukeopsId = "Nukeops";
[ValidatePrototypeId<DatasetPrototype>]
private const string OperationPrefixDataset = "operationPrefix";
[ValidatePrototypeId<DatasetPrototype>]
private const string OperationSuffixDataset = "operationSuffix";
public override void Initialize()
{
base.Initialize();
_sawmill = _logManager.GetSawmill("NukeOps");
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayersSpawning);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
SubscribeLocalEvent<NukeExplodedEvent>(OnNukeExploded);
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRunLevelChanged);
SubscribeLocalEvent<NukeDisarmSuccessEvent>(OnNukeDisarm);
SubscribeLocalEvent<NukeOperativeComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<NukeOperativeComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<NukeOperativeComponent, GhostRoleSpawnerUsedEvent>(OnPlayersGhostSpawning);
SubscribeLocalEvent<NukeOperativeComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
SubscribeLocalEvent<NukeOpsShuttleComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ConsoleFTLAttemptEvent>(OnShuttleFTLAttempt);
SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt);
SubscribeLocalEvent<NukeopsRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
SubscribeLocalEvent<NukeopsRuleComponent, AfterAntagEntitySelectedEvent>(OnAfterAntagEntSelected);
}
protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
var eligibleQuery = EntityQueryEnumerator<StationEventEligibleComponent, NpcFactionMemberComponent>();
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<NukieSpawn> { 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<NukeopsRoleComponent, MindContainerComponent>();
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<NukeopsRuleComponent>
{
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<NukeopsRuleComponent>
}
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<NukeopsRuleComponent>
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<NukeopsRuleComponent> 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<NukeComponent, TransformComponent>();
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<NukeDiskComponent, TransformComponent>();
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<NukeopsRuleComponent>
CheckRoundShouldEnd();
}
private void OnPlayersGhostSpawning(EntityUid uid, NukeOperativeComponent component, GhostRoleSpawnerUsedEvent args)
{
var spawner = args.Spawner;
if (!TryComp<NukeOperativeSpawnerComponent>(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<NukeopsRuleComponent>())
{
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<NukeOpsShuttleComponent> ent, ref MapInitEvent args)
{
var map = Transform(ent).MapID;
var rules = EntityQueryEnumerator<NukeopsRuleComponent, LoadMapRuleComponent>();
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<NukeopsRuleComponent>
{
// 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<LoadMapRuleComponent>(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<NukeopsRuleComponent>
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<NukeopsRuleComponent> 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<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
var eligibleQuery = EntityQueryEnumerator<StationEventEligibleComponent, NpcFactionMemberComponent>();
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<NukeOperativeComponent, ActorComponent>();
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<NukeComponent, TransformComponent>();
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<NukeopsRoleComponent, MindContainerComponent, MobStateComponent>();
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<NukeDiskComponent, TransformComponent>();
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<NukeopsRuleComponent>
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<NukeComponent>())
{
if (nuke.Status == NukeStatus.ARMED)
{
armed = true;
break;
}
}
if (armed)
continue;
private void CheckRoundShouldEnd(Entity<NukeopsRuleComponent> 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<NukeComponent>())
{
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<NukeOperativeComponent, MobStateComponent, TransformComponent>(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<NukeOperativeSpawnerComponent>(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<NukeopsRuleComponent> 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<NukeOpsShuttleComponent, TransformComponent>();
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<NukeOperativeComponent, MobStateComponent, TransformComponent>(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<NukeOperativeSpawnerComponent>(true).Any();
if (spawnsAvailable && CompOrNull<LoadMapRuleComponent>(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;
}
/// <summary>
/// Adds missing nuke operative components, equips starting gear and renames the entity.
/// </summary>
private void SetupOperativeEntity(EntityUid mob, string name, NukeopSpawnPreset spawnDetails, HumanoidCharacterProfile? profile)
// this should really go anywhere else but im tired.
private void OnAntagSelectEntity(Entity<NukeopsRuleComponent> ent, ref AntagSelectEntityEvent args)
{
_metaData.SetEntityName(mob, name);
EnsureComp<NukeOperativeComponent>(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<NukieSpawn> sessions, bool spawnGhostRoles, NukeopsRuleComponent component)
{
if (component.NukieOutpost is not { Valid: true } outpostUid)
if (args.Handled)
return;
var spawns = new List<EntityCoordinates>();
foreach (var (_, meta, xform) in EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(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<SpeciesPrototype>(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<SpeciesPrototype>(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<GhostRoleComponent>(spawnPoint);
EnsureComp<GhostRoleMobSpawnerComponent>(spawnPoint);
ghostRole.RoleName = Loc.GetString(nukeOpsAntag.Name);
ghostRole.RoleDescription = Loc.GetString(nukeOpsAntag.Objective);
var nukeOpSpawner = EnsureComp<NukeOperativeSpawnerComponent>(spawnPoint);
nukeOpSpawner.OperativeName = name;
nukeOpSpawner.SpawnDetails = nukieSession.Type;
}
}
args.Entity = Spawn(species.Prototype);
_humanoid.LoadProfile(args.Entity.Value, profile);
}
/// <summary>
/// Display a greeting message and play a sound for a nukie
/// </summary>
private void NotifyNukie(ICommonSession session, NukeOperativeComponent nukeop, NukeopsRuleComponent nukeopsRule)
private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> 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);
}
/// <summary>
/// Spawn nukie ghost roles if this gamerule was started mid round
/// </summary>
private void SpawnOperativesForGhostRoles(EntityUid uid, NukeopsRuleComponent? component = null)
/// <remarks>
/// Is this method the shitty glue holding together the last of my sanity? yes.
/// Do i have a better solution? not presently.
/// </remarks>
private EntityUid? GetOutpost(Entity<LoadMapRuleComponent?> 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<NukieSpawn>();
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)
/// <remarks>
/// Is this method the shitty glue holding together the last of my sanity? yes.
/// Do i have a better solution? not presently.
/// </remarks>
private EntityUid? GetShuttle(Entity<NukeopsRuleComponent?> 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<NukeOpsShuttleComponent>();
while (query.MoveNext(out var uid, out var comp))
{
Session = session;
Type = type;
if (comp.AssociatedRule == ent.Owner)
return uid;
}
return null;
}
}

View File

@@ -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;
/// <summary>
/// This handles the Pirates minor antag, which is designed to coincide with other modes on occasion.
/// </summary>
public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
{
[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<EntityPrototype>]
private const string GameRuleId = "Pirates";
[ValidatePrototypeId<EntityPrototype>]
private const string MobId = "MobHuman";
[ValidatePrototypeId<SpeciesPrototype>]
private const string SpeciesId = "Human";
[ValidatePrototypeId<NpcFactionPrototype>]
private const string PirateFactionId = "Syndicate";
[ValidatePrototypeId<NpcFactionPrototype>]
private const string EnemyFactionId = "NanoTrasen";
[ValidatePrototypeId<StartingGearPrototype>]
private const string GearId = "PirateGear";
[ValidatePrototypeId<EntityPrototype>]
private const string SpawnPointId = "SpawnPointPirates";
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawningEvent);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndTextEvent);
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
}
private void OnRoundEndTextEvent(RoundEndTextAppendEvent ev)
{
var query = EntityQueryEnumerator<PiratesRuleComponent, GameRuleComponent>();
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<PiratesRuleComponent, GameRuleComponent>();
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 <max pirate count>: 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<TransformComponent>();
var aabbs = EntityQuery<StationDataComponent>().SelectMany(x =>
x.Grids.Select(x =>
xformQuery.GetComponent(x).WorldMatrix.TransformBox(Comp<MapGridComponent>(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<StartingGearPrototype>(GearId); // YARRR
var spawns = new List<EntityCoordinates>();
// Forgive me for hardcoding prototypes
foreach (var (_, meta, xform) in
EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(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<PiratesRuleComponent>().FirstOrDefault();
if (pirateRule == null)
{
//todo fuck me this shit is awful
GameTicker.StartGameRule(GameRuleId, out var ruleEntity);
pirateRule = Comp<PiratesRuleComponent>(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<PiratesRuleComponent, GameRuleComponent>();
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();
}
}
}
}

View File

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

View File

@@ -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<RevolutionaryRuleCo
{
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly EuiManager _euiMan = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
@@ -51,7 +49,6 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
//Used in OnPostFlash, no reference to the rule component is available
public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
@@ -60,23 +57,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayerJobAssigned);
SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(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<RevolutionaryRuleCo
}
}
private void OnRoundEndText(RoundEndTextAppendEvent ev)
protected override void AppendRoundEndText(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
base.AppendRoundEndText(uid, component, gameRule, ref args);
var revsLost = CheckRevsLose();
var commandLost = CheckCommandLose();
var query = AllEntityQuery<RevolutionaryRuleComponent>();
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<RevolutionaryRoleComponent>(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<RevolutionaryRoleComponent>(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<RevolutionaryRuleCo
args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
}
//Check for enough players to start rule
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
TryRoundStartAttempt(ev, Loc.GetString("roles-antag-rev-name"));
}
private void OnPlayerJobAssigned(RulePlayerJobsAssignedEvent ev)
{
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out var activeGameRule, out var comp, out var gameRule))
{
var eligiblePlayers = _antagSelection.GetEligiblePlayers(ev.Players, comp.HeadRevPrototypeId);
if (eligiblePlayers.Count == 0)
continue;
var headRevCount = _antagSelection.CalculateAntagCount(ev.Players.Length, comp.PlayersPerHeadRev, comp.MaxHeadRevs);
var headRevs = _antagSelection.ChooseAntags(headRevCount, eligiblePlayers);
GiveHeadRev(headRevs, comp.HeadRevPrototypeId, comp);
}
}
private void GiveHeadRev(IEnumerable<EntityUid> chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
{
foreach (var headRev in chosen)
GiveHeadRev(headRev, antagProto, comp);
}
private void GiveHeadRev(EntityUid chosen, ProtoId<AntagPrototype> antagProto, RevolutionaryRuleComponent comp)
{
RemComp<CommandStaffComponent>(chosen);
var inCharacterName = MetaData(chosen).EntityName;
if (!_mind.TryGetMind(chosen, out var mind, out _))
return;
if (!_role.MindHasRole<RevolutionaryRoleComponent>(mind))
{
_role.MindAddRole(mind, new RevolutionaryRoleComponent { PrototypeId = antagProto }, silent: true);
}
comp.HeadRevs.Add(inCharacterName, mind);
_inventory.SpawnItemsOnEntity(chosen, comp.StartingGear);
var revComp = EnsureComp<RevolutionaryComponent>(chosen);
EnsureComp<HeadRevolutionaryComponent>(chosen);
_antagSelection.SendBriefing(chosen, Loc.GetString("head-rev-role-greeting"), Color.CornflowerBlue, revComp.RevStartSound);
}
/// <summary>
/// Called when a Head Rev uses a flash in melee to convert somebody else.
/// </summary>
@@ -232,22 +156,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
}
if (mind?.Session != null)
_antagSelection.SendBriefing(mind.Session, Loc.GetString("rev-role-greeting"), Color.Red, revComp.RevStartSound);
}
public void OnHeadRevAdmin(EntityUid entity)
{
if (HasComp<HeadRevolutionaryComponent>(entity))
return;
var revRule = EntityQuery<RevolutionaryRuleComponent>().FirstOrDefault();
if (revRule == null)
{
GameTicker.StartGameRule("Revolutionary", out var ruleEnt);
revRule = Comp<RevolutionaryRuleComponent>(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<RevolutionaryRuleCo
_popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid);
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying.");
if (!_mind.TryGetMind(uid, out var mindId, out var mind, mc))
if (!_mind.TryGetMind(uid, out var mindId, out _, mc))
continue;
// remove their antag role

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Sandbox;

View File

@@ -1,4 +1,5 @@
using Content.Server.Administration.Logs;
using Content.Server.GameTicking.Components;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking.Presets;
using Content.Server.GameTicking.Rules.Components;

View File

@@ -1,3 +1,4 @@
using Content.Server.GameTicking.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Shared.Storage;

View File

@@ -3,118 +3,37 @@ using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.Roles;
using Content.Shared.Antag;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.GameTicking.Rules;
public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
{
[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<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
SubscribeLocalEvent<ThiefRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagSelected);
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<ThiefRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
}
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
private void AfterAntagSelected(Entity<ThiefRuleComponent> 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<EntityUid> 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<ThiefRoleComponent>(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<PacifiedComponent>(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<ThiefRuleComponent>().FirstOrDefault();
if (thiefRule == null)
{
GameTicker.StartGameRule("Thief", out var ruleEntity);
thiefRule = Comp<ThiefRuleComponent>(ruleEntity);
}
if (HasComp<ThiefRoleComponent>(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<ThiefRuleComponent>
private string MakeBriefing(EntityUid thief)
{
var isHuman = HasComp<HumanoidAppearanceComponent>(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<ThiefRuleComponent>
return briefing;
}
private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> thiefs, ref ObjectivesTextGetInfoEvent args)
private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> ent, ref ObjectivesTextGetInfoEvent args)
{
args.Minds = thiefs.Comp.ThievesMinds;
args.Minds = _antag.GetAntagMindEntityUids(ent.Owner);
args.AgentName = Loc.GetString("thief-round-end-agent-name");
}
}

View File

@@ -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<TraitorRuleComponent>
{
[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<RoundStartAttemptEvent>(OnStartAttempt);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(HandleLatejoin);
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(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<TraitorRuleComponent> 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;
}
}
/// <summary>
/// Check for enough players
/// </summary>
/// <param name="ev"></param>
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<DatasetPrototype>(component.CodewordAdjectives).Values;
var verbs = _prototypeManager.Index<DatasetPrototype>(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<TraitorRuleComponent>
}
}
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<EntityUid> 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<TraitorRoleComponent>(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<TraitorRuleComponent>
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<TraitorRuleComponent>
// 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<TraitorRuleComponent>
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<TraitorRuleComponent>
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
}
/// <summary>
/// Start this game rule manually
/// </summary>
public TraitorRuleComponent StartGameRule()
{
var comp = EntityQuery<TraitorRuleComponent>().FirstOrDefault();
if (comp == null)
{
GameTicker.StartGameRule("Traitor", out var ruleEntity);
comp = Comp<TraitorRuleComponent>(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<TraitorRuleComponent>
public List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind)
{
List<(EntityUid Id, MindComponent Mind)> allTraitors = new();
foreach (var traitor in EntityQuery<TraitorRuleComponent>())
var query = EntityQueryEnumerator<TraitorRuleComponent>();
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<TraitorRuleComponent>
return allTraitors;
}
private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, TraitorRuleComponent component)
private List<(EntityUid Id, MindComponent Mind)> GetOtherTraitorMindsAliveAndConnected(MindComponent ourMind, Entity<TraitorRuleComponent> 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;

View File

@@ -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<ZombieRuleComponent>
{
[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<RoundStartAttemptEvent>(OnStartAttempt);
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
SubscribeLocalEvent<PendingZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
}
/// <summary>
/// Set the required minimum players for this gamemode to start
/// </summary>
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<ZombieRuleComponent>())
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<ZombieRuleComponent>
_roundEnd.EndRound();
}
/// <summary>
/// Check we have enough players to start this game mode, if not - cancel and announce
/// </summary>
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<ZombieRuleComponent>
}
return healthy;
}
/// <summary>
/// Infects the first players with the passive zombie virus.
/// Also records their names for the end of round screen.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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<ZombieImmuneComponent>(player) || HasComp<InitialInfectedExemptComponent>(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<ZombieImmuneComponent>
);
//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<EntityUid> 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<InitialInfectedComponent>(entity);
//Add the zombie components and grace period
var pending = EnsureComp<PendingZombieComponent>(entity);
pending.GracePeriod = _random.Next(component.MinInitialInfectedGrace, component.MaxInitialInfectedGrace);
EnsureComp<ZombifyOnDeathComponent>(entity);
EnsureComp<IncurableZombieComponent>(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);
}
}

View File

@@ -0,0 +1,18 @@
using Content.Shared.Actions;
using Content.Shared.Polymorph;
using Robust.Shared.Prototypes;
namespace Content.Server.Geras;
/// <summary>
/// This component assigns the entity with a polymorph action.
/// </summary>
[RegisterComponent]
public sealed partial class GerasComponent : Component
{
[DataField] public ProtoId<PolymorphPrototype> GerasPolymorphId = "SlimeMorphGeras";
[DataField] public ProtoId<EntityPrototype> GerasAction = "ActionMorphGeras";
[DataField] public EntityUid? GerasActionEntity;
}

View File

@@ -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;
/// <inheritdoc/>
public sealed class GerasSystem : SharedGerasSystem
{
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly PolymorphSystem _polymorphSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<GerasComponent, MorphIntoGeras>(OnMorphIntoGeras);
SubscribeLocalEvent<GerasComponent, MapInitEvent>(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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(List<NetUserId> userIds);
bool HavePreferencesLoaded(ICommonSession session);
}

View File

@@ -256,6 +256,20 @@ namespace Content.Server.Preferences.Managers
return prefs;
}
/// <summary>
/// Retrieves preferences for the given username from storage or returns null.
/// Creates and saves default preferences if they are not found, then returns them.
/// </summary>
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<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId)
{
var prefs = await _db.GetPlayerPreferencesAsync(userId);

View File

@@ -1,20 +0,0 @@
using Content.Server.Radio.EntitySystems;
namespace Content.Server.Radio.Components;
/// <summary>
/// When activated (<see cref="ActiveRadioJammerComponent"/>) prevents from sending messages in range
/// </summary>
[RegisterComponent]
[Access(typeof(JammerSystem))]
public sealed partial class RadioJammerComponent : Component
{
[DataField("range"), ViewVariables(VVAccess.ReadWrite)]
public float Range = 8f;
/// <summary>
/// Power usage per second when enabled
/// </summary>
[DataField("wattage"), ViewVariables(VVAccess.ReadWrite)]
public float Wattage = 2f;
}

View File

@@ -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<ActiveRadioJammerComponent, RadioJammerComponent>();
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<ActiveRadioJammerComponent>(uid);
RemComp<DeviceNetworkJammerComponent>(uid);
if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage(jam) * frameTime, battery))
{
ChangeLEDState(false, uid);
RemComp<ActiveRadioJammerComponent>(uid);
RemComp<DeviceNetworkJammerComponent>(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<ActiveRadioJammerComponent>(uid) &&
_powerCell.TryGetBatteryFromSlot(uid, out var battery) &&
battery.CurrentCharge > comp.Wattage;
battery.CurrentCharge > GetCurrentWattage(comp);
if (activated)
{
ChangeLEDState(true, uid);
EnsureComp<ActiveRadioJammerComponent>(uid);
EnsureComp<DeviceNetworkJammerComponent>(uid, out var jammingComp);
jammingComp.Range = comp.Range;
jammingComp.Range = GetCurrentRange(comp);
jammingComp.JammableNetworks.Add(DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString());
Dirty(uid, jammingComp);
}
else
{
RemComp<ActiveRadioJammerComponent>(uid);
RemComp<DeviceNetworkJammerComponent>(uid);
ChangeLEDState(false, uid);
RemCompDeferred<ActiveRadioJammerComponent>(uid);
RemCompDeferred<DeviceNetworkJammerComponent>(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<ActiveRadioJammerComponent>(uid);
{
ChangeLEDState(false, uid);
RemCompDeferred<ActiveRadioJammerComponent>(uid);
}
}
private void OnExamine(EntityUid uid, RadioJammerComponent comp, ExaminedEvent args)
{
if (args.IsInDetailsRange)
{
var msg = HasComp<ActiveRadioJammerComponent>(uid)
var powerIndicator = HasComp<ActiveRadioJammerComponent>(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;
}

View File

@@ -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<string>();
foreach (var segment in segments)
{
outputSegments.Add(_prototype.TryIndex<DatasetPrototype>(segment, out var proto)
? Loc.GetString(_random.Pick(proto.Values))
: Loc.GetString(segment));
if (_prototype.TryIndex<DatasetPrototype>(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);
}

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More