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:
8
Content.Client/DeviceNetwork/JammerSystem.cs
Normal file
8
Content.Client/DeviceNetwork/JammerSystem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Radio.EntitySystems;
|
||||
|
||||
namespace Content.Client.DeviceNetwork;
|
||||
|
||||
public sealed class JammerSystem : SharedJammerSystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
55
Content.Client/UserInterface/Controls/ClipControl.cs
Normal file
55
Content.Client/UserInterface/Controls/ClipControl.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
};
|
||||
|
||||
|
||||
203
Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
Normal file
203
Content.IntegrationTests/Tests/GameRules/NukeOpsTest.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
}
|
||||
102
Content.IntegrationTests/Tests/Mapping/MappingTests.cs
Normal file
102
Content.IntegrationTests/Tests/Mapping/MappingTests.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
29
Content.Server/Antag/AntagSelectionPlayerPool.cs
Normal file
29
Content.Server/Antag/AntagSelectionPlayerPool.cs
Normal 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);
|
||||
}
|
||||
302
Content.Server/Antag/AntagSelectionSystem.API.cs
Normal file
302
Content.Server/Antag/AntagSelectionSystem.API.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
189
Content.Server/Antag/Components/AntagSelectionComponent.cs
Normal file
189
Content.Server/Antag/Components/AntagSelectionComponent.cs
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"/>.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Content.Server.GameTicking.Rules.Components;
|
||||
namespace Content.Server.GameTicking.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Added to game rules before <see cref="GameRuleEndedEvent"/>.
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -133,6 +133,7 @@ namespace Content.Server.GameTicking
|
||||
return;
|
||||
base.Update(frameTime);
|
||||
UpdateRoundFlow(frameTime);
|
||||
UpdateGameRules();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
[RegisterComponent]
|
||||
public sealed partial class NukeOpsShuttleComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public EntityUid AssociatedRule;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
80
Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs
Normal file
80
Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Sandbox;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Shared.Storage;
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
18
Content.Server/Geras/GerasComponent.cs
Normal file
18
Content.Server/Geras/GerasComponent.cs
Normal 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;
|
||||
}
|
||||
41
Content.Server/Geras/GerasSystem.cs
Normal file
41
Content.Server/Geras/GerasSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user