Merge remote-tracking branch 'upstream/stable' into april-fools-stable-merge

This commit is contained in:
Milon
2025-03-21 06:22:25 +01:00
931 changed files with 41132 additions and 26235 deletions

View File

@@ -44,7 +44,7 @@ namespace Content.Benchmarks
for (var i = 0; i < Aabbs1.Length; i++)
{
var aabb = Aabbs1[i];
_b2Tree.CreateProxy(aabb, i);
_b2Tree.CreateProxy(aabb, uint.MaxValue, i);
_tree.Add(i);
}
}

View File

@@ -50,6 +50,8 @@ internal sealed class AdminNameOverlay : Overlay
//TODO make this adjustable via GUI
var classic = _config.GetCVar(CCVars.AdminOverlayClassic);
var playTime = _config.GetCVar(CCVars.AdminOverlayPlaytime);
var startingJob = _config.GetCVar(CCVars.AdminOverlayStartingJob);
foreach (var playerInfo in _system.PlayerList)
{
@@ -76,25 +78,44 @@ internal sealed class AdminNameOverlay : Overlay
}
var uiScale = _userInterfaceManager.RootControl.UIScale;
var lineoffset = new Vector2(0f, 11f) * uiScale;
var lineoffset = new Vector2(0f, 14f) * uiScale;
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
var currentOffset = Vector2.Zero;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
currentOffset += lineoffset;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
currentOffset += lineoffset;
if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && playTime)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? Color.Orange : Color.White);
currentOffset += lineoffset;
}
if (!string.IsNullOrEmpty(playerInfo.StartingJob) && startingJob)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? Color.GreenYellow : Color.White);
currentOffset += lineoffset;
}
if (classic && playerInfo.Antag)
{
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), _antagLabelClassic, uiScale, _antagColorClassic);
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, _antagLabelClassic, uiScale, Color.OrangeRed);
currentOffset += lineoffset;
}
else if (!classic && _filter.Contains(playerInfo.RoleProto))
{
var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
var color = playerInfo.RoleProto.Color;
var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
var color = playerInfo.RoleProto.Color;
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), label, uiScale, color);
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
}
args.ScreenHandle.DrawString(_font, screenCoordinates + lineoffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
}
}
}

View File

@@ -2,24 +2,26 @@
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
<PanelContainer StyleClasses="BackgroundDark">
<SplitContainer Orientation="Horizontal" VerticalExpand="True">
<cc:PlayerListControl Access="Public" Name="ChannelSelector" HorizontalExpand="True" SizeFlagsStretchRatio="1" />
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<CheckBox Name="AdminOnly" Access="Public" Text="{Loc 'admin-ahelp-admin-only'}" ToolTip="{Loc 'admin-ahelp-admin-only-tooltip'}" />
<Control HorizontalExpand="True" MinWidth="5" />
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
<Control HorizontalExpand="True" MinWidth="5" />
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
<Control HorizontalExpand="True" />
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Follow" Text="{Loc 'admin-player-actions-follow'}" StyleClasses="OpenLeft" />
<SplitContainer Orientation="Vertical">
<SplitContainer Orientation="Horizontal" VerticalExpand="True">
<cc:PlayerListControl Access="Public" Name="ChannelSelector" HorizontalExpand="True" SizeFlagsStretchRatio="2" />
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="2">
<BoxContainer Access="Public" Name="BwoinkArea" VerticalExpand="True" />
</BoxContainer>
</SplitContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<CheckBox Name="AdminOnly" Access="Public" Text="{Loc 'admin-ahelp-admin-only'}" ToolTip="{Loc 'admin-ahelp-admin-only-tooltip'}" />
<Control HorizontalExpand="True" MinWidth="5" />
<CheckBox Name="PlaySound" Access="Public" Text="{Loc 'admin-bwoink-play-sound'}" Pressed="True" />
<Control HorizontalExpand="True" MinWidth="5" />
<Button Visible="True" Name="PopOut" Access="Public" Text="{Loc 'admin-logs-pop-out'}" StyleClasses="OpenBoth" HorizontalAlignment="Left" />
<Control HorizontalExpand="True" />
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Follow" Text="{Loc 'admin-player-actions-follow'}" StyleClasses="OpenLeft" />
</BoxContainer>
</SplitContainer>
</PanelContainer>

View File

@@ -62,9 +62,9 @@ namespace Content.Client.Administration.UI.Bwoink
var sb = new StringBuilder();
if (info.Connected)
sb.Append('●');
sb.Append(info.ActiveThisRound ? '⚫' : '◐');
else
sb.Append(info.ActiveThisRound ? '' : '·');
sb.Append(info.ActiveThisRound ? '' : '·');
sb.Append(' ');
if (AHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
@@ -76,10 +76,12 @@ namespace Content.Client.Administration.UI.Bwoink
sb.Append(' ');
}
// Mark antagonists with symbol
if (info.Antag && info.ActiveThisRound)
sb.Append(new Rune(0x1F5E1)); // 🗡
if (newPlayerThreshold != 0 && (info.OverallPlaytime == null || info.OverallPlaytime <= TimeSpan.FromMinutes(newPlayerThreshold)))
// Mark new players with symbol
if (IsNewPlayer(info))
sb.Append(new Rune(0x23F2)); // ⏲
sb.AppendFormat("\"{0}\"", text);
@@ -87,6 +89,19 @@ namespace Content.Client.Administration.UI.Bwoink
return sb.ToString();
};
// <summary>
// Returns true if the player's overall playtime is under the set threshold
// </summary>
bool IsNewPlayer(PlayerInfo info)
{
// Don't show every disconnected player as new, don't show 0-minute players as new if threshold is
if (newPlayerThreshold <= 0 || info.OverallPlaytime is null && !info.Connected)
return false;
return (info.OverallPlaytime is null
|| info.OverallPlaytime < TimeSpan.FromMinutes(newPlayerThreshold));
}
ChannelSelector.Comparison = (a, b) =>
{
var ach = AHelpHelper.EnsurePanel(a.SessionId);
@@ -96,31 +111,37 @@ namespace Content.Client.Administration.UI.Bwoink
if (a.IsPinned != b.IsPinned)
return a.IsPinned ? -1 : 1;
// First, sort by unread. Any chat with unread messages appears first.
// Then, any chat with unread messages.
var aUnread = ach.Unread > 0;
var bUnread = bch.Unread > 0;
if (aUnread != bUnread)
return aUnread ? -1 : 1;
// Sort by recent messages during the current round.
// Then, any chat with recent messages from the current round
var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue;
var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue;
if (aRecent != bRecent)
return aRecent ? -1 : 1;
// Next, sort by connection status. Any disconnected players are grouped towards the end.
// Sort by connection status. Disconnected players will be last.
if (a.Connected != b.Connected)
return a.Connected ? -1 : 1;
// Sort connected players by New Player status, then by Antag status
// Sort connected players by whether they have joined the round, then by New Player status, then by Antag status
if (a.Connected && b.Connected)
{
var aNewPlayer = a.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
var bNewPlayer = b.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
var aNewPlayer = IsNewPlayer(a);
var bNewPlayer = IsNewPlayer(b);
// Players who have joined the round will be listed before players in the lobby
if (a.ActiveThisRound != b.ActiveThisRound)
return a.ActiveThisRound ? -1 : 1;
// Within both the joined group and lobby group, new players will be grouped and listed first
if (aNewPlayer != bNewPlayer)
return aNewPlayer ? -1 : 1;
// Within all four previous groups, antagonists will be listed first.
if (a.Antag != b.Antag)
return a.Antag ? -1 : 1;
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Advertise.Systems;
namespace Content.Client.Advertise.Systems;
public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;

View File

@@ -28,7 +28,6 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
private void OnMapInit(EntityUid uid, SolutionContainerVisualsComponent component, MapInitEvent args)
{
var meta = MetaData(uid);
component.InitialName = meta.EntityName;
component.InitialDescription = meta.EntityDescription;
}

View File

@@ -36,29 +36,6 @@ internal sealed class ShowSubFloor : LocalizedCommands
}
}
internal sealed class ShowSubFloorForever : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
public const string CommandName = "showsubfloorforever";
public override string Command => CommandName;
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
_entitySystemManager.GetEntitySystem<SubFloorHideSystem>().ShowAll = true;
var entMan = IoCManager.Resolve<IEntityManager>();
var components = entMan.EntityQuery<SubFloorHideComponent, SpriteComponent>(true);
foreach (var (_, sprite) in components)
{
sprite.DrawDepth = (int) DrawDepth.Overlays;
}
}
}
internal sealed class NotifyCommand : LocalizedCommands
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;

View File

@@ -24,7 +24,7 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands
{
_entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible = true;
_lightManager.Enabled = false;
shell.ExecuteCommand("showsubfloorforever");
shell.ExecuteCommand("showsubfloor");
_entitySystemManager.GetEntitySystem<ActionsSystem>().LoadActionAssignments("/mapping_actions.yml", false);
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Delivery;
namespace Content.Client.Delivery;
public sealed class DeliverySystem : SharedDeliverySystem;

View File

@@ -0,0 +1,45 @@
using Content.Shared.Delivery;
using Content.Shared.StatusIcon;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Delivery;
public sealed class DeliveryVisualizerSystem : VisualizerSystem<DeliveryComponent>
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
private static readonly ProtoId<JobIconPrototype> UnknownIcon = "JobIconUnknown";
protected override void OnAppearanceChange(EntityUid uid, DeliveryComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
_appearance.TryGetData(uid, DeliveryVisuals.JobIcon, out string job, args.Component);
if (string.IsNullOrEmpty(job))
job = UnknownIcon;
if (!_prototype.TryIndex<JobIconPrototype>(job, out var icon))
{
args.Sprite.LayerSetTexture(DeliveryVisualLayers.JobStamp, _sprite.Frame0(_prototype.Index("JobIconUnknown")));
return;
}
args.Sprite.LayerSetTexture(DeliveryVisualLayers.JobStamp, _sprite.Frame0(icon.Icon));
}
}
public enum DeliveryVisualLayers : byte
{
Icon,
Lock,
FragileStamp,
JobStamp,
PriorityTape,
Breakage,
Trash,
}

View File

@@ -1,30 +1,34 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Horizontal"
Orientation="Vertical"
HorizontalAlignment="Stretch"
HorizontalExpand="True"
Margin="0 0 0 5">
<BoxContainer Name="ReactantsContainer" Orientation="Vertical" HorizontalExpand="True" VerticalAlignment="Center">
<RichTextLabel Name="ReactantsLabel"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Access="Public"
Visible="False"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextureRect TexturePath="/Textures/Interface/Misc/beakerlarge.png"
HorizontalAlignment="Center"
Name="MixTexture"
Access="Public"/>
<RichTextLabel Name="MixLabel"
HorizontalAlignment="Center"
Access="Public"
Margin="2 0 0 0"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalAlignment="Center">
<RichTextLabel Name="ProductsLabel"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Access="Public"
Visible="False"/>
<BoxContainer Orientation="Horizontal">
<BoxContainer Name="ReactantsContainer" Orientation="Vertical" HorizontalExpand="True"
VerticalAlignment="Center">
<RichTextLabel Name="ReactantsLabel"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Access="Public"
Visible="False" />
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextureRect TexturePath="/Textures/Interface/Misc/beakerlarge.png"
HorizontalAlignment="Center"
Name="MixTexture"
Access="Public" />
<RichTextLabel Name="MixLabel"
HorizontalAlignment="Center"
Access="Public"
Margin="2 0 0 0" />
</BoxContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalAlignment="Center">
<RichTextLabel Name="ProductsLabel"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Access="Public"
Visible="False" />
</BoxContainer>
</BoxContainer>
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
</BoxContainer>

View File

@@ -1,8 +1,10 @@
using Content.Shared.CCVar;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Robust.Client.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -12,12 +14,15 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly MarkingManager _markingManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HumanoidAppearanceComponent, AfterAutoHandleStateEvent>(OnHandleState);
Subs.CVar(_configurationManager, CCVars.AccessibilityClientCensorNudity, OnCvarChanged, true);
Subs.CVar(_configurationManager, CCVars.AccessibilityServerCensorNudity, OnCvarChanged, true);
}
private void OnHandleState(EntityUid uid, HumanoidAppearanceComponent component, ref AfterAutoHandleStateEvent args)
@@ -25,6 +30,15 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
UpdateSprite(component, Comp<SpriteComponent>(uid));
}
private void OnCvarChanged(bool value)
{
var humanoidQuery = EntityManager.AllEntityQueryEnumerator<HumanoidAppearanceComponent, SpriteComponent>();
while (humanoidQuery.MoveNext(out var _, out var humanoidComp, out var spriteComp))
{
UpdateSprite(humanoidComp, spriteComp);
}
}
private void UpdateSprite(HumanoidAppearanceComponent component, SpriteComponent sprite)
{
UpdateLayers(component, sprite);
@@ -207,16 +221,30 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
// Really, markings should probably be a separate component altogether.
ClearAllMarkings(humanoid, sprite);
var censorNudity = _configurationManager.GetCVar(CCVars.AccessibilityClientCensorNudity) ||
_configurationManager.GetCVar(CCVars.AccessibilityServerCensorNudity);
// The reason we're splitting this up is in case the character already has undergarment equipped in that slot.
var applyUndergarmentTop = censorNudity;
var applyUndergarmentBottom = censorNudity;
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
{
foreach (var marking in markingList)
{
if (_markingManager.TryGetMarking(marking, out var markingPrototype))
{
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentTop)
applyUndergarmentTop = false;
else if (markingPrototype.BodyPart == HumanoidVisualLayers.UndergarmentBottom)
applyUndergarmentBottom = false;
}
}
}
humanoid.ClientOldMarkings = new MarkingSet(humanoid.MarkingSet);
AddUndergarments(humanoid, sprite, applyUndergarmentTop, applyUndergarmentBottom);
}
private void ClearAllMarkings(HumanoidAppearanceComponent humanoid, SpriteComponent sprite)
@@ -264,6 +292,31 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
spriteComp.RemoveLayer(index);
}
}
private void AddUndergarments(HumanoidAppearanceComponent humanoid, SpriteComponent sprite, bool undergarmentTop, bool undergarmentBottom)
{
if (undergarmentTop && humanoid.UndergarmentTop != null)
{
var marking = new Marking(humanoid.UndergarmentTop, new List<Color> { new Color() });
if (_markingManager.TryGetMarking(marking, out var prototype))
{
// Markings are added to ClientOldMarkings because otherwise it causes issues when toggling the feature on/off.
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentTop, new List<Marking>{ marking });
ApplyMarking(prototype, null, true, humanoid, sprite);
}
}
if (undergarmentBottom && humanoid.UndergarmentBottom != null)
{
var marking = new Marking(humanoid.UndergarmentBottom, new List<Color> { new Color() });
if (_markingManager.TryGetMarking(marking, out var prototype))
{
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentBottom, new List<Marking>{ marking });
ApplyMarking(prototype, null, true, humanoid, sprite);
}
}
}
private void ApplyMarking(MarkingPrototype markingPrototype,
IReadOnlyList<Color>? colors,
bool visible,

View File

@@ -26,6 +26,12 @@ namespace Content.Client.IconSmoothing
[ViewVariables(VVAccess.ReadWrite), DataField("key")]
public string? SmoothKey { get; private set; }
/// <summary>
/// Additional keys to smooth with.
/// </summary>
[DataField]
public List<string> AdditionalKeys = new();
/// <summary>
/// Prepended to the RSI state.
/// </summary>

View File

@@ -376,7 +376,8 @@ namespace Content.Client.IconSmoothing
while (candidates.MoveNext(out var entity))
{
if (smoothQuery.TryGetComponent(entity, out var other) &&
other.SmoothKey == smooth.SmoothKey &&
other.SmoothKey != null &&
(other.SmoothKey == smooth.SmoothKey || smooth.AdditionalKeys.Contains(other.SmoothKey)) &&
other.Enabled)
{
return true;

View File

@@ -59,6 +59,7 @@
PlaceHolder="0"
Text="1"
HorizontalExpand="True" />
<Label Name="RecipeCount" Margin="8 0 8 0" MinWidth="90" Align="Right" />
</BoxContainer>
</BoxContainer>
</BoxContainer>

View File

@@ -120,6 +120,8 @@ public sealed partial class LatheMenu : DefaultWindow
if (!int.TryParse(AmountLineEdit.Text, out var quantity) || quantity <= 0)
quantity = 1;
RecipeCount.Text = Loc.GetString("lathe-menu-recipe-count", ("count", recipesToShow.Count));
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
RecipeList.Children.Clear();
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);

View File

@@ -54,6 +54,6 @@ public sealed class AfterLightTargetOverlay : Overlay
worldHandle.SetTransform(localMatrix);
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
}, null);
}, Color.Transparent);
}
}

View File

@@ -16,6 +16,7 @@ public sealed class PlanetLightSystem : EntitySystem
_overlayMan.AddOverlay(new RoofOverlay(EntityManager));
_overlayMan.AddOverlay(new TileEmissionOverlay(EntityManager));
_overlayMan.AddOverlay(new LightBlurOverlay());
_overlayMan.AddOverlay(new SunShadowOverlay());
_overlayMan.AddOverlay(new AfterLightTargetOverlay());
}
@@ -31,6 +32,7 @@ public sealed class PlanetLightSystem : EntitySystem
_overlayMan.RemoveOverlay<RoofOverlay>();
_overlayMan.RemoveOverlay<TileEmissionOverlay>();
_overlayMan.RemoveOverlay<LightBlurOverlay>();
_overlayMan.RemoveOverlay<SunShadowOverlay>();
_overlayMan.RemoveOverlay<AfterLightTargetOverlay>();
}
}

View File

@@ -0,0 +1,92 @@
using System.Diagnostics.Contracts;
using System.Numerics;
using Content.Client.GameTicking.Managers;
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Light.EntitySystems;
public sealed class SunShadowSystem : SharedSunShadowSystem
{
[Dependency] private readonly ClientGameTicker _ticker = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
return;
var mapQuery = AllEntityQuery<SunShadowCycleComponent, SunShadowComponent>();
while (mapQuery.MoveNext(out var uid, out var cycle, out var shadow))
{
if (!cycle.Running || cycle.Directions.Count == 0)
continue;
var pausedTime = _metadata.GetPauseTime(uid);
var time = (float)(_timing.CurTime
.Add(cycle.Offset)
.Subtract(_ticker.RoundStartTimeSpan)
.Subtract(pausedTime)
.TotalSeconds % cycle.Duration.TotalSeconds);
var (direction, alpha) = GetShadow((uid, cycle), time);
shadow.Direction = direction;
shadow.Alpha = alpha;
}
}
[Pure]
public (Vector2 Direction, float Alpha) GetShadow(Entity<SunShadowCycleComponent> entity, float time)
{
// So essentially the values are stored as the percentages of the total duration just so it adjusts the speed
// dynamically and we don't have to manually handle it.
// It will lerp from each value to the next one with angle and length handled separately
var ratio = (float) (time / entity.Comp.Duration.TotalSeconds);
for (var i = entity.Comp.Directions.Count - 1; i >= 0; i--)
{
var dir = entity.Comp.Directions[i];
if (ratio > dir.Ratio)
{
var next = entity.Comp.Directions[(i + 1) % entity.Comp.Directions.Count];
float nextRatio;
// Last entry
if (i == entity.Comp.Directions.Count - 1)
{
nextRatio = next.Ratio + 1f;
}
else
{
nextRatio = next.Ratio;
}
var range = nextRatio - dir.Ratio;
var diff = (ratio - dir.Ratio) / range;
DebugTools.Assert(diff is >= 0f and <= 1f);
// We lerp angle + length separately as we don't want a straight-line lerp and want the rotation to be consistent.
var currentAngle = dir.Direction.ToAngle();
var nextAngle = next.Direction.ToAngle();
var angle = Angle.Lerp(currentAngle, nextAngle, diff);
// This is to avoid getting weird issues where the angle gets pretty close but length still noticeably catches up.
var lengthDiff = MathF.Pow(diff, 1f / 2f);
var length = float.Lerp(dir.Direction.Length(), next.Direction.Length(), lengthDiff);
var vector = angle.ToVec() * length;
var alpha = float.Lerp(dir.Alpha, next.Alpha, diff);
return (vector, alpha);
}
}
throw new InvalidOperationException();
}
}

View File

@@ -1,6 +1,7 @@
using Content.Client.GameTicking.Managers;
using Content.Shared;
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
@@ -11,19 +12,29 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
{
[Dependency] private readonly ClientGameTicker _ticker = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
return;
var mapQuery = AllEntityQuery<LightCycleComponent, MapLightComponent>();
while (mapQuery.MoveNext(out var uid, out var cycle, out var map))
{
if (!cycle.Running)
continue;
// We still iterate paused entities as we still want to override the lighting color and not have
// it apply the server state
var pausedTime = _metadata.GetPauseTime(uid);
var time = (float) _timing.CurTime
.Add(cycle.Offset)
.Subtract(_ticker.RoundStartTimeSpan)
.Subtract(pausedTime)
.TotalSeconds;
var color = GetColor((uid, cycle), cycle.OriginalColor, time);

View File

@@ -94,13 +94,15 @@ public sealed class RoofOverlay : Overlay
// Due to stencilling we essentially draw on unrooved tiles
while (tileEnumerator.MoveNext(out var tileRef))
{
if (!_roof.IsRooved(roofEnt, tileRef.GridIndices))
var color = _roof.GetColor(roofEnt, tileRef.GridIndices);
if (color == null)
{
continue;
}
var local = _lookup.GetLocalBounds(tileRef, grid.Comp.TileSize);
worldHandle.DrawRect(local, roof.Color);
worldHandle.DrawRect(local, color.Value);
}
}
}, null);

View File

@@ -0,0 +1,160 @@
using System.Numerics;
using Content.Shared.Light.Components;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
namespace Content.Client.Light;
public sealed class SunShadowOverlay : Overlay
{
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private readonly EntityLookupSystem _lookup;
private readonly SharedTransformSystem _xformSys;
private readonly HashSet<Entity<SunShadowCastComponent>> _shadows = new();
private IRenderTexture? _blurTarget;
private IRenderTexture? _target;
public SunShadowOverlay()
{
IoCManager.InjectDependencies(this);
_xformSys = _entManager.System<SharedTransformSystem>();
_lookup = _entManager.System<EntityLookupSystem>();
ZIndex = AfterLightTargetOverlay.ContentZIndex + 1;
}
private List<Entity<MapGridComponent>> _grids = new();
protected override void Draw(in OverlayDrawArgs args)
{
var viewport = args.Viewport;
var eye = viewport.Eye;
if (eye == null)
return;
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId,
args.WorldBounds.Enlarged(SunShadowComponent.MaxLength),
ref _grids);
var worldHandle = args.WorldHandle;
var mapId = args.MapId;
var worldBounds = args.WorldBounds;
var targetSize = viewport.LightRenderTarget.Size;
if (_target?.Size != targetSize)
{
_target = _clyde
.CreateRenderTarget(targetSize,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: "sun-shadow-target");
if (_blurTarget?.Size != targetSize)
{
_blurTarget = _clyde
.CreateRenderTarget(targetSize, new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "sun-shadow-blur");
}
}
var lightScale = viewport.LightRenderTarget.Size / (Vector2)viewport.Size;
var scale = viewport.RenderScale / (Vector2.One / lightScale);
foreach (var grid in _grids)
{
if (!_entManager.TryGetComponent(grid.Owner, out SunShadowComponent? sun))
{
continue;
}
var direction = sun.Direction;
var alpha = Math.Clamp(sun.Alpha, 0f, 1f);
// Nowhere to cast to so ignore it.
if (direction.Equals(Vector2.Zero) || alpha == 0f)
continue;
// Feature todo: dynamic shadows for mobs and trees. Also ideally remove the fake tree shadows.
// TODO: Jittering still not quite perfect
var expandedBounds = worldBounds.Enlarged(direction.Length() + 0.01f);
_shadows.Clear();
// Draw shadow polys to stencil
args.WorldHandle.RenderInRenderTarget(_target,
() =>
{
var invMatrix =
_target.GetWorldToLocalMatrix(eye, scale);
var indices = new Vector2[PhysicsConstants.MaxPolygonVertices * 2];
// Go through shadows in range.
// For each one we:
// - Get the original vertices.
// - Extrapolate these along the sun direction.
// - Combine the above into 1 single polygon to draw.
// Note that this is range-limited for accuracy; if you set it too high it will clip through walls or other undesirable entities.
// This is probably not noticeable most of the time but if you want something "accurate" you'll want to code a solution.
// Ideally the CPU would have its own shadow-map copy that we could just ray-cast each vert into though
// You might need to batch verts or the likes as this could get expensive.
_lookup.GetEntitiesIntersecting(mapId, expandedBounds, _shadows);
foreach (var ent in _shadows)
{
var xform = _entManager.GetComponent<TransformComponent>(ent.Owner);
var (worldPos, worldRot) = _xformSys.GetWorldPositionRotation(xform);
// Need no rotation on matrix as sun shadow direction doesn't care.
var worldMatrix = Matrix3x2.CreateTranslation(worldPos);
var renderMatrix = Matrix3x2.Multiply(worldMatrix, invMatrix);
var pointCount = ent.Comp.Points.Length;
Array.Copy(ent.Comp.Points, indices, pointCount);
for (var i = 0; i < pointCount; i++)
{
// Update point based on entity rotation.
indices[i] = worldRot.RotateVec(indices[i]);
// Add the offset point by the sun shadow direction.
indices[pointCount + i] = indices[i] + direction;
}
var points = PhysicsHull.ComputePoints(indices, pointCount * 2);
worldHandle.SetTransform(renderMatrix);
worldHandle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, points, Color.White);
}
},
Color.Transparent);
// Slightly blur it just to avoid aliasing issues on the later viewport-wide blur.
_clyde.BlurRenderTarget(viewport, _target, _blurTarget!, eye, 1f);
// Draw stencil (see roofoverlay).
args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
() =>
{
var invMatrix =
viewport.LightRenderTarget.GetWorldToLocalMatrix(eye, scale);
worldHandle.SetTransform(invMatrix);
var maskShader = _protoManager.Index<ShaderPrototype>("Mix").Instance();
worldHandle.UseShader(maskShader);
worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
}, null);
}
}
}

View File

@@ -4,6 +4,8 @@
<BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical" Margin="8">
<Label Text="{Loc 'ui-options-accessability-header-visuals'}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
@@ -12,6 +14,9 @@
<ui:OptionSlider Name="SpeechBubbleTextOpacitySlider" Title="{Loc 'ui-options-speech-bubble-text-opacity'}" />
<ui:OptionSlider Name="SpeechBubbleSpeakerOpacitySlider" Title="{Loc 'ui-options-speech-bubble-speaker-opacity'}" />
<ui:OptionSlider Name="SpeechBubbleBackgroundOpacitySlider" Title="{Loc 'ui-options-speech-bubble-background-opacity'}" />
<Label Text="{Loc 'ui-options-accessability-header-content'}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="CensorNudityCheckBox" Text="{Loc 'ui-options-censor-nudity'}" />
</BoxContainer>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />

View File

@@ -21,6 +21,8 @@ public sealed partial class AccessibilityTab : Control
Control.AddOptionPercentSlider(CCVars.SpeechBubbleSpeakerOpacity, SpeechBubbleSpeakerOpacitySlider);
Control.AddOptionPercentSlider(CCVars.SpeechBubbleBackgroundOpacity, SpeechBubbleBackgroundOpacitySlider);
Control.AddOptionCheckBox(CCVars.AccessibilityClientCensorNudity, CensorNudityCheckBox);
Control.Initialize();
}
}

View File

@@ -265,6 +265,51 @@ namespace Content.Client.Options.UI.Tabs
AddButton(EngineKeyFunctions.HideUI);
AddButton(ContentKeyFunctions.InspectEntity);
AddHeader("ui-options-header-text-cursor");
AddButton(EngineKeyFunctions.TextCursorLeft);
AddButton(EngineKeyFunctions.TextCursorRight);
AddButton(EngineKeyFunctions.TextCursorUp);
AddButton(EngineKeyFunctions.TextCursorDown);
AddButton(EngineKeyFunctions.TextCursorWordLeft);
AddButton(EngineKeyFunctions.TextCursorWordRight);
AddButton(EngineKeyFunctions.TextCursorBegin);
AddButton(EngineKeyFunctions.TextCursorEnd);
AddHeader("ui-options-header-text-cursor-select");
AddButton(EngineKeyFunctions.TextCursorSelect);
AddButton(EngineKeyFunctions.TextCursorSelectLeft);
AddButton(EngineKeyFunctions.TextCursorSelectRight);
AddButton(EngineKeyFunctions.TextCursorSelectUp);
AddButton(EngineKeyFunctions.TextCursorSelectDown);
AddButton(EngineKeyFunctions.TextCursorSelectWordLeft);
AddButton(EngineKeyFunctions.TextCursorSelectWordRight);
AddButton(EngineKeyFunctions.TextCursorSelectBegin);
AddButton(EngineKeyFunctions.TextCursorSelectEnd);
AddHeader("ui-options-header-text-edit");
AddButton(EngineKeyFunctions.TextBackspace);
AddButton(EngineKeyFunctions.TextDelete);
AddButton(EngineKeyFunctions.TextWordBackspace);
AddButton(EngineKeyFunctions.TextWordDelete);
AddButton(EngineKeyFunctions.TextNewline);
AddButton(EngineKeyFunctions.TextSubmit);
AddButton(EngineKeyFunctions.MultilineTextSubmit);
AddButton(EngineKeyFunctions.TextSelectAll);
AddButton(EngineKeyFunctions.TextCopy);
AddButton(EngineKeyFunctions.TextCut);
AddButton(EngineKeyFunctions.TextPaste);
AddHeader("ui-options-header-text-chat");
AddButton(EngineKeyFunctions.TextHistoryPrev);
AddButton(EngineKeyFunctions.TextHistoryNext);
AddButton(EngineKeyFunctions.TextReleaseFocus);
AddButton(EngineKeyFunctions.TextScrollToBottom);
AddHeader("ui-options-header-text-other");
AddButton(EngineKeyFunctions.TextTabComplete);
AddButton(EngineKeyFunctions.TextCompleteNext);
AddButton(EngineKeyFunctions.TextCompletePrev);
foreach (var control in _keyControls.Values)
{
UpdateKeyControl(control);

View File

@@ -27,6 +27,8 @@ public sealed class PowerReceiverSystem : SharedPowerReceiverSystem
return;
component.Powered = state.Powered;
component.NeedsPower = state.NeedsPower;
component.PowerDisabled = state.PowerDisabled;
}
public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component)

View File

@@ -64,9 +64,15 @@ public sealed class SubFloorHideSystem : SharedSubFloorHideSystem
args.Sprite.Visible = hasVisibleLayer || revealed;
// allows a t-ray to show wires/pipes above carpets/puddles
if (scannerRevealed)
if (ShowAll)
{
// Allows sandbox mode to make wires visible over other stuff.
component.OriginalDrawDepth ??= args.Sprite.DrawDepth;
args.Sprite.DrawDepth = (int)Shared.DrawDepth.DrawDepth.Overdoors;
}
else if (scannerRevealed)
{
// Allows a t-ray to show wires/pipes above carpets/puddles.
if (component.OriginalDrawDepth is not null)
return;
component.OriginalDrawDepth = args.Sprite.DrawDepth;

View File

@@ -96,9 +96,12 @@ public class ListContainer : Control
{
ListContainerButton control = new(data[0], 0);
GenerateItem?.Invoke(data[0], control);
// Yes this AddChild is necessary for reasons (get proper style or whatever?)
// without it the DesiredSize may be different to the final DesiredSize.
AddChild(control);
control.Measure(Vector2Helpers.Infinity);
_itemHeight = control.DesiredSize.Y;
control.Dispose();
control.Orphan();
}
// Ensure buttons are re-generated.
@@ -384,6 +387,7 @@ public sealed class ListContainerButton : ContainerButton, IEntityControl
public ListContainerButton(ListData data, int index)
{
AddStyleClass(StyleClassButton);
Data = data;
Index = index;
// AddChild(Background = new PanelContainer

View File

@@ -467,8 +467,9 @@ public sealed class ChatUIController : UIController
if (existing.Count > SpeechBubbleCap)
{
// Get the oldest to start fading fast.
var last = existing[0];
// Get the next speech bubble to fade
// Any speech bubbles before it are already fading
var last = existing[^(SpeechBubbleCap + 1)];
last.FadeNow();
}
}

View File

@@ -67,7 +67,7 @@ public sealed class DamageOverlayUiController : UIController
{
_overlay.DeadLevel = 0f;
_overlay.CritLevel = 0f;
_overlay.BruteLevel = 0f;
_overlay.PainLevel = 0f;
_overlay.OxygenLevel = 0f;
}
@@ -95,13 +95,22 @@ public sealed class DamageOverlayUiController : UIController
{
case MobState.Alive:
{
if (EntityManager.HasComponent<PainNumbnessComponent>(entity))
FixedPoint2 painLevel = 0;
_overlay.PainLevel = 0;
if (!EntityManager.HasComponent<PainNumbnessComponent>(entity))
{
_overlay.BruteLevel = 0;
}
else if (damageable.DamagePerGroup.TryGetValue("Brute", out var bruteDamage))
{
_overlay.BruteLevel = FixedPoint2.Min(1f, bruteDamage / critThreshold).Float();
foreach (var painDamageType in damageable.PainDamageGroups)
{
damageable.DamagePerGroup.TryGetValue(painDamageType, out var painDamage);
painLevel += painDamage;
}
_overlay.PainLevel = FixedPoint2.Min(1f, painLevel / critThreshold).Float();
if (_overlay.PainLevel < 0.05f) // Don't show damage overlay if they're near enough to max.
{
_overlay.PainLevel = 0;
}
}
if (damageable.DamagePerGroup.TryGetValue("Airloss", out var oxyDamage))
@@ -109,11 +118,6 @@ public sealed class DamageOverlayUiController : UIController
_overlay.OxygenLevel = FixedPoint2.Min(1f, oxyDamage / critThreshold).Float();
}
if (_overlay.BruteLevel < 0.05f) // Don't show damage overlay if they're near enough to max.
{
_overlay.BruteLevel = 0;
}
_overlay.CritLevel = 0;
_overlay.DeadLevel = 0;
break;
@@ -125,13 +129,13 @@ public sealed class DamageOverlayUiController : UIController
return;
_overlay.CritLevel = critLevel.Value.Float();
_overlay.BruteLevel = 0;
_overlay.PainLevel = 0;
_overlay.DeadLevel = 0;
break;
}
case MobState.Dead:
{
_overlay.BruteLevel = 0;
_overlay.PainLevel = 0;
_overlay.CritLevel = 0;
break;
}

View File

@@ -25,9 +25,9 @@ public sealed class DamageOverlay : Overlay
/// <summary>
/// Handles the red pulsing overlay
/// </summary>
public float BruteLevel = 0f;
public float PainLevel = 0f;
private float _oldBruteLevel = 0f;
private float _oldPainLevel = 0f;
/// <summary>
/// Handles the darkening overlay.
@@ -92,14 +92,14 @@ public sealed class DamageOverlay : Overlay
DeadLevel = 0f;
}
if (!MathHelper.CloseTo(_oldBruteLevel, BruteLevel, 0.001f))
if (!MathHelper.CloseTo(_oldPainLevel, PainLevel, 0.001f))
{
var diff = BruteLevel - _oldBruteLevel;
_oldBruteLevel += GetDiff(diff, lastFrameTime);
var diff = PainLevel - _oldPainLevel;
_oldPainLevel += GetDiff(diff, lastFrameTime);
}
else
{
_oldBruteLevel = BruteLevel;
_oldPainLevel = PainLevel;
}
if (!MathHelper.CloseTo(_oldOxygenLevel, OxygenLevel, 0.001f))
@@ -135,7 +135,7 @@ public sealed class DamageOverlay : Overlay
// Makes debugging easier don't @ me
float level = 0f;
level = _oldBruteLevel;
level = _oldPainLevel;
// TODO: Lerping
if (level > 0f && _oldCritLevel <= 0f)
@@ -165,7 +165,7 @@ public sealed class DamageOverlay : Overlay
}
else
{
_oldBruteLevel = BruteLevel;
_oldPainLevel = PainLevel;
}
level = State != MobState.Critical ? _oldOxygenLevel : 1f;

View File

@@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer
NameLabel.Text = text;
}
public void SetText(string text)
{
NameLabel.Text = text;
}
}

View File

@@ -1,3 +1,4 @@
using System.Linq;
using System.Numerics;
using Content.Shared.VendingMachines;
using Robust.Client.AutoGenerated;
@@ -19,11 +20,16 @@ namespace Content.Client.VendingMachines.UI
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly Dictionary<EntProtoId, EntityUid> _dummies = [];
private readonly Dictionary<EntProtoId, (ListContainerButton Button, VendingMachineItem Item)> _listItems = new();
private readonly Dictionary<EntProtoId, uint> _amounts = new();
/// <summary>
/// Whether the vending machine is able to be interacted with or not.
/// </summary>
private bool _enabled;
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
public VendingMachineMenu()
{
MinSize = SetSize = new Vector2(250, 150);
@@ -68,18 +74,23 @@ namespace Content.Client.VendingMachines.UI
if (data is not VendorItemsListData { ItemProtoID: var protoID, ItemText: var text })
return;
button.AddChild(new VendingMachineItem(protoID, text));
button.ToolTip = text;
button.StyleBoxOverride = _styleBox;
var item = new VendingMachineItem(protoID, text);
_listItems[protoID] = (button, item);
button.AddChild(item);
button.AddStyleClass("ButtonSquare");
button.Disabled = !_enabled || _amounts[protoID] == 0;
}
/// <summary>
/// Populates the list of available items on the vending machine interface
/// and sets icons based on their prototypes
/// </summary>
public void Populate(List<VendingMachineInventoryEntry> inventory)
public void Populate(List<VendingMachineInventoryEntry> inventory, bool enabled)
{
_enabled = enabled;
_listItems.Clear();
_amounts.Clear();
if (inventory.Count == 0 && VendingContents.Visible)
{
SearchBar.Visible = false;
@@ -109,7 +120,10 @@ namespace Content.Client.VendingMachines.UI
var entry = inventory[i];
if (!_prototypeManager.TryIndex(entry.ID, out var prototype))
{
_amounts[entry.ID] = 0;
continue;
}
if (!_dummies.TryGetValue(entry.ID, out var dummy))
{
@@ -119,11 +133,15 @@ namespace Content.Client.VendingMachines.UI
var itemName = Identity.Name(dummy, _entityManager);
var itemText = $"{itemName} [{entry.Amount}]";
_amounts[entry.ID] = entry.Amount;
if (itemText.Length > longestEntry.Length)
longestEntry = itemText;
listData.Add(new VendorItemsListData(prototype.ID, itemText, i));
listData.Add(new VendorItemsListData(prototype.ID, i)
{
ItemText = itemText,
});
}
VendingContents.PopulateList(listData);
@@ -131,12 +149,43 @@ namespace Content.Client.VendingMachines.UI
SetSizeAfterUpdate(longestEntry.Length, inventory.Count);
}
/// <summary>
/// Updates text entries for vending data in place without modifying the list controls.
/// </summary>
public void UpdateAmounts(List<VendingMachineInventoryEntry> cachedInventory, bool enabled)
{
_enabled = enabled;
foreach (var proto in _dummies.Keys)
{
if (!_listItems.TryGetValue(proto, out var button))
continue;
var dummy = _dummies[proto];
var amount = cachedInventory.First(o => o.ID == proto).Amount;
// Could be better? Problem is all inventory entries get squashed.
var text = GetItemText(dummy, amount);
button.Item.SetText(text);
button.Button.Disabled = !enabled || amount == 0;
}
}
private string GetItemText(EntityUid dummy, uint amount)
{
var itemName = Identity.Name(dummy, _entityManager);
return $"{itemName} [{amount}]";
}
private void SetSizeAfterUpdate(int longestEntryLength, int contentCount)
{
SetSize = new Vector2(Math.Clamp((longestEntryLength + 2) * 12, 250, 400),
Math.Clamp(contentCount * 50, 150, 350));
}
}
}
public record VendorItemsListData(EntProtoId ItemProtoID, string ItemText, int ItemIndex) : ListData;
public record VendorItemsListData(EntProtoId ItemProtoID, int ItemIndex) : ListData
{
public string ItemText = string.Empty;
}
}

View File

@@ -31,10 +31,21 @@ namespace Content.Client.VendingMachines
public void Refresh()
{
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
var system = EntMan.System<VendingMachineSystem>();
_cachedInventory = system.GetAllInventory(Owner);
_menu?.Populate(_cachedInventory);
_menu?.Populate(_cachedInventory, enabled);
}
public void UpdateAmounts()
{
var enabled = EntMan.TryGetComponent(Owner, out VendingMachineComponent? bendy) && !bendy.Ejecting;
var system = EntMan.System<VendingMachineSystem>();
_cachedInventory = system.GetAllInventory(Owner);
_menu?.UpdateAmounts(_cachedInventory, enabled);
}
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
@@ -53,7 +64,7 @@ namespace Content.Client.VendingMachines
if (selectedItem == null)
return;
SendMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
SendPredictedMessage(new VendingMachineEjectMessage(selectedItem.Type, selectedItem.ID));
}
protected override void Dispose(bool disposing)

View File

@@ -1,6 +1,8 @@
using System.Linq;
using Content.Shared.VendingMachines;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.GameStates;
namespace Content.Client.VendingMachines;
@@ -8,7 +10,6 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
@@ -16,14 +17,69 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
SubscribeLocalEvent<VendingMachineComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<VendingMachineComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<VendingMachineComponent, AfterAutoHandleStateEvent>(OnVendingAfterState);
SubscribeLocalEvent<VendingMachineComponent, ComponentHandleState>(OnVendingHandleState);
}
private void OnVendingAfterState(EntityUid uid, VendingMachineComponent component, ref AfterAutoHandleStateEvent args)
private void OnVendingHandleState(Entity<VendingMachineComponent> entity, ref ComponentHandleState args)
{
if (_uiSystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
if (args.Current is not VendingMachineComponentState state)
return;
var uid = entity.Owner;
var component = entity.Comp;
component.Contraband = state.Contraband;
component.EjectEnd = state.EjectEnd;
component.DenyEnd = state.DenyEnd;
component.DispenseOnHitEnd = state.DispenseOnHitEnd;
// If all we did was update amounts then we can leave BUI buttons in place.
var fullUiUpdate = !component.Inventory.Keys.SequenceEqual(state.Inventory.Keys) ||
!component.EmaggedInventory.Keys.SequenceEqual(state.EmaggedInventory.Keys) ||
!component.ContrabandInventory.Keys.SequenceEqual(state.ContrabandInventory.Keys);
component.Inventory.Clear();
component.EmaggedInventory.Clear();
component.ContrabandInventory.Clear();
foreach (var entry in state.Inventory)
{
bui.Refresh();
component.Inventory.Add(entry.Key, new(entry.Value));
}
foreach (var entry in state.EmaggedInventory)
{
component.EmaggedInventory.Add(entry.Key, new(entry.Value));
}
foreach (var entry in state.ContrabandInventory)
{
component.ContrabandInventory.Add(entry.Key, new(entry.Value));
}
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(uid, VendingMachineUiKey.Key, out var bui))
{
if (fullUiUpdate)
{
bui.Refresh();
}
else
{
bui.UpdateAmounts();
}
}
}
protected override void UpdateUI(Entity<VendingMachineComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return;
if (UISystem.TryGetOpenUi<VendingMachineBoundUserInterface>(entity.Owner,
VendingMachineUiKey.Key,
out var bui))
{
bui.UpdateAmounts();
}
}
@@ -70,13 +126,13 @@ public sealed class VendingMachineSystem : SharedVendingMachineSystem
if (component.LoopDenyAnimation)
SetLayerState(VendingMachineVisualLayers.BaseUnshaded, component.DenyState, sprite);
else
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, component.DenyDelay, sprite);
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.DenyState, (float)component.DenyDelay.TotalSeconds, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break;
case VendingMachineVisualState.Eject:
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, component.EjectDelay, sprite);
PlayAnimation(uid, VendingMachineVisualLayers.BaseUnshaded, component.EjectState, (float)component.EjectDelay.TotalSeconds, sprite);
SetLayerState(VendingMachineVisualLayers.Screen, component.ScreenState, sprite);
break;

View File

@@ -54,6 +54,9 @@ public sealed class CraftingTests : InteractionTest
await CraftItem(Spear);
await FindEntity(Spear);
// Reset target because entitylookup will dump this.
Target = null;
// Player's hands should be full of the remaining rods, except those dropped during the failed crafting attempt.
// Spear and left over stacks should be on the floor.
await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1));

View File

@@ -17,23 +17,26 @@ public sealed class ContrabandTest
await client.WaitAssertion(() =>
{
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
Assert.Multiple(() =>
{
if (proto.Abstract || pair.IsTestPrototype(proto))
continue;
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
{
if (proto.Abstract || pair.IsTestPrototype(proto))
continue;
if (!proto.TryGetComponent<ContrabandComponent>(out var contraband, componentFactory))
continue;
if (!proto.TryGetComponent<ContrabandComponent>(out var contraband, componentFactory))
continue;
Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
@$"{proto.ID} has a ContrabandComponent with a unknown severity.");
Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
@$"{proto.ID} has a ContrabandComponent with a unknown severity.");
if (!severity.ShowDepartmentsAndJobs)
continue;
if (!severity.ShowDepartmentsAndJobs)
continue;
Assert.That(contraband.AllowedDepartments.Count + contraband.AllowedJobs.Count, Is.Not.EqualTo(0),
@$"{proto.ID} has a ContrabandComponent with ShowDepartmentsAndJobs but no allowed departments or jobs.");
}
Assert.That(contraband.AllowedDepartments.Count + contraband.AllowedJobs.Count, Is.Not.EqualTo(0),
@$"{proto.ID} has a ContrabandComponent with ShowDepartmentsAndJobs but no allowed departments or jobs.");
}
});
});
await pair.CleanReturnAsync();

View File

@@ -62,7 +62,10 @@ public abstract partial class InteractionTest
// Please someone purge async construction code
Task<bool> task = default!;
await Server.WaitPost(() => task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player)));
await Server.WaitPost(() =>
{
task = SConstruction.TryStartItemConstruction(prototype, SEntMan.GetEntity(Player));
});
Task? tickTask = null;
while (!task.IsCompleted)

View File

@@ -23,46 +23,49 @@ public sealed class MagazineVisualsSpriteTest
await client.WaitAssertion(() =>
{
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
Assert.Multiple(() =>
{
if (proto.Abstract || pair.IsTestPrototype(proto))
continue;
if (!proto.TryGetComponent<MagazineVisualsComponent>(out var visuals, componentFactory))
continue;
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory),
@$"{proto.ID} has MagazineVisualsComponent but no SpriteComponent.");
Assert.That(proto.HasComponent<AppearanceComponent>(componentFactory),
@$"{proto.ID} has MagazineVisualsComponent but no AppearanceComponent.");
var toTest = new List<(int, string)>();
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out var magLayerId))
toTest.Add((magLayerId, ""));
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out var magUnshadedLayerId))
toTest.Add((magUnshadedLayerId, "-unshaded"));
Assert.That(toTest, Is.Not.Empty,
@$"{proto.ID} has MagazineVisualsComponent but no Mag or MagUnshaded layer map.");
var start = visuals.ZeroVisible ? 0 : 1;
foreach (var (id, midfix) in toTest)
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
{
Assert.That(sprite.TryGetLayer(id, out var layer));
var rsi = layer.ActualRsi;
for (var i = start; i < visuals.MagSteps; i++)
{
var state = $"{visuals.MagState}{midfix}-{i}";
Assert.That(rsi.TryGetState(state, out _),
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but {rsi.Path} doesn't have state {state}!");
}
if (proto.Abstract || pair.IsTestPrototype(proto))
continue;
// MagSteps includes the 0th step, so sometimes people are off by one.
var extraState = $"{visuals.MagState}{midfix}-{visuals.MagSteps}";
Assert.That(rsi.TryGetState(extraState, out _), Is.False,
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but more states exist!");
if (!proto.TryGetComponent<MagazineVisualsComponent>(out var visuals, componentFactory))
continue;
Assert.That(proto.TryGetComponent<SpriteComponent>(out var sprite, componentFactory),
@$"{proto.ID} has MagazineVisualsComponent but no SpriteComponent.");
Assert.That(proto.HasComponent<AppearanceComponent>(componentFactory),
@$"{proto.ID} has MagazineVisualsComponent but no AppearanceComponent.");
var toTest = new List<(int, string)>();
if (sprite.LayerMapTryGet(GunVisualLayers.Mag, out var magLayerId))
toTest.Add((magLayerId, ""));
if (sprite.LayerMapTryGet(GunVisualLayers.MagUnshaded, out var magUnshadedLayerId))
toTest.Add((magUnshadedLayerId, "-unshaded"));
Assert.That(toTest, Is.Not.Empty,
@$"{proto.ID} has MagazineVisualsComponent but no Mag or MagUnshaded layer map.");
var start = visuals.ZeroVisible ? 0 : 1;
foreach (var (id, midfix) in toTest)
{
Assert.That(sprite.TryGetLayer(id, out var layer));
var rsi = layer.ActualRsi;
for (var i = start; i < visuals.MagSteps; i++)
{
var state = $"{visuals.MagState}{midfix}-{i}";
Assert.That(rsi.TryGetState(state, out _),
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but {rsi.Path} doesn't have state {state}!");
}
// MagSteps includes the 0th step, so sometimes people are off by one.
var extraState = $"{visuals.MagState}{midfix}-{visuals.MagSteps}";
Assert.That(rsi.TryGetState(extraState, out _), Is.False,
@$"{proto.ID} has MagazineVisualsComponent with MagSteps = {visuals.MagSteps}, but more states exist!");
}
}
}
});
});
await pair.CleanReturnAsync();

View File

@@ -31,7 +31,7 @@ public sealed class IdCardSystem : SharedIdCardSystem
private void OnMicrowaved(EntityUid uid, IdCardComponent component, BeingMicrowavedEvent args)
{
if (!component.CanMicrowave || !TryComp<MicrowaveComponent>(args.Microwave, out var micro) || micro.Broken)
return;
return;
if (TryComp<AccessComponent>(uid, out var access))
{
@@ -78,7 +78,12 @@ public sealed class IdCardSystem : SharedIdCardSystem
}
// Give them a wonderful new access to compensate for everything
var random = _random.Pick(_prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().ToArray());
var ids = _prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().Where(x => x.CanAddToIdCard).ToArray();
if (ids.Length == 0)
return;
var random = _random.Pick(ids);
access.Tags.Add(random.ID);
Dirty(uid, access);

View File

@@ -0,0 +1,61 @@
using Content.Server.GameTicking;
using Content.Server.Ghost;
using Content.Shared.Administration;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Robust.Server.Player;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class ForceGhostCommand : LocalizedEntityCommands
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
public override string Command => "forceghost";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length == 0 || args.Length > 1)
{
shell.WriteError(LocalizationManager.GetString("shell-wrong-arguments-number"));
return;
}
if (!_playerManager.TryGetSessionByUsername(args[0], out var player))
{
shell.WriteError(LocalizationManager.GetString("shell-target-player-does-not-exist"));
return;
}
if (!_gameTicker.PlayerGameStatuses.TryGetValue(player.UserId, out var playerStatus) ||
playerStatus is not PlayerGameStatus.JoinedGame)
{
shell.WriteLine(Loc.GetString("cmd-forceghost-error-lobby"));
return;
}
if (!_mind.TryGetMind(player, out var mindId, out var mind))
(mindId, mind) = _mind.CreateMind(player.UserId);
if (!_ghost.OnGhostAttempt(mindId, false, true, true, mind))
shell.WriteLine(Loc.GetString("cmd-forceghost-denied"));
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
CompletionHelper.SessionNames(players: _playerManager),
Loc.GetString("cmd-forceghost-hint"));
}
return CompletionResult.Empty;
}
}

View File

@@ -7,6 +7,8 @@ using Content.Shared.Database;
using Content.Shared.Roles;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Ban)]
@@ -15,6 +17,7 @@ public sealed class RoleBanCommand : IConsoleCommand
[Dependency] private readonly IPlayerLocator _locator = default!;
[Dependency] private readonly IBanManager _bans = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public string Command => "roleban";
public string Description => Loc.GetString("cmd-roleban-desc");
@@ -76,6 +79,12 @@ public sealed class RoleBanCommand : IConsoleCommand
return;
}
if (!_proto.HasIndex<JobPrototype>(job))
{
shell.WriteError(Loc.GetString("cmd-roleban-job-parse",("job", job)));
return;
}
var located = await _locator.LookupIdByNameOrIdAsync(target);
if (located == null)
{

View File

@@ -222,6 +222,7 @@ public sealed class AdminSystem : EntitySystem
var entityName = string.Empty;
var identityName = string.Empty;
// Visible (identity) name can be different from real name
if (session?.AttachedEntity != null)
{
entityName = EntityManager.GetComponent<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
@@ -230,6 +231,7 @@ public sealed class AdminSystem : EntitySystem
var antag = false;
// Starting role, antagonist status and role type
RoleTypePrototype roleType = new();
var startingRole = string.Empty;
if (_minds.TryGetMind(session, out var mindId, out var mindComp))
@@ -243,8 +245,13 @@ public sealed class AdminSystem : EntitySystem
startingRole = _jobs.MindTryGetJobName(mindId);
}
// Connection status and playtime
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
TimeSpan? overallPlaytime = null;
// Start with the last available playtime data
var cachedInfo = GetCachedPlayerInfo(data.UserId);
var overallPlaytime = cachedInfo?.OverallPlaytime;
// Overwrite with current playtime data, unless it's null (such as if the player just disconnected)
if (session != null &&
_playTime.TryGetTrackerTimes(session, out var playTimes) &&
playTimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var playTime))

View File

@@ -52,23 +52,25 @@ public sealed partial class AdminVerbSystem
var targetPlayer = targetActor.PlayerSession;
var traitorName = Loc.GetString("admin-verb-text-make-traitor");
Verb traitor = new()
{
Text = Loc.GetString("admin-verb-text-make-traitor"),
Text = traitorName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"),
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Misc/job_icons.rsi"), "Syndicate"),
Act = () =>
{
_antag.ForceMakeAntag<TraitorRuleComponent>(targetPlayer, DefaultTraitorRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-traitor"),
Message = string.Join(": ", traitorName, Loc.GetString("admin-verb-make-traitor")),
};
args.Verbs.Add(traitor);
var initialInfectedName = Loc.GetString("admin-verb-text-make-initial-infected");
Verb initialInfected = new()
{
Text = Loc.GetString("admin-verb-text-make-initial-infected"),
Text = initialInfectedName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "InitialInfected"),
Act = () =>
@@ -76,42 +78,44 @@ public sealed partial class AdminVerbSystem
_antag.ForceMakeAntag<ZombieRuleComponent>(targetPlayer, DefaultInitialInfectedRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-initial-infected"),
Message = string.Join(": ", initialInfectedName, Loc.GetString("admin-verb-make-initial-infected")),
};
args.Verbs.Add(initialInfected);
var zombieName = Loc.GetString("admin-verb-text-make-zombie");
Verb zombie = new()
{
Text = Loc.GetString("admin-verb-text-make-zombie"),
Text = zombieName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/zombie-turn.png")),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "Zombie"),
Act = () =>
{
_zombie.ZombifyEntity(args.Target);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-zombie"),
Message = string.Join(": ", zombieName, Loc.GetString("admin-verb-make-zombie")),
};
args.Verbs.Add(zombie);
var nukeOpName = Loc.GetString("admin-verb-text-make-nuclear-operative");
Verb nukeOp = new()
{
Text = Loc.GetString("admin-verb-text-make-nuclear-operative"),
Text = nukeOpName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"),
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hardsuits/syndicate.rsi"), "icon"),
Act = () =>
{
_antag.ForceMakeAntag<NukeopsRuleComponent>(targetPlayer, DefaultNukeOpRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-nuclear-operative"),
Message = string.Join(": ", nukeOpName, Loc.GetString("admin-verb-make-nuclear-operative")),
};
args.Verbs.Add(nukeOp);
var pirateName = Loc.GetString("admin-verb-text-make-pirate");
Verb pirate = new()
{
Text = Loc.GetString("admin-verb-text-make-pirate"),
Text = pirateName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"),
Act = () =>
@@ -120,13 +124,14 @@ public sealed partial class AdminVerbSystem
SetOutfitCommand.SetOutfit(args.Target, PirateGearId, EntityManager);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-pirate"),
Message = string.Join(": ", pirateName, Loc.GetString("admin-verb-make-pirate")),
};
args.Verbs.Add(pirate);
var headRevName = Loc.GetString("admin-verb-text-make-head-rev");
Verb headRev = new()
{
Text = Loc.GetString("admin-verb-text-make-head-rev"),
Text = headRevName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "HeadRevolutionary"),
Act = () =>
@@ -134,13 +139,14 @@ public sealed partial class AdminVerbSystem
_antag.ForceMakeAntag<RevolutionaryRuleComponent>(targetPlayer, DefaultRevsRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-head-rev"),
Message = string.Join(": ", headRevName, Loc.GetString("admin-verb-make-head-rev")),
};
args.Verbs.Add(headRev);
var thiefName = Loc.GetString("admin-verb-text-make-thief");
Verb thief = new()
{
Text = Loc.GetString("admin-verb-text-make-thief"),
Text = thiefName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"), "icon"),
Act = () =>
@@ -148,7 +154,7 @@ public sealed partial class AdminVerbSystem
_antag.ForceMakeAntag<ThiefRuleComponent>(targetPlayer, DefaultThiefRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-thief"),
Message = string.Join(": ", thiefName, Loc.GetString("admin-verb-make-thief")),
};
args.Verbs.Add(thief);
}

View File

@@ -149,7 +149,7 @@ public sealed partial class AdminVerbSystem
var flamesName = Loc.GetString("admin-smite-set-alight-name").ToLowerInvariant();
Verb flames = new()
{
Text = "admin-smite-set-alight-name",
Text = flamesName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Fire/fire.png")),
Act = () =>
@@ -481,7 +481,7 @@ public sealed partial class AdminVerbSystem
var breadName = Loc.GetString("admin-smite-become-bread-name").ToLowerInvariant(); // Will I get cancelled for breadName-ing you?
Verb bread = new()
{
Text = "admin-smite-kill-sign-name",
Text = breadName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/bread.rsi"), "plain"),
Act = () =>
@@ -496,7 +496,7 @@ public sealed partial class AdminVerbSystem
var mouseName = Loc.GetString("admin-smite-become-mouse-name").ToLowerInvariant();
Verb mouse = new()
{
Text = "admin-smite-cluwne-name",
Text = mouseName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/mouse.rsi"), "icon-0"),
Act = () =>
@@ -650,7 +650,7 @@ public sealed partial class AdminVerbSystem
var instrumentationName = Loc.GetString("admin-smite-become-instrument-name").ToLowerInvariant();
Verb instrumentation = new()
{
Text = "admin-smite-become-mouse-name",
Text = instrumentationName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Instruments/h_synthesizer.rsi"), "supersynth"),
Act = () =>
@@ -721,7 +721,7 @@ public sealed partial class AdminVerbSystem
var headstandName = Loc.GetString("admin-smite-headstand-name").ToLowerInvariant();
Verb headstand = new()
{
Text = "admin-smite-run-walk-swap-name",
Text = headstandName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
Act = () =>
@@ -819,7 +819,7 @@ public sealed partial class AdminVerbSystem
var superSpeedName = Loc.GetString("admin-smite-super-speed-name").ToLowerInvariant();
Verb superSpeed = new()
{
Text = "admin-smite-garbage-can-name",
Text = superSpeedName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/super_speed.png")),
Act = () =>
@@ -852,7 +852,7 @@ public sealed partial class AdminVerbSystem
args.Verbs.Add(superBonkLite);
var superBonkName = Loc.GetString("admin-smite-super-bonk-name").ToLowerInvariant();
Verb superBonk= new()
Verb superBonk = new()
{
Text = superBonkName,
Category = VerbCategory.Smite,

View File

@@ -1,13 +1,13 @@
using Content.Server.Advertise.Components;
using Content.Server.Chat.Systems;
using Content.Shared.Dataset;
using Content.Shared.Advertise.Components;
using Content.Shared.Advertise.Systems;
using Content.Shared.UserInterface;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using ActivatableUIComponent = Content.Shared.UserInterface.ActivatableUIComponent;
namespace Content.Server.Advertise;
namespace Content.Server.Advertise.EntitySystems;
public sealed partial class SpeakOnUIClosedSystem : EntitySystem
public sealed partial class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -46,13 +46,4 @@ public sealed partial class SpeakOnUIClosedSystem : EntitySystem
entity.Comp.Flag = false;
return true;
}
public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
{
if (!Resolve(entity, ref entity.Comp))
return false;
entity.Comp.Flag = value;
return true;
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Antag.Components;
using Content.Shared.GameTicking.Components;
using Content.Server.GameTicking.Rules;
namespace Content.Server.Antag;
@@ -14,9 +15,20 @@ public sealed class AntagRandomSpawnSystem : GameRuleSystem<AntagRandomSpawnComp
SubscribeLocalEvent<AntagRandomSpawnComponent, AntagSelectLocationEvent>(OnSelectLocation);
}
protected override void Added(EntityUid uid, AntagRandomSpawnComponent comp, GameRuleComponent gameRule, GameRuleAddedEvent args)
{
base.Added(uid, comp, gameRule, args);
// we have to select this here because AntagSelectLocationEvent is raised twice because MakeAntag is called twice
// once when a ghost role spawner is created and once when someone takes the ghost role
if (TryFindRandomTile(out _, out _, out _, out var coords))
comp.Coords = coords;
}
private void OnSelectLocation(Entity<AntagRandomSpawnComponent> ent, ref AntagSelectLocationEvent args)
{
if (TryFindRandomTile(out _, out _, out _, out var coords))
args.Coordinates.Add(_transform.ToMapCoordinates(coords));
if (ent.Comp.Coords != null)
args.Coordinates.Add(_transform.ToMapCoordinates(ent.Comp.Coords.Value));
}
}

View File

@@ -3,7 +3,9 @@ using System.Linq;
using Content.Server.Antag.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Objectives;
using Content.Shared.Antag;
using Content.Shared.Chat;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.Preferences;
using JetBrains.Annotations;
@@ -25,7 +27,7 @@ public sealed partial class AntagSelectionSystem
definition = null;
var totalTargetCount = GetTargetAntagCount(ent, players);
var mindCount = ent.Comp.SelectedMinds.Count;
var mindCount = ent.Comp.AssignedMinds.Count;
if (mindCount >= totalTargetCount)
return false;
@@ -95,7 +97,7 @@ public sealed partial class AntagSelectionSystem
var countOffset = 0;
foreach (var otherDef in ent.Comp.Definitions)
{
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio; // Note: Is the PlayerRatio necessary here? Seems like it can cause issues for defs with varied PlayerRatio.
}
// make sure we don't double-count the current selection
countOffset -= Math.Clamp(poolSize / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
@@ -115,7 +117,7 @@ public sealed partial class AntagSelectionSystem
return new List<(EntityUid, SessionData, string)>();
var output = new List<(EntityUid, SessionData, string)>();
foreach (var (mind, name) in ent.Comp.SelectedMinds)
foreach (var (mind, name) in ent.Comp.AssignedMinds)
{
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
continue;
@@ -137,7 +139,7 @@ public sealed partial class AntagSelectionSystem
return new();
var output = new List<Entity<MindComponent>>();
foreach (var (mind, _) in ent.Comp.SelectedMinds)
foreach (var (mind, _) in ent.Comp.AssignedMinds)
{
if (!TryComp<MindComponent>(mind, out var mindComp) || mindComp.OriginalOwnerUserId == null)
continue;
@@ -155,7 +157,7 @@ public sealed partial class AntagSelectionSystem
if (!Resolve(ent, ref ent.Comp, false))
return new();
return ent.Comp.SelectedMinds.Select(p => p.Item1).ToList();
return ent.Comp.AssignedMinds.Select(p => p.Item1).ToList();
}
/// <summary>
@@ -247,7 +249,7 @@ public sealed partial class AntagSelectionSystem
if (!Resolve(ent, ref ent.Comp, false))
return false;
return GetAliveAntagCount(ent) == ent.Comp.SelectedMinds.Count;
return GetAliveAntagCount(ent) == ent.Comp.AssignedMinds.Count;
}
/// <summary>
@@ -352,8 +354,66 @@ public sealed partial class AntagSelectionSystem
var ruleEnt = GameTicker.AddGameRule(id);
RemComp<LoadMapRuleComponent>(ruleEnt);
var antag = Comp<AntagSelectionComponent>(ruleEnt);
antag.SelectionsComplete = true; // don't do normal selection.
antag.AssignmentComplete = true; // don't do normal selection.
GameTicker.StartGameRule(ruleEnt);
return (ruleEnt, antag);
}
/// <summary>
/// Get all sessions that have been preselected for antag.
/// </summary>
/// <param name="except">A specific definition to be excluded from the check.</param>
public HashSet<ICommonSession> GetPreSelectedAntagSessions(AntagSelectionDefinition? except = null)
{
var result = new HashSet<ICommonSession>();
var query = QueryAllRules();
while (query.MoveNext(out var uid, out var comp, out _))
{
if (HasComp<EndedGameRuleComponent>(uid))
continue;
foreach (var def in comp.Definitions)
{
if (def.Equals(except))
continue;
if (comp.PreSelectedSessions.TryGetValue(def, out var set))
result.UnionWith(set);
}
}
return result;
}
/// <summary>
/// Get all sessions that have been preselected for antag and are exclusive, i.e. should not be paired with other antags.
/// </summary>
/// <param name="except">A specific definition to be excluded from the check.</param>
// Note: This is a bit iffy since technically this exclusive definition is defined via the MultiAntagSetting, while there's a separately tracked antagExclusive variable in the mindrole.
// We can't query that however since there's no guarantee the mindrole has been given out yet when checking pre-selected antags.
// I don't think there's any instance where they differ, but it's something to be aware of for a potential future refactor.
public HashSet<ICommonSession> GetPreSelectedExclusiveAntagSessions(AntagSelectionDefinition? except = null)
{
var result = new HashSet<ICommonSession>();
var query = QueryAllRules();
while (query.MoveNext(out var uid, out var comp, out _))
{
if (HasComp<EndedGameRuleComponent>(uid))
continue;
foreach (var def in comp.Definitions)
{
if (def.Equals(except))
continue;
if (def.MultiAntagSetting == AntagAcceptability.None && comp.PreSelectedSessions.TryGetValue(def, out var set))
{
result.UnionWith(set);
break;
}
}
}
return result;
}
}

View File

@@ -11,8 +11,11 @@ using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.Roles.Jobs;
using Content.Server.Shuttles.Components;
using Content.Server.Station.Events;
using Content.Shared.Administration.Logs;
using Content.Shared.Antag;
using Content.Shared.Clothing;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Ghost;
@@ -46,6 +49,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
// arbitrary random number to give late joining some mild interest.
public const float LateJoinRandomChance = 0.5f;
@@ -89,19 +93,33 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var comp, out _))
{
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn)
if (comp.SelectionTime != AntagSelectionTime.PrePlayerSpawn && comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
continue;
if (comp.SelectionsComplete)
if (comp.AssignmentComplete)
continue;
ChooseAntags((uid, comp), pool); // We choose the antags here...
if (comp.SelectionTime == AntagSelectionTime.PrePlayerSpawn)
{
AssignPreSelectedSessions((uid, comp)); // ...But only assign them if PrePlayerSpawn
foreach (var session in comp.AssignedSessions)
{
args.PlayerPool.Remove(session);
GameTicker.PlayerJoinGame(session);
}
}
}
// If IntraPlayerSpawn is selected, delayed rules should choose at this point too.
var queryDelayed = QueryDelayedRules();
while (queryDelayed.MoveNext(out var uid, out _, out var comp, out _))
{
if (comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
continue;
ChooseAntags((uid, comp), pool);
foreach (var session in comp.SelectedSessions)
{
args.PlayerPool.Remove(session);
GameTicker.PlayerJoinGame(session);
}
}
}
@@ -110,10 +128,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var comp, out _))
{
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn)
if (comp.SelectionTime != AntagSelectionTime.PostPlayerSpawn && comp.SelectionTime != AntagSelectionTime.IntraPlayerSpawn)
continue;
ChooseAntags((uid, comp), args.Players);
AssignPreSelectedSessions((uid, comp));
}
}
@@ -126,11 +145,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
// eventually this should probably store the players per definition with some kind of unique identifier.
// something to figure out later.
var query = QueryActiveRules();
var query = QueryAllRules();
var rules = new List<(EntityUid, AntagSelectionComponent)>();
while (query.MoveNext(out var uid, out _, out var antag, out _))
while (query.MoveNext(out var uid, out var antag, out _))
{
rules.Add((uid, antag));
if (HasComp<ActiveGameRuleComponent>(uid) ||
(HasComp<DelayedStartRuleComponent>(uid) && antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn)) //IntraPlayerSpawn selects antags before spawning, but doesn't activate until after.
rules.Add((uid, antag));
}
RobustRandom.Shuffle(rules);
@@ -142,7 +163,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!antag.Definitions.Any(p => p.LateJoinAdditional))
continue;
DebugTools.AssertEqual(antag.SelectionTime, AntagSelectionTime.PostPlayerSpawn);
DebugTools.AssertNotEqual(antag.SelectionTime, AntagSelectionTime.PrePlayerSpawn);
// do not count players in the lobby for the antag ratio
var players = _playerManager.NetworkedSessions.Count(x => x.AttachedEntity != null);
@@ -150,7 +171,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!TryGetNextAvailableDefinition((uid, antag), out var def, players))
continue;
if (TryMakeAntag((uid, antag), args.Player, def.Value))
var onlyPreSelect = (antag.SelectionTime == AntagSelectionTime.IntraPlayerSpawn && !antag.AssignmentComplete); // Don't wanna give them antag status if the rule hasn't assigned its existing ones yet
if (TryMakeAntag((uid, antag), args.Player, def.Value, onlyPreSelect: onlyPreSelect))
break;
}
}
@@ -183,14 +206,20 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (GameTicker.RunLevel != GameRunLevel.InRound)
return;
if (component.SelectionsComplete)
if (component.AssignmentComplete)
return;
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) && status == PlayerGameStatus.JoinedGame)
.ToList();
if (!component.PreSelectionsComplete)
{
var players = _playerManager.Sessions
.Where(x => GameTicker.PlayerGameStatuses.TryGetValue(x.UserId, out var status) &&
status == PlayerGameStatus.JoinedGame)
.ToList();
ChooseAntags((uid, component), players, midround: true);
ChooseAntags((uid, component), players, midround: true);
}
AssignPreSelectedSessions((uid, component));
}
/// <summary>
@@ -201,7 +230,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
{
if (ent.Comp.SelectionsComplete)
if (ent.Comp.PreSelectionsComplete)
return;
foreach (var def in ent.Comp.Definitions)
@@ -209,7 +238,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
ChooseAntags(ent, pool, def, midround: midround);
}
ent.Comp.SelectionsComplete = true;
ent.Comp.PreSelectionsComplete = true;
}
/// <summary>
@@ -250,21 +279,53 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
break;
}
if (session != null && ent.Comp.SelectedSessions.Contains(session))
if (session != null && ent.Comp.PreSelectedSessions.Values.Any(x => x.Contains(session)))
{
Log.Warning($"Somehow picked {session} for an antag when this rule already selected them previously");
continue;
}
}
MakeAntag(ent, session, def);
if (session == null)
MakeAntag(ent, null, def); // This is for spawner antags
else
{
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
set.Add(session); // Selection done!
Log.Debug($"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
}
}
}
/// <summary>
/// Assigns antag roles to sessions selected for it.
/// </summary>
public void AssignPreSelectedSessions(Entity<AntagSelectionComponent> ent)
{
// Only assign if there's been a pre-selection, and the selection hasn't already been made
if (!ent.Comp.PreSelectionsComplete || ent.Comp.AssignmentComplete)
return;
foreach (var def in ent.Comp.Definitions)
{
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
continue;
foreach (var session in set)
{
TryMakeAntag(ent, session, def);
}
}
ent.Comp.AssignmentComplete = true;
}
/// <summary>
/// Tries to makes a given player into the specified antagonist.
/// </summary>
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true)
public bool TryMakeAntag(Entity<AntagSelectionComponent> ent, ICommonSession? session, AntagSelectionDefinition def, bool ignoreSpawner = false, bool checkPref = true, bool onlyPreSelect = false)
{
if (checkPref && !HasPrimaryAntagPreference(session, def))
return false;
@@ -272,7 +333,19 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (!IsSessionValid(ent, session, def) || !IsEntityValid(session?.AttachedEntity, def))
return false;
MakeAntag(ent, session, def, ignoreSpawner);
if (onlyPreSelect && session != null)
{
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
set.Add(session);
Log.Debug($"Pre-selected {session!.Name} as antagonist: {ToPrettyString(ent)}");
_adminLogger.Add(LogType.AntagSelection, $"Pre-selected {session.Name} as antagonist: {ToPrettyString(ent)}");
}
else
{
MakeAntag(ent, session, def, ignoreSpawner);
}
return true;
}
@@ -286,7 +359,10 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session != null)
{
ent.Comp.SelectedSessions.Add(session);
if (!ent.Comp.PreSelectedSessions.TryGetValue(def, out var set))
ent.Comp.PreSelectedSessions.Add(def, set = new HashSet<ICommonSession>());
set.Add(session);
ent.Comp.AssignedSessions.Add(session);
// we shouldn't be blocking the entity if they're just a ghost or smth.
if (!HasComp<GhostComponent>(session.AttachedEntity))
@@ -309,10 +385,19 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
Log.Error($"Attempted to make {session} antagonist in gamerule {ToPrettyString(ent)} but there was no valid entity for player.");
if (session != null)
ent.Comp.SelectedSessions.Remove(session);
{
ent.Comp.AssignedSessions.Remove(session);
ent.Comp.PreSelectedSessions[def].Remove(session);
}
return;
}
// TODO: This is really messy because this part runs twice for midround events.
// Once when the ghostrole spawner is created and once when a player takes it.
// Therefore any component subscribing to this has to make sure both subscriptions return the same value
// or the ghost role raffle location preview will be wrong.
var getPosEv = new AntagSelectLocationEvent(session, ent);
RaiseLocalEvent(ent, ref getPosEv, true);
if (getPosEv.Handled)
@@ -330,7 +415,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
if (session != null)
ent.Comp.SelectedSessions.Remove(session);
{
ent.Comp.AssignedSessions.Remove(session);
ent.Comp.PreSelectedSessions[def].Remove(session);
}
return;
}
@@ -363,10 +452,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
_role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
ent.Comp.AssignedMinds.Add((curMind.Value, Name(player)));
SendBriefing(session, def.Briefing);
Log.Debug($"Selected {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
Log.Debug($"Assigned {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
_adminLogger.Add(LogType.AntagSelection, $"Assigned {ToPrettyString(curMind)} as antagonist: {ToPrettyString(ent)}");
}
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
@@ -412,15 +502,11 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session.Status is SessionStatus.Disconnected or SessionStatus.Zombie)
return false;
if (ent.Comp.SelectedSessions.Contains(session))
if (ent.Comp.AssignedSessions.Contains(session))
return false;
mind ??= session.GetMind();
// If the player has not spawned in as any entity (e.g., in the lobby), they can be given an antag role/entity.
if (mind == null)
return true;
//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)
@@ -429,12 +515,16 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
{
if (_role.MindIsAntagonist(mind))
return false;
if (GetPreSelectedAntagSessions(def).Contains(session)) // Used for rules where the antag has been selected, but not started yet
return false;
break;
}
case AntagAcceptability.NotExclusive:
{
if (_role.MindIsExclusiveAntagonist(mind))
return false;
if (GetPreSelectedExclusiveAntagSessions(def).Contains(session))
return false;
break;
}
}
@@ -481,7 +571,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (ent.Comp.AgentName is not { } name)
return;
args.Minds = ent.Comp.SelectedMinds;
args.Minds = ent.Comp.AssignedMinds;
args.AgentName = Loc.GetString(name);
}
}

View File

@@ -1,3 +1,5 @@
using Robust.Shared.Map;
namespace Content.Server.Antag.Components;
/// <summary>
@@ -5,4 +7,11 @@ namespace Content.Server.Antag.Components;
/// Requires <see cref="AntagSelectionComponent"/>.
/// </summary>
[RegisterComponent]
public sealed partial class AntagRandomSpawnComponent : Component;
public sealed partial class AntagRandomSpawnComponent : Component
{
/// <summary>
/// Location that was picked.
/// </summary>
[DataField]
public EntityCoordinates? Coords;
}

View File

@@ -14,10 +14,16 @@ namespace Content.Server.Antag.Components;
public sealed partial class AntagSelectionComponent : Component
{
/// <summary>
/// Has the primary selection of antagonists finished yet?
/// Has the primary assignment of antagonists finished yet?
/// </summary>
[DataField]
public bool SelectionsComplete;
public bool AssignmentComplete;
/// <summary>
/// Has the antagonists been preselected but yet to be fully assigned?
/// </summary>
[DataField]
public bool PreSelectionsComplete;
/// <summary>
/// The definitions for the antagonists
@@ -26,10 +32,10 @@ public sealed partial class AntagSelectionComponent : Component
public List<AntagSelectionDefinition> Definitions = new();
/// <summary>
/// The minds and original names of the players selected to be antagonists.
/// The minds and original names of the players assigned to be antagonists.
/// </summary>
[DataField]
public List<(EntityUid, string)> SelectedMinds = new();
public List<(EntityUid, string)> AssignedMinds = new();
/// <summary>
/// When the antag selection will occur.
@@ -37,11 +43,17 @@ public sealed partial class AntagSelectionComponent : Component
[DataField]
public AntagSelectionTime SelectionTime = AntagSelectionTime.PostPlayerSpawn;
/// <summary>
/// Cached sessions of antag definitions and selected players. Players in this dict are not guaranteed to have been assigned the role yet.
/// </summary>
[DataField]
public Dictionary<AntagSelectionDefinition, HashSet<ICommonSession>>PreSelectedSessions = new();
/// <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();
public HashSet<ICommonSession> AssignedSessions = new();
/// <summary>
/// Locale id for the name of the antag.

View File

@@ -1,11 +1,9 @@
using Content.Server.Power.Components;
using Content.Shared.UserInterface;
using Content.Server.Advertise;
using Content.Server.Advertise.Components;
using Content.Server.Advertise.EntitySystems;
using Content.Shared.Advertise.Components;
using Content.Shared.Arcade;
using Content.Shared.Power;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
namespace Content.Server.Arcade.BlockGame;

View File

@@ -1,9 +1,9 @@
using Content.Server.Power.Components;
using Content.Shared.UserInterface;
using Content.Server.Advertise;
using Content.Server.Advertise.Components;
using Content.Server.Advertise.EntitySystems;
using Content.Shared.Advertise.Components;
using Content.Shared.Arcade;
using Content.Shared.Power;
using static Content.Shared.Arcade.SharedSpaceVillainArcadeComponent;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
@@ -24,7 +24,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
SubscribeLocalEvent<SpaceVillainArcadeComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<SpaceVillainArcadeComponent, AfterActivatableUIOpenEvent>(OnAfterUIOpenSV);
SubscribeLocalEvent<SpaceVillainArcadeComponent, SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
SubscribeLocalEvent<SpaceVillainArcadeComponent, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage>(OnSVPlayerAction);
SubscribeLocalEvent<SpaceVillainArcadeComponent, PowerChangedEvent>(OnSVillainPower);
}
@@ -70,7 +70,7 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
component.RewardAmount = new Random().Next(component.RewardMinAmount, component.RewardMaxAmount + 1);
}
private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SpaceVillainArcadePlayerActionMessage msg)
private void OnSVPlayerAction(EntityUid uid, SpaceVillainArcadeComponent component, SharedSpaceVillainArcadeComponent.SpaceVillainArcadePlayerActionMessage msg)
{
if (component.Game == null)
return;
@@ -79,22 +79,22 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
switch (msg.PlayerAction)
{
case PlayerAction.Attack:
case PlayerAction.Heal:
case PlayerAction.Recharge:
case SharedSpaceVillainArcadeComponent.PlayerAction.Attack:
case SharedSpaceVillainArcadeComponent.PlayerAction.Heal:
case SharedSpaceVillainArcadeComponent.PlayerAction.Recharge:
component.Game.ExecutePlayerAction(uid, msg.PlayerAction, component);
// Any sort of gameplay action counts
if (TryComp<SpeakOnUIClosedComponent>(uid, out var speakComponent))
_speakOnUIClosed.TrySetFlag((uid, speakComponent));
break;
case PlayerAction.NewGame:
case SharedSpaceVillainArcadeComponent.PlayerAction.NewGame:
_audioSystem.PlayPvs(component.NewGameSound, uid, AudioParams.Default.WithVolume(-4f));
component.Game = new SpaceVillainGame(uid, component, this);
_uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
_uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
break;
case PlayerAction.RequestData:
_uiSystem.ServerSendUiMessage(uid, SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
case SharedSpaceVillainArcadeComponent.PlayerAction.RequestData:
_uiSystem.ServerSendUiMessage(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key, component.Game.GenerateMetaDataMessage());
break;
}
}
@@ -109,6 +109,6 @@ public sealed partial class SpaceVillainArcadeSystem : EntitySystem
if (TryComp<ApcPowerReceiverComponent>(uid, out var power) && power.Powered)
return;
_uiSystem.CloseUi(uid, SpaceVillainArcadeUiKey.Key);
_uiSystem.CloseUi(uid, SharedSpaceVillainArcadeComponent.SpaceVillainArcadeUiKey.Key);
}
}

View File

@@ -1,7 +1,6 @@
using Content.Server.Body.Components;
using Content.Server.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Popups;
using Content.Shared.Alert;
using Content.Shared.Chemistry.Components;
@@ -40,7 +39,6 @@ public sealed class BloodstreamSystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
public override void Initialize()
{
@@ -193,17 +191,8 @@ public sealed class BloodstreamSystem : EntitySystem
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
// Ensure blood that should have DNA has it; must be run here, in case DnaComponent has not yet been initialized
if (TryComp<DnaComponent>(entity.Owner, out var donorComp) && donorComp.DNA == String.Empty)
{
donorComp.DNA = _forensicsSystem.GenerateDNA();
var ev = new GenerateDnaEvent { Owner = entity.Owner, DNA = donorComp.DNA };
RaiseLocalEvent(entity.Owner, ref ev);
}
// Fill blood solution with BLOOD
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
}
@@ -492,6 +481,8 @@ public sealed class BloodstreamSystem : EntitySystem
reagentData.AddRange(GetEntityBloodData(entity.Owner));
}
}
else
Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
}
/// <summary>
@@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem
var bloodData = new List<ReagentData>();
var dnaData = new DnaData();
if (TryComp<DnaComponent>(uid, out var donorComp))
{
if (TryComp<DnaComponent>(uid, out var donorComp) && donorComp.DNA != null)
dnaData.DNA = donorComp.DNA;
} else
{
else
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
}
bloodData.Add(dnaData);

View File

@@ -62,7 +62,7 @@ namespace Content.Server.Cargo.Systems
return;
_audio.PlayPvs(component.ConfirmSound, uid);
UpdateBankAccount(stationUid.Value, bank, (int) price);
UpdateBankAccount((stationUid.Value, bank), (int) price);
QueueDel(args.Used);
args.Handled = true;
}
@@ -103,7 +103,7 @@ namespace Content.Server.Cargo.Systems
while (stationQuery.MoveNext(out var uid, out var bank))
{
var balanceToAdd = bank.IncreasePerSecond * Delay;
UpdateBankAccount(uid, bank, balanceToAdd);
UpdateBankAccount((uid, bank), balanceToAdd);
}
var query = EntityQueryEnumerator<CargoOrderConsoleComponent>();
@@ -229,7 +229,7 @@ namespace Content.Server.Cargo.Systems
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bank.Balance}");
orderDatabase.Orders.Remove(order);
UpdateBankAccount(station.Value, bank, -cost);
UpdateBankAccount((station.Value, bank), -cost);
UpdateOrders(station.Value);
}

View File

@@ -76,19 +76,23 @@ public sealed partial class CargoSystem : SharedCargoSystem
}
[PublicAPI]
public void UpdateBankAccount(EntityUid uid, StationBankAccountComponent component, int balanceAdded)
public void UpdateBankAccount(Entity<StationBankAccountComponent?> ent, int balanceAdded)
{
component.Balance += balanceAdded;
var query = EntityQueryEnumerator<BankClientComponent, TransformComponent>();
if (!Resolve(ent, ref ent.Comp))
return;
var ev = new BankBalanceUpdatedEvent(uid, component.Balance);
ent.Comp.Balance += balanceAdded;
var ev = new BankBalanceUpdatedEvent(ent, ent.Comp.Balance);
var query = EntityQueryEnumerator<BankClientComponent, TransformComponent>();
while (query.MoveNext(out var client, out var comp, out var xform))
{
var station = _station.GetOwningStation(client, xform);
if (station != uid)
if (station != ent)
continue;
comp.Balance = component.Balance;
comp.Balance = ent.Comp.Balance;
Dirty(client, comp);
RaiseLocalEvent(client, ref ev);
}

View File

@@ -9,13 +9,13 @@ namespace Content.Server.Cloning
{
private readonly EntityUid _mindId;
private readonly MindComponent _mind;
private readonly CloningSystem _cloningSystem;
private readonly CloningPodSystem _cloningPodSystem;
public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningSystem cloningSys)
public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningPodSystem cloningPodSys)
{
_mindId = mindId;
_mind = mind;
_cloningSystem = cloningSys;
_cloningPodSystem = cloningPodSys;
}
public override void HandleMessage(EuiMessageBase msg)
@@ -29,7 +29,7 @@ namespace Content.Server.Cloning
return;
}
_cloningSystem.TransferMindToClone(_mindId, _mind);
_cloningPodSystem.TransferMindToClone(_mindId, _mind);
Close();
}
}

View File

@@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Cloning.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.Medical.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.UserInterface;
using Content.Shared.Cloning;
@@ -16,19 +15,17 @@ using Content.Shared.Mind;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Power;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Server.Player;
namespace Content.Server.Cloning
{
[UsedImplicitly]
public sealed class CloningConsoleSystem : EntitySystem
{
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly CloningSystem _cloningSystem = default!;
[Dependency] private readonly CloningPodSystem _cloningPodSystem = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
@@ -171,7 +168,7 @@ namespace Content.Server.Cloning
if (mind.UserId.HasValue == false || mind.Session == null)
return;
if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
if (_cloningPodSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
}

View File

@@ -0,0 +1,323 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Systems;
using Content.Server.Cloning.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.EUI;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Materials;
using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Cloning;
using Content.Shared.Damage;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Systems;
using Robust.Server.Containers;
using Robust.Server.Player;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Cloning;
public sealed class CloningPodSystem : EntitySystem
{
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly EuiManager _euiManager = null!;
[Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly MaterialStorageSystem _material = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly CloningSystem _cloning = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
public readonly ProtoId<CloningSettingsPrototype> SettingsId = "CloningPod";
public const float EasyModeCloningCost = 0.7f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
}
private void OnComponentInit(Entity<CloningPodComponent> ent, ref ComponentInit args)
{
ent.Comp.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(ent.Owner, "clonepod-bodyContainer");
_signalSystem.EnsureSinkPorts(ent.Owner, ent.Comp.PodPort);
}
internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
{
if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
!EntityManager.EntityExists(entity) ||
!TryComp<MindContainerComponent>(entity, out var mindComp) ||
mindComp.Mind != null)
return;
_mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
_mindSystem.UnVisit(mindId, mind);
ClonesWaitingForMind.Remove(mind);
}
private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
{
if (clonedComponent.Parent == EntityUid.Invalid ||
!EntityManager.EntityExists(clonedComponent.Parent) ||
!TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
{
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
return;
}
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
}
private void OnPortDisconnected(Entity<CloningPodComponent> ent, ref PortDisconnectedEvent args)
{
ent.Comp.ConnectedConsole = null;
}
private void OnAnchor(Entity<CloningPodComponent> ent, ref AnchorStateChangedEvent args)
{
if (ent.Comp.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(ent.Comp.ConnectedConsole, out var console))
return;
if (args.Anchored)
{
_cloningConsoleSystem.RecheckConnections(ent.Comp.ConnectedConsole.Value, ent.Owner, console.GeneticScanner, console);
return;
}
_cloningConsoleSystem.UpdateUserInterface(ent.Comp.ConnectedConsole.Value, console);
}
private void OnExamined(Entity<CloningPodComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(ent.Owner))
return;
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(ent.Owner, ent.Comp.RequiredMaterial))));
}
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
{
if (!Resolve(uid, ref clonePod))
return false;
if (HasComp<ActiveCloningPodComponent>(uid))
return false;
var mind = mindEnt.Comp;
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
{
if (EntityManager.EntityExists(clone) &&
!_mobStateSystem.IsDead(clone) &&
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
return false; // Mind already has clone
ClonesWaitingForMind.Remove(mind);
}
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
return false; // Body controlled by mind is not dead
// Yes, we still need to track down the client because we need to open the Eui
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
return false;
var cloningCost = (int)Math.Round(physics.FixturesMass);
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
cloningCost = (int)Math.Round(cloningCost * EasyModeCloningCost);
// biomass checks
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
if (biomassAmount < cloningCost)
{
if (clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
return false;
}
// end of biomass checks
// genetic damage checks
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
{
var chance = Math.Clamp((float)(cellularDmg / 100), 0, 1);
chance *= failChanceModifier;
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
if (_robustRandom.Prob(chance))
{
clonePod.FailedClone = true;
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
AddComp<ActiveCloningPodComponent>(uid);
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
clonePod.UsedBiomass = cloningCost;
return true;
}
}
// end of genetic damage checks
if (!_cloning.TryCloning(bodyToClone, _transformSystem.GetMapCoordinates(bodyToClone), SettingsId, out var mob)) // spawn a new body
{
if (clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-uncloneable-trait-error"), InGameICChatType.Speak, false);
return false;
}
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob.Value);
cloneMindReturn.Mind = mind;
cloneMindReturn.Parent = uid;
_containerSystem.Insert(mob.Value, clonePod.BodyContainer);
ClonesWaitingForMind.Add(mind, mob.Value);
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
AddComp<ActiveCloningPodComponent>(uid);
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
clonePod.UsedBiomass = cloningCost;
return true;
}
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
{
cloningPod.Status = status;
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
while (query.MoveNext(out var uid, out var _, out var cloning))
{
if (!_powerReceiverSystem.IsPowered(uid))
continue;
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
continue;
cloning.CloningProgress += frameTime;
if (cloning.CloningProgress < cloning.CloningTime)
continue;
if (cloning.FailedClone)
EndFailedCloning(uid, cloning);
else
Eject(uid, cloning);
}
}
/// <summary>
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
/// </summary>
private void OnEmagged(Entity<CloningPodComponent> ent, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(ent.Owner, EmagType.Interaction))
return;
if (!this.IsPowered(ent.Owner, EntityManager))
return;
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), ent.Owner);
args.Handled = true;
}
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
{
if (!Resolve(uid, ref clonePod))
return;
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
return;
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
_containerSystem.Remove(entity, clonePod.BodyContainer);
clonePod.CloningProgress = 0f;
clonePod.UsedBiomass = 0;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
{
clonePod.FailedClone = false;
clonePod.CloningProgress = 0f;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
var transform = Transform(uid);
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
if (HasComp<EmaggedComponent>(uid))
{
_audio.PlayPvs(clonePod.ScreamSound, uid);
Spawn(clonePod.MobSpawnId, transform.Coordinates);
}
Solution bloodSolution = new();
var i = 0;
while (i < 1)
{
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
bloodSolution.AddReagent("Blood", 50);
if (_robustRandom.Prob(0.2f))
i++;
}
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
if (!HasComp<EmaggedComponent>(uid))
{
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int)(clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
}
clonePod.UsedBiomass = 0;
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
public void Reset(RoundRestartCleanupEvent ev)
{
ClonesWaitingForMind.Clear();
}
}

View File

@@ -1,350 +1,123 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Systems;
using Content.Server.Cloning.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.EUI;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Humanoid;
using Content.Server.Jobs;
using Content.Server.Materials;
using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.CCVar;
using Content.Shared.Chemistry.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Cloning;
using Content.Shared.Damage;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Cloning.Events;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles.Jobs;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Physics.Components;
using Content.Shared.Inventory;
using Content.Shared.NameModifier.Components;
using Content.Shared.StatusEffect;
using Content.Shared.Whitelist;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.Cloning
namespace Content.Server.Cloning;
/// <summary>
/// System responsible for making a copy of a humanoid's body.
/// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
/// </summary>
public sealed class CloningSystem : EntitySystem
{
public sealed class CloningSystem : EntitySystem
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
/// <summary>
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
/// </summary>
public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId<CloningSettingsPrototype> settingsId, [NotNullWhen(true)] out EntityUid? clone)
{
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly EuiManager _euiManager = null!;
[Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly ContainerSystem _containerSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly PuddleSystem _puddleSystem = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly MaterialStorageSystem _material = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly EmagSystem _emag = default!;
clone = null;
if (!_prototype.TryIndex(settingsId, out var settings))
return false; // invalid settings
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
public const float EasyModeCloningCost = 0.7f;
if (!TryComp<HumanoidAppearanceComponent>(original, out var humanoid))
return false; // whatever body was to be cloned, was not a humanoid
public override void Initialize()
if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
return false; // invalid species
var attemptEv = new CloningAttemptEvent(settings);
RaiseLocalEvent(original, ref attemptEv);
if (attemptEv.Cancelled && !settings.ForceCloning)
return false; // cannot clone, for example due to the unrevivable trait
clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
_humanoidSystem.CloneAppearance(original, clone.Value);
var componentsToCopy = settings.Components;
// don't make status effects permanent
if (TryComp<StatusEffectsComponent>(original, out var statusComp))
componentsToCopy.ExceptWith(statusComp.ActiveEffects.Values.Select(s => s.RelevantComponent).Where(s => s != null)!);
foreach (var componentName in componentsToCopy)
{
base.Initialize();
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
}
private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
{
clonePod.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, "clonepod-bodyContainer");
_signalSystem.EnsureSinkPorts(uid, CloningPodComponent.PodPort);
}
internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
{
if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
!EntityManager.EntityExists(entity) ||
!TryComp<MindContainerComponent>(entity, out var mindComp) ||
mindComp.Mind != null)
return;
_mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind);
_mindSystem.UnVisit(mindId, mind);
ClonesWaitingForMind.Remove(mind);
}
private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message)
{
if (clonedComponent.Parent == EntityUid.Invalid ||
!EntityManager.EntityExists(clonedComponent.Parent) ||
!TryComp<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
{
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
return;
}
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
}
private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args)
{
pod.ConnectedConsole = null;
}
private void OnAnchor(EntityUid uid, CloningPodComponent component, ref AnchorStateChangedEvent args)
{
if (component.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(component.ConnectedConsole, out var console))
return;
if (args.Anchored)
{
_cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, uid, console.GeneticScanner, console);
return;
}
_cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console);
}
private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid))
return;
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(uid, component.RequiredMaterial))));
}
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
{
if (!Resolve(uid, ref clonePod))
return false;
if (HasComp<ActiveCloningPodComponent>(uid))
return false;
var mind = mindEnt.Comp;
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
{
if (EntityManager.EntityExists(clone) &&
!_mobStateSystem.IsDead(clone) &&
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
return false; // Mind already has clone
ClonesWaitingForMind.Remove(mind);
Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
continue;
}
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
return false; // Body controlled by mind is not dead
// Yes, we still need to track down the client because we need to open the Eui
if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client))
return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad.
if (!TryComp<HumanoidAppearanceComponent>(bodyToClone, out var humanoid))
return false; // whatever body was to be cloned, was not a humanoid
if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
return false;
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
return false;
var cloningCost = (int) Math.Round(physics.FixturesMass);
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost);
// biomass checks
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
if (biomassAmount < cloningCost)
if (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
{
if (clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
return false;
}
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
clonePod.UsedBiomass = cloningCost;
// end of biomass checks
// genetic damage checks
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
{
var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1);
chance *= failChanceModifier;
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
if (_robustRandom.Prob(chance))
{
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
clonePod.FailedClone = true;
AddComp<ActiveCloningPodComponent>(uid);
return true;
}
}
// end of genetic damage checks
var mob = Spawn(speciesPrototype.Prototype, _transformSystem.GetMapCoordinates(uid));
_humanoidSystem.CloneAppearance(bodyToClone, mob);
var ev = new CloningEvent(bodyToClone, mob);
RaiseLocalEvent(bodyToClone, ref ev);
if (!ev.NameHandled)
_metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob);
cloneMindReturn.Mind = mind;
cloneMindReturn.Parent = uid;
_containerSystem.Insert(mob, clonePod.BodyContainer);
ClonesWaitingForMind.Add(mind, mob);
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
AddComp<ActiveCloningPodComponent>(uid);
// TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
// Add on special job components to the mob.
if (_jobs.MindTryGetJob(mindEnt, out var prototype))
{
foreach (var special in prototype.Special)
{
if (special is AddComponentSpecial)
special.AfterEquip(mob);
}
}
return true;
}
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
{
cloningPod.Status = status;
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
while (query.MoveNext(out var uid, out var _, out var cloning))
{
if (!_powerReceiverSystem.IsPowered(uid))
continue;
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
continue;
cloning.CloningProgress += frameTime;
if (cloning.CloningProgress < cloning.CloningTime)
continue;
if (cloning.FailedClone)
EndFailedCloning(uid, cloning);
else
Eject(uid, cloning);
if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
RemComp(clone.Value, componentRegistration.Type);
CopyComp(original, clone.Value, sourceComp);
}
}
/// <summary>
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
/// </summary>
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
var cloningEv = new CloningEvent(settings, clone.Value);
RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
// Add equipment first so that SetEntityName also renames the ID card.
if (settings.CopyEquipment != null)
CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
var originalName = Name(original);
if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
originalName = nameModComp.BaseName;
// This will properly set the BaseName and EntityName for the clone.
// Adding the component first before renaming will make sure RefreshNameModifers is called.
// Without this the name would get reverted to Urist.
// If the clone has no name modifiers, NameModifierComponent will be removed again.
EnsureComp<NameModifierComponent>(clone.Value);
_metaData.SetEntityName(clone.Value, originalName);
_adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
return true;
}
/// <summary>
/// Copies the equipment the original has to the clone.
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
/// </summary>
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
return;
// Iterate over all inventory slots
var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
while (slotEnumerator.NextItem(out var item, out var slot))
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
// Spawn a copy of the item using the original prototype.
// This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
// we use a whitelist and blacklist to be sure to exclude any problematic entities
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
continue;
if (!this.IsPowered(uid, EntityManager))
return;
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
args.Handled = true;
}
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
{
if (!Resolve(uid, ref clonePod))
return;
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
return;
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
_containerSystem.Remove(entity, clonePod.BodyContainer);
clonePod.CloningProgress = 0f;
clonePod.UsedBiomass = 0;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
{
clonePod.FailedClone = false;
clonePod.CloningProgress = 0f;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
var transform = Transform(uid);
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
if (_emag.CheckFlag(uid, EmagType.Interaction))
{
_audio.PlayPvs(clonePod.ScreamSound, uid);
Spawn(clonePod.MobSpawnId, transform.Coordinates);
}
Solution bloodSolution = new();
var i = 0;
while (i < 1)
{
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
bloodSolution.AddReagent("Blood", 50);
if (_robustRandom.Prob(0.2f))
i++;
}
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
if (!_emag.CheckFlag(uid, EmagType.Interaction))
{
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
}
clonePod.UsedBiomass = 0;
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
public void Reset(RoundRestartCleanupEvent ev)
{
ClonesWaitingForMind.Clear();
var prototype = MetaData(item).EntityPrototype;
if (prototype != null)
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
}
}
}

View File

@@ -0,0 +1,17 @@
using Content.Shared.Cloning;
using Robust.Shared.Prototypes;
namespace Content.Server.Cloning.Components;
/// <summary>
/// This is added to a marker entity in order to spawn a clone of a random player.
/// </summary>
[RegisterComponent, EntityCategory("Spawner")]
public sealed partial class RandomCloneSpawnerComponent : Component
{
/// <summary>
/// Cloning settings to be used.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
}

View File

@@ -0,0 +1,47 @@
using Content.Server.Cloning.Components;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Cloning;
/// <summary>
/// This deals with spawning and setting up a clone of a random crew member.
/// </summary>
public sealed class RandomCloneSpawnerSystem : EntitySystem
{
[Dependency] private readonly CloningSystem _cloning = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RandomCloneSpawnerComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<RandomCloneSpawnerComponent> ent, ref MapInitEvent args)
{
QueueDel(ent.Owner);
if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
{
Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for RandomCloneSpawner");
return;
}
var allHumans = _mind.GetAliveHumans();
if (allHumans.Count == 0)
return;
var bodyToClone = _random.Pick(allHumans).Comp.OwnedEntity;
if (bodyToClone != null)
_cloning.TryCloning(bodyToClone.Value, _transformSystem.GetMapCoordinates(ent.Owner), settings, out _);
}
}

View File

@@ -0,0 +1,51 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Delivery;
/// <summary>
/// Component given to a station to indicate it can have deliveries spawn on it.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class CargoDeliveryDataComponent : Component
{
/// <summary>
/// The time at which the next delivery will spawn.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextDelivery;
/// <summary>
/// Minimum cooldown after a delivery spawns.
/// </summary>
[DataField]
public TimeSpan MinDeliveryCooldown = TimeSpan.FromMinutes(3);
/// <summary>
/// Maximum cooldown after a delivery spawns.
/// </summary>
[DataField]
public TimeSpan MaxDeliveryCooldown = TimeSpan.FromMinutes(7);
/// <summary>
/// The ratio at which deliveries will spawn, based on the amount of people in the crew manifest.
/// 1 delivery per X players.
/// </summary>
[DataField]
public float PlayerToDeliveryRatio = 7f;
/// <summary>
/// The minimum amount of deliveries that will spawn.
/// This is not per spawner unless DistributeRandomly is false.
/// </summary>
[DataField]
public int MinimumDeliverySpawn = 1;
/// <summary>
/// Should deliveries be randomly split between spawners?
/// If true, the amount of deliveries will be spawned randomly across all spawners.
/// If false, an amount of mail based on PlayerToDeliveryRatio will be spawned on all spawners.
/// </summary>
[DataField]
public bool DistributeRandomly = true;
}

View File

@@ -0,0 +1,131 @@
using Content.Server.Power.EntitySystems;
using Content.Server.StationRecords;
using Content.Shared.Delivery;
using Content.Shared.EntityTable;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Delivery;
/// <summary>
/// System for managing deliveries spawned by the mail teleporter.
/// This covers for spawning deliveries.
/// </summary>
public sealed partial class DeliverySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityTableSystem _entityTable = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!;
private void InitializeSpawning()
{
SubscribeLocalEvent<CargoDeliveryDataComponent, MapInitEvent>(OnDataMapInit);
}
private void OnDataMapInit(Entity<CargoDeliveryDataComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextDelivery = _timing.CurTime + ent.Comp.MinDeliveryCooldown; // We want an early wave of mail so cargo doesn't have to wait
}
private void SpawnDelivery(Entity<DeliverySpawnerComponent?> ent, int amount)
{
if (!Resolve(ent.Owner, ref ent.Comp))
return;
var coords = Transform(ent).Coordinates;
_audio.PlayPvs(ent.Comp.SpawnSound, ent.Owner);
for (int i = 0; i < amount; i++)
{
var spawns = _entityTable.GetSpawns(ent.Comp.Table);
foreach (var id in spawns)
{
Spawn(id, coords);
}
}
}
private void SpawnStationDeliveries(Entity<CargoDeliveryDataComponent> ent)
{
if (!TryComp<StationRecordsComponent>(ent, out var records))
return;
var spawners = GetValidSpawners(ent);
// Skip if theres no spawners available
if (spawners.Count == 0)
return;
// Skip if there's nobody in crew manifest
if (records.Records.Keys.Count == 0)
return;
// We take the amount of mail calculated based on player amount or the minimum, whichever is higher.
// We don't want stations with less than the player ratio to not get mail at all
var initialDeliveryCount = (int)Math.Ceiling(records.Records.Keys.Count / ent.Comp.PlayerToDeliveryRatio);
var deliveryCount = Math.Max(initialDeliveryCount, ent.Comp.MinimumDeliverySpawn);
if (!ent.Comp.DistributeRandomly)
{
foreach (var spawner in spawners)
{
SpawnDelivery(spawner, deliveryCount);
}
}
else
{
int[] amounts = new int[spawners.Count];
// Distribute items randomly
for (int i = 0; i < deliveryCount; i++)
{
var randomListIndex = _random.Next(spawners.Count);
amounts[randomListIndex]++;
}
for (int j = 0; j < spawners.Count; j++)
{
SpawnDelivery(spawners[j], amounts[j]);
}
}
}
private List<EntityUid> GetValidSpawners(Entity<CargoDeliveryDataComponent> ent)
{
var validSpawners = new List<EntityUid>();
var spawners = EntityQueryEnumerator<DeliverySpawnerComponent>();
while (spawners.MoveNext(out var spawnerUid, out _))
{
var spawnerStation = _station.GetOwningStation(spawnerUid);
if (spawnerStation != ent.Owner)
continue;
if (!_power.IsPowered(spawnerUid))
continue;
validSpawners.Add(spawnerUid);
}
return validSpawners;
}
private void UpdateSpawner(float frameTime)
{
var dataQuery = EntityQueryEnumerator<CargoDeliveryDataComponent>();
var curTime = _timing.CurTime;
while (dataQuery.MoveNext(out var uid, out var deliveryData))
{
if (deliveryData.NextDelivery > curTime)
continue;
deliveryData.NextDelivery += _random.Next(deliveryData.MinDeliveryCooldown, deliveryData.MaxDeliveryCooldown); // Random cooldown between min and max
SpawnStationDeliveries((uid, deliveryData));
}
}
}

View File

@@ -0,0 +1,85 @@
using Content.Server.Cargo.Components;
using Content.Server.Cargo.Systems;
using Content.Server.Station.Systems;
using Content.Server.StationRecords.Systems;
using Content.Shared.Delivery;
using Content.Shared.FingerprintReader;
using Content.Shared.Labels.EntitySystems;
using Content.Shared.StationRecords;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
namespace Content.Server.Delivery;
/// <summary>
/// System for managing deliveries spawned by the mail teleporter.
/// This covers for mail spawning, as well as granting cargo money.
/// </summary>
public sealed partial class DeliverySystem : SharedDeliverySystem
{
[Dependency] private readonly CargoSystem _cargo = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly StationRecordsSystem _records = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly FingerprintReaderSystem _fingerprintReader = default!;
[Dependency] private readonly SharedLabelSystem _label = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeliveryComponent, MapInitEvent>(OnMapInit);
InitializeSpawning();
}
private void OnMapInit(Entity<DeliveryComponent> ent, ref MapInitEvent args)
{
_container.EnsureContainer<Container>(ent, ent.Comp.Container);
var stationId = _station.GetStationInMap(Transform(ent).MapID);
if (stationId == null)
return;
_records.TryGetRandomRecord<GeneralStationRecord>(stationId.Value, out var entry);
if (entry == null)
return;
ent.Comp.RecipientName = entry.Name;
ent.Comp.RecipientJobTitle = entry.JobTitle;
ent.Comp.RecipientStation = stationId.Value;
_appearance.SetData(ent, DeliveryVisuals.JobIcon, entry.JobIcon);
_label.Label(ent, ent.Comp.RecipientName);
if (TryComp<FingerprintReaderComponent>(ent, out var reader) && entry.Fingerprint != null)
{
_fingerprintReader.AddAllowedFingerprint((ent.Owner, reader), entry.Fingerprint);
}
Dirty(ent);
}
protected override void GrantSpesoReward(Entity<DeliveryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return;
if (!TryComp<StationBankAccountComponent>(ent.Comp.RecipientStation, out var account))
return;
_cargo.UpdateBankAccount((ent.Comp.RecipientStation.Value, account), ent.Comp.SpesoReward);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateSpawner(frameTime);
}
}

View File

@@ -1,12 +1,24 @@
using Content.Server.Explosion.EntitySystems;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Spawns a protoype when triggered.
/// </summary>
[RegisterComponent, Access(typeof(TriggerSystem))]
public sealed partial class SpawnOnTriggerComponent : Component
{
[ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Proto = string.Empty;
/// <summary>
/// The prototype to spawn.
/// </summary>
[DataField(required: true)]
public EntProtoId Proto = string.Empty;
/// <summary>
/// Use MapCoordinates for spawning?
/// Set to true if you don't want the new entity parented to the spawner.
/// </summary>
[DataField]
public bool mapCoords;
}

View File

@@ -1,15 +1,20 @@
namespace Content.Server.Explosion.Components
{
[RegisterComponent]
public sealed partial class TriggerOnCollideComponent : Component
{
[DataField("fixtureID", required: true)]
public string FixtureID = String.Empty;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Doesn't trigger if the other colliding fixture is nonhard.
/// </summary>
[DataField("ignoreOtherNonHard")]
public bool IgnoreOtherNonHard = true;
}
/// <summary>
/// Triggers when colliding with another entity.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnCollideComponent : Component
{
/// <summary>
/// The fixture with which to collide.
/// </summary>
[DataField(required: true)]
public string FixtureID = string.Empty;
/// <summary>
/// Doesn't trigger if the other colliding fixture is nonhard.
/// </summary>
[DataField]
public bool IgnoreOtherNonHard = true;
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Triggers on use in hand.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnUseComponent : Component { }

View File

@@ -0,0 +1,23 @@
using Content.Shared.Whitelist;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Checks if the user of a Trigger satisfies a whitelist and blacklist condition.
/// Cancels the trigger otherwise.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerWhitelistComponent : Component
{
/// <summary>
/// Whitelist for what entites can cause this trigger.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Blacklist for what entites can cause this trigger.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
}

View File

@@ -53,6 +53,9 @@ namespace Content.Server.Explosion.EntitySystems
_adminLogger.Add(LogType.Trigger, LogImpact.High,
$"A voice-trigger on {ToPrettyString(ent):entity} was triggered by {ToPrettyString(args.Source):speaker} speaking the key-phrase {component.KeyPhrase}.");
Trigger(ent, args.Source);
var voice = new VoiceTriggeredEvent(args.Source, message);
RaiseLocalEvent(ent, ref voice);
}
}
@@ -137,3 +140,12 @@ namespace Content.Server.Explosion.EntitySystems
}
}
}
/// <summary>
/// Raised when a voice trigger is activated, containing the message that triggered it.
/// </summary>
/// <param name="Source"> The EntityUid of the entity sending the message</param>
/// <param name="Message"> The contents of the message</param>
[ByRefEvent]
public readonly record struct VoiceTriggeredEvent(EntityUid Source, string? Message);

View File

@@ -14,6 +14,7 @@ using Content.Shared.Explosion.Components;
using Content.Shared.Explosion.Components.OnTrigger;
using Content.Shared.Implants.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
@@ -23,6 +24,7 @@ using Content.Shared.Slippery;
using Content.Shared.StepTrigger.Systems;
using Content.Shared.Trigger;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
@@ -31,10 +33,7 @@ using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Player;
using Content.Shared.Coordinates;
using Robust.Shared.Utility;
using Robust.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems
{
@@ -53,6 +52,12 @@ namespace Content.Server.Explosion.EntitySystems
}
}
/// <summary>
/// Raised before a trigger is activated.
/// </summary>
[ByRefEvent]
public record struct BeforeTriggerEvent(EntityUid Triggered, EntityUid? User, bool Cancelled = false);
/// <summary>
/// Raised when timer trigger becomes active.
/// </summary>
@@ -78,6 +83,7 @@ namespace Content.Server.Explosion.EntitySystems
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
public override void Initialize()
{
@@ -93,6 +99,7 @@ namespace Content.Server.Explosion.EntitySystems
SubscribeLocalEvent<TriggerOnSpawnComponent, MapInitEvent>(OnSpawnTriggered);
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<TriggerOnUseComponent, UseInHandEvent>(OnUse);
SubscribeLocalEvent<TriggerImplantActionComponent, ActivateImplantEvent>(OnImplantTrigger);
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredOffEvent>(OnStepTriggered);
SubscribeLocalEvent<TriggerOnSlipComponent, SlipEvent>(OnSlipTriggered);
@@ -109,6 +116,13 @@ namespace Content.Server.Explosion.EntitySystems
SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(OnSoundTrigger);
SubscribeLocalEvent<ShockOnTriggerComponent, TriggerEvent>(HandleShockTrigger);
SubscribeLocalEvent<RattleComponent, TriggerEvent>(HandleRattleTrigger);
SubscribeLocalEvent<TriggerWhitelistComponent, BeforeTriggerEvent>(HandleWhitelist);
}
private void HandleWhitelist(Entity<TriggerWhitelistComponent> ent, ref BeforeTriggerEvent args)
{
args.Cancelled = !_whitelist.CheckBoth(args.User, ent.Comp.Blacklist, ent.Comp.Whitelist);
}
private void OnSoundTrigger(EntityUid uid, SoundOnTriggerComponent component, TriggerEvent args)
@@ -155,16 +169,23 @@ namespace Content.Server.Explosion.EntitySystems
RemCompDeferred<AnchorOnTriggerComponent>(uid);
}
private void OnSpawnTrigger(EntityUid uid, SpawnOnTriggerComponent component, TriggerEvent args)
private void OnSpawnTrigger(Entity<SpawnOnTriggerComponent> ent, ref TriggerEvent args)
{
var xform = Transform(uid);
var xform = Transform(ent);
var coords = xform.Coordinates;
if (ent.Comp.mapCoords)
{
var mapCoords = _transformSystem.GetMapCoordinates(ent, xform);
Spawn(ent.Comp.Proto, mapCoords);
}
else
{
var coords = xform.Coordinates;
if (!coords.IsValid(EntityManager))
return;
Spawn(ent.Comp.Proto, coords);
if (!coords.IsValid(EntityManager))
return;
Spawn(component.Proto, coords);
}
}
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
@@ -248,6 +269,15 @@ namespace Content.Server.Explosion.EntitySystems
args.Handled = true;
}
private void OnUse(Entity<TriggerOnUseComponent> ent, ref UseInHandEvent args)
{
if (args.Handled)
return;
Trigger(ent.Owner, args.User);
args.Handled = true;
}
private void OnImplantTrigger(EntityUid uid, TriggerImplantActionComponent component, ActivateImplantEvent args)
{
args.Handled = Trigger(uid);
@@ -275,6 +305,11 @@ namespace Content.Server.Explosion.EntitySystems
public bool Trigger(EntityUid trigger, EntityUid? user = null)
{
var beforeTriggerEvent = new BeforeTriggerEvent(trigger, user);
RaiseLocalEvent(trigger, ref beforeTriggerEvent);
if (beforeTriggerEvent.Cancelled)
return false;
var triggerEvent = new TriggerEvent(trigger, user);
EntityManager.EventBus.RaiseLocalEvent(trigger, triggerEvent, true);
return triggerEvent.Handled;

View File

@@ -1,4 +1,5 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.DoAfter;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics.Components;
@@ -32,8 +33,9 @@ namespace Content.Server.Forensics
public override void Initialize()
{
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit);
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit);
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit, after: new[] { typeof(BloodstreamSystem) });
// The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
@@ -65,18 +67,19 @@ namespace Content.Server.Forensics
private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
{
ent.Comp.Fingerprint = GenerateFingerprint();
Dirty(ent);
if (ent.Comp.Fingerprint == null)
RandomizeFingerprint((ent.Owner, ent.Comp));
}
private void OnDNAInit(EntityUid uid, DnaComponent component, MapInitEvent args)
private void OnDNAInit(Entity<DnaComponent> ent, ref MapInitEvent args)
{
if (component.DNA == String.Empty)
if (ent.Comp.DNA == null)
RandomizeDNA((ent.Owner, ent.Comp));
else
{
component.DNA = GenerateDNA();
var ev = new GenerateDnaEvent { Owner = uid, DNA = component.DNA };
RaiseLocalEvent(uid, ref ev);
// If set manually (for example by cloning) we also need to inform the bloodstream of the correct DNA string so it can be updated
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
RaiseLocalEvent(ent.Owner, ref ev);
}
}
@@ -84,7 +87,7 @@ namespace Content.Server.Forensics
{
string dna = Loc.GetString("forensics-dna-unknown");
if (TryComp(uid, out DnaComponent? dnaComp))
if (TryComp(uid, out DnaComponent? dnaComp) && dnaComp.DNA != null)
dna = dnaComp.DNA;
foreach (EntityUid part in args.GibbedParts)
@@ -103,7 +106,7 @@ namespace Content.Server.Forensics
{
foreach (EntityUid hitEntity in args.HitEntities)
{
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp))
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null)
component.DNAs.Add(hitEntityComp.DNA);
}
}
@@ -301,6 +304,9 @@ namespace Content.Server.Forensics
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
{
if (component.DNA == null)
return;
var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
recipientComp.DNAs.Add(component.DNA);
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
@@ -308,6 +314,35 @@ namespace Content.Server.Forensics
#region Public API
/// <summary>
/// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
/// Does nothing if it does not have the DnaComponent.
/// </summary>
public void RandomizeDNA(Entity<DnaComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
ent.Comp.DNA = GenerateDNA();
Dirty(ent);
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
RaiseLocalEvent(ent.Owner, ref ev);
}
/// <summary>
/// Give the entity a new, random fingerprint string.
/// Does nothing if it does not have the FingerprintComponent.
/// </summary>
public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
ent.Comp.Fingerprint = GenerateFingerprint();
Dirty(ent);
}
/// <summary>
/// Transfer DNA from one entity onto the forensics of another
/// </summary>
@@ -316,7 +351,7 @@ namespace Content.Server.Forensics
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
{
if (TryComp<DnaComponent>(donor, out var donorComp))
if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
{
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
recipientComp.DNAs.Add(donorComp.DNA);

View File

@@ -0,0 +1,23 @@
using Content.Shared.Cloning;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Gamerule component for spawning a paradox clone antagonist.
/// </summary>
[RegisterComponent]
public sealed partial class ParadoxCloneRuleComponent : Component
{
/// <summary>
/// Cloning settings to be used.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
/// <summary>
/// Visual effect spawned when gibbing at round end.
/// </summary>
[DataField]
public EntProtoId GibProto = "MobParadoxTimed";
}

View File

@@ -24,13 +24,13 @@ public sealed partial class TraitorRuleComponent : Component
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
[DataField]
public ProtoId<DatasetPrototype> CodewordAdjectives = "adjectives";
public ProtoId<LocalizedDatasetPrototype> CodewordAdjectives = "Adjectives";
[DataField]
public ProtoId<DatasetPrototype> CodewordVerbs = "verbs";
public ProtoId<LocalizedDatasetPrototype> CodewordVerbs = "Verbs";
[DataField]
public ProtoId<DatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
public ProtoId<LocalizedDatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
/// <summary>
/// Give this traitor an Uplink on spawn.

View File

@@ -19,6 +19,11 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
return EntityQueryEnumerator<ActiveGameRuleComponent, T, GameRuleComponent>();
}
protected EntityQueryEnumerator<DelayedStartRuleComponent, T, GameRuleComponent> QueryDelayedRules()
{
return EntityQueryEnumerator<DelayedStartRuleComponent, T, GameRuleComponent>();
}
/// <summary>
/// Queries all gamerules, regardless of if they're active or not.
/// </summary>

View File

@@ -0,0 +1,83 @@
using Content.Server.Antag;
using Content.Server.Cloning;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Objectives.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Gibbing.Components;
using Content.Shared.Mind;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public sealed class ParadoxCloneRuleSystem : GameRuleSystem<ParadoxCloneRuleComponent>
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly CloningSystem _cloning = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ParadoxCloneRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
}
protected override void Started(EntityUid uid, ParadoxCloneRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
// check if we got enough potential cloning targets, otherwise cancel the gamerule so that the ghost role does not show up
var allHumans = _mind.GetAliveHumans();
if (allHumans.Count == 0)
{
Log.Info("Could not find any alive players to create a paradox clone from! Ending gamerule.");
ForceEndSelf(uid, gameRule);
}
}
// we have to do the spawning here so we can transfer the mind to the correct entity and can assign the objectives correctly
private void OnAntagSelectEntity(Entity<ParadoxCloneRuleComponent> ent, ref AntagSelectEntityEvent args)
{
if (args.Session?.AttachedEntity is not { } spawner)
return;
if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
{
Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for ParadoxCloneRule");
return;
}
// get possible targets
var allHumans = _mind.GetAliveHumans();
// we already checked when starting the gamerule, but someone might have died since then.
if (allHumans.Count == 0)
{
Log.Warning("Could not find any alive players to create a paradox clone from!");
return;
}
// pick a random player
var playerToClone = _random.Pick(allHumans);
var bodyToClone = playerToClone.Comp.OwnedEntity;
if (bodyToClone == null || !_cloning.TryCloning(bodyToClone.Value, _transform.GetMapCoordinates(spawner), settings, out var clone))
{
Log.Error($"Unable to make a paradox clone of entity {ToPrettyString(bodyToClone)}");
return;
}
var targetComp = EnsureComp<TargetOverrideComponent>(clone.Value);
targetComp.Target = playerToClone.Owner; // set the kill target
var gibComp = EnsureComp<GibOnRoundEndComponent>(clone.Value);
gibComp.SpawnProto = ent.Comp.GibProto;
gibComp.PreventGibbingObjectives = new() { "ParadoxCloneKillObjective" }; // don't gib them if they killed the original.
args.Entity = clone;
}
}

View File

@@ -189,7 +189,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
commandList.Add(id);
}
return IsGroupDetainedOrDead(commandList, true, true);
return IsGroupDetainedOrDead(commandList, true, true, true);
}
private void OnHeadRevMobStateChanged(EntityUid uid, HeadRevolutionaryComponent comp, MobStateChangedEvent ev)
@@ -214,7 +214,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
// If no Head Revs are alive all normal Revs will lose their Rev status and rejoin Nanotrasen
// Cuffing Head Revs is not enough - they must be killed.
if (IsGroupDetainedOrDead(headRevList, false, false))
if (IsGroupDetainedOrDead(headRevList, false, false, false))
{
var rev = AllEntityQuery<RevolutionaryComponent, MindContainerComponent>();
while (rev.MoveNext(out var uid, out _, out var mc))
@@ -251,34 +251,45 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
/// <param name="list">The list of the entities</param>
/// <param name="checkOffStation">Bool for if you want to check if someone is in space and consider them missing in action. (Won't check when emergency shuttle arrives just in case)</param>
/// <param name="countCuffed">Bool for if you don't want to count cuffed entities.</param>
/// <param name="countRevolutionaries">Bool for if you want to count revolutionaries.</param>
/// <returns></returns>
private bool IsGroupDetainedOrDead(List<EntityUid> list, bool checkOffStation, bool countCuffed)
private bool IsGroupDetainedOrDead(List<EntityUid> list, bool checkOffStation, bool countCuffed, bool countRevolutionaries)
{
var gone = 0;
foreach (var entity in list)
{
if (TryComp<CuffableComponent>(entity, out var cuffed) && cuffed.CuffedHandCount > 0 && countCuffed)
{
gone++;
continue;
}
else
if (TryComp<MobStateComponent>(entity, out var state))
{
if (TryComp<MobStateComponent>(entity, out var state))
{
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
{
gone++;
}
else if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
{
gone++;
}
}
//If they don't have the MobStateComponent they might as well be dead.
else
if (state.CurrentState == MobState.Dead || state.CurrentState == MobState.Invalid)
{
gone++;
continue;
}
if (checkOffStation && _stationSystem.GetOwningStation(entity) == null && !_emergencyShuttle.EmergencyShuttleArrived)
{
gone++;
continue;
}
}
//If they don't have the MobStateComponent they might as well be dead.
else
{
gone++;
continue;
}
if ((HasComp<RevolutionaryComponent>(entity) || HasComp<HeadRevolutionaryComponent>(entity)) && countRevolutionaries)
{
gone++;
continue;
}
}

View File

@@ -12,6 +12,7 @@ using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.NPC.Systems;
using Content.Shared.PDA;
using Content.Shared.Random.Helpers;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.RoleCodeword;
@@ -74,7 +75,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
string[] codewords = new string[finalCodewordCount];
for (var i = 0; i < finalCodewordCount; i++)
{
codewords[i] = _random.PickAndTake(codewordPool);
codewords[i] = Loc.GetString(_random.PickAndTake(codewordPool));
}
return codewords;
}
@@ -98,7 +99,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
}
var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values);
var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers));
// Uplink code will go here if applicable, but we still need the variable if there aren't any
Note[]? code = null;

View File

@@ -50,7 +50,7 @@ namespace Content.Server.Ghost
mind = _entities.GetComponent<MindComponent>(mindId);
}
if (!_entities.System<GhostSystem>().OnGhostAttempt(mindId, true, true, mind))
if (!_entities.System<GhostSystem>().OnGhostAttempt(mindId, true, true, mind: mind))
{
shell.WriteLine(Loc.GetString("ghost-command-denied"));
}

View File

@@ -499,7 +499,7 @@ namespace Content.Server.Ghost
return ghost;
}
public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, MindComponent? mind = null)
public bool OnGhostAttempt(EntityUid mindId, bool canReturnGlobal, bool viaCommand = false, bool forced = false, MindComponent? mind = null)
{
if (!Resolve(mindId, ref mind))
return false;
@@ -507,7 +507,12 @@ namespace Content.Server.Ghost
var playerEntity = mind.CurrentEntity;
if (playerEntity != null && viaCommand)
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
{
if (forced)
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} was forced to ghost via command");
else
_adminLog.Add(LogType.Mind, $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command");
}
var handleEv = new GhostAttemptHandleEvent(mind, canReturnGlobal);
RaiseLocalEvent(handleEv);
@@ -516,7 +521,7 @@ namespace Content.Server.Ghost
if (handleEv.Handled)
return handleEv.Result;
if (mind.PreventGhosting)
if (mind.PreventGhosting && !forced)
{
if (mind.Session != null) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts
{

View File

@@ -0,0 +1,55 @@
using Content.Shared.GameTicking;
using Content.Shared.Gibbing.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Systems;
using Content.Server.Body.Systems;
namespace Content.Server.Gibbing.Systems;
public sealed class GibOnRoundEndSystem : EntitySystem
{
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
public override void Initialize()
{
base.Initialize();
// this is raised after RoundEndTextAppendEvent, so they can successfully greentext before we gib them
SubscribeLocalEvent<RoundEndMessageEvent>(OnRoundEnd);
}
private void OnRoundEnd(RoundEndMessageEvent args)
{
var gibQuery = EntityQueryEnumerator<GibOnRoundEndComponent>();
// gib everyone with the component
while (gibQuery.MoveNext(out var uid, out var gibComp))
{
var gib = false;
// if they fulfill all objectives given in the component they are not gibbed
if (_mind.TryGetMind(uid, out var mindId, out var mindComp))
{
foreach (var objectiveId in gibComp.PreventGibbingObjectives)
{
if (!_mind.TryFindObjective((mindId, mindComp), objectiveId, out var objective)
|| !_objectives.IsCompleted(objective.Value, (mindId, mindComp)))
{
gib = true;
break;
}
}
}
else
gib = true;
if (!gib)
continue;
if (gibComp.SpawnProto != null)
SpawnAtPosition(gibComp.SpawnProto, Transform(uid).Coordinates);
_body.GibBody(uid, splatModifier: 5f);
}
}
}

View File

@@ -216,18 +216,12 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
_humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
_metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
if (TryComp<DnaComponent>(ent, out var dna))
{
dna.DNA = _forensicsSystem.GenerateDNA();
var ev = new GenerateDnaEvent { Owner = ent, DNA = dna.DNA };
RaiseLocalEvent(ent, ref ev);
}
if (TryComp<FingerprintComponent>(ent, out var fingerprint))
{
fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
}
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
// If the entity has the respecive components, then scramble the dna and fingerprint strings
_forensicsSystem.RandomizeDNA(ent);
_forensicsSystem.RandomizeFingerprint(ent);
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
_popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
}

View File

@@ -1,5 +1,6 @@
using Content.Shared;
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Robust.Shared.Random;
namespace Content.Server.Light.EntitySystems;
@@ -15,8 +16,7 @@ public sealed class LightCycleSystem : SharedLightCycleSystem
if (ent.Comp.InitialOffset)
{
ent.Comp.Offset = _random.Next(ent.Comp.Duration);
Dirty(ent);
SetOffset(ent, _random.Next(ent.Comp.Duration));
}
}
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.Light.EntitySystems;
namespace Content.Server.Light.EntitySystems;
public sealed class SunShadowSystem : SharedSunShadowSystem
{
}

View File

@@ -8,6 +8,7 @@ using Content.Shared.Implants.Components;
using Content.Shared.Mindshield.Components;
using Content.Shared.Revolutionary.Components;
using Content.Shared.Tag;
using Robust.Shared.Containers;
namespace Content.Server.Mindshield;
@@ -29,6 +30,7 @@ public sealed class MindShieldSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<SubdermalImplantComponent, ImplantImplantedEvent>(ImplantCheck);
SubscribeLocalEvent<MindShieldImplantComponent, EntGotRemovedFromContainerMessage>(OnImplantDraw);
}
/// <summary>
@@ -61,4 +63,10 @@ public sealed class MindShieldSystem : EntitySystem
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(implanted)} was deconverted due to being implanted with a Mindshield.");
}
}
private void OnImplantDraw(Entity<MindShieldImplantComponent> ent, ref EntGotRemovedFromContainerMessage args)
{
RemComp<MindShieldComponent>(args.Container.Owner);
}
}

View File

@@ -40,6 +40,13 @@ public sealed partial class NPCRangedCombatComponent : Component
[ViewVariables(VVAccess.ReadWrite)]
public bool TargetInLOS = false;
/// <summary>
/// If true, only opaque objects will block line of sight.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
// ReSharper disable once InconsistentNaming
public bool UseOpaqueForLOSChecks = false;
/// <summary>
/// Delay after target is in LOS before we start shooting.
/// </summary>

View File

@@ -10,7 +10,7 @@ public sealed partial class HTNComponent : NPCComponent
/// The base task to use for planning
/// </summary>
[ViewVariables(VVAccess.ReadWrite),
DataField("rootTask", required: true)]
DataField("rootTask", required: true)]
public HTNCompoundTask RootTask = default!;
/// <summary>
@@ -47,4 +47,10 @@ public sealed partial class HTNComponent : NPCComponent
/// Is this NPC currently planning?
/// </summary>
[ViewVariables] public bool Planning => PlanningJob != null;
/// <summary>
/// Determines whether plans should be made / updated for this entity
/// </summary>
[DataField]
public bool Enabled = true;
}

View File

@@ -133,6 +133,39 @@ public sealed class HTNSystem : EntitySystem
component.PlanningJob = null;
}
/// <summary>
/// Enable / disable the hierarchical task network of an entity
/// </summary>
/// <param name="ent">The entity and its <see cref="HTNComponent"/></param>
/// <param name="state">Set 'true' to enable, or 'false' to disable, the HTN</param>
/// <param name="planCooldown">Specifies a time in seconds before the entity can start planning a new action (only takes effect when the HTN is enabled)</param>
// ReSharper disable once InconsistentNaming
[PublicAPI]
public void SetHTNEnabled(Entity<HTNComponent> ent, bool state, float planCooldown = 0f)
{
if (ent.Comp.Enabled == state)
return;
ent.Comp.Enabled = state;
ent.Comp.PlanAccumulator = planCooldown;
ent.Comp.PlanningToken?.Cancel();
ent.Comp.PlanningToken = null;
if (ent.Comp.Plan != null)
{
var currentOperator = ent.Comp.Plan.CurrentOperator;
ShutdownTask(currentOperator, ent.Comp.Blackboard, HTNOperatorStatus.Failed);
ShutdownPlan(ent.Comp);
ent.Comp.Plan = null;
}
if (ent.Comp.Enabled && ent.Comp.PlanAccumulator <= 0)
RequestPlan(ent.Comp);
}
/// <summary>
/// Forces the NPC to replan.
/// </summary>
@@ -147,12 +180,15 @@ public sealed class HTNSystem : EntitySystem
_planQueue.Process();
var query = EntityQueryEnumerator<ActiveNPCComponent, HTNComponent>();
while(query.MoveNext(out var uid, out _, out var comp))
while (query.MoveNext(out var uid, out _, out var comp))
{
// If we're over our max count or it's not MapInit then ignore the NPC.
if (count >= maxUpdates)
break;
if (!comp.Enabled)
continue;
if (comp.PlanningJob != null)
{
if (comp.PlanningJob.Exception != null)

View File

@@ -1,4 +1,5 @@
using Content.Server.Interaction;
using Content.Shared.Physics;
namespace Content.Server.NPC.HTN.Preconditions;
@@ -13,6 +14,9 @@ public sealed partial class TargetInLOSPrecondition : HTNPrecondition
[DataField("rangeKey")]
public string RangeKey = "RangeKey";
[DataField("opaqueKey")]
public bool UseOpaqueForLOSChecksKey = true;
public override void Initialize(IEntitySystemManager sysManager)
{
base.Initialize(sysManager);
@@ -27,7 +31,8 @@ public sealed partial class TargetInLOSPrecondition : HTNPrecondition
return false;
var range = blackboard.GetValueOrDefault<float>(RangeKey, _entManager);
var collisionGroup = UseOpaqueForLOSChecksKey ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
return _interaction.InRangeUnobstructed(owner, target, range);
return _interaction.InRangeUnobstructed(owner, target, range, collisionGroup);
}
}

View File

@@ -33,6 +33,12 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
[DataField("requireLOS")]
public bool RequireLOS = false;
/// <summary>
/// If true, only opaque objects will block line of sight.
/// </summary>
[DataField("opaqueKey")]
public bool UseOpaqueForLOSChecks = false;
// Like movement we add a component and pass it off to the dedicated system.
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
@@ -56,8 +62,10 @@ public sealed partial class GunOperator : HTNOperator, IHtnConditionalShutdown
public override void Startup(NPCBlackboard blackboard)
{
base.Startup(blackboard);
var ranged = _entManager.EnsureComponent<NPCRangedCombatComponent>(blackboard.GetValue<EntityUid>(NPCBlackboard.Owner));
ranged.Target = blackboard.GetValue<EntityUid>(TargetKey);
ranged.UseOpaqueForLOSChecks = UseOpaqueForLOSChecks;
if (blackboard.TryGetValue<float>(NPCBlackboard.RotateSpeed, out var rotSpeed, _entManager))
{

View File

@@ -0,0 +1,12 @@
namespace Content.Server.NPC.Queries.Considerations;
/// <summary>
/// Returns 0f if the NPC has a <see cref="TurretTargetSettingsComponent"/> and the
/// target entity is exempt from being targeted, otherwise it returns 1f.
/// See <see cref="TurretTargetSettingsSystem.EntityIsTargetForTurret"/>
/// for further details on turret target validation.
/// </summary>
public sealed partial class TurretTargetingCon : UtilityConsideration
{
}

View File

@@ -1,6 +1,7 @@
using Content.Server.NPC.Components;
using Content.Shared.CombatMode;
using Content.Shared.Interaction;
using Content.Shared.Physics;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Map;
@@ -132,8 +133,10 @@ public sealed partial class NPCCombatSystem
if (comp.LOSAccumulator < 0f)
{
comp.LOSAccumulator += UnoccludedCooldown;
// For consistency with NPC steering.
comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f);
var collisionGroup = comp.UseOpaqueForLOSChecks ? CollisionGroup.Opaque : (CollisionGroup.Impassable | CollisionGroup.InteractImpassable);
comp.TargetInLOS = _interaction.InRangeUnobstructed(uid, comp.Target, distance + 0.1f, collisionGroup);
}
if (!comp.TargetInLOS)

View File

@@ -20,6 +20,7 @@ using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Tools.Systems;
using Content.Shared.Turrets;
using Content.Shared.Weapons.Melee;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
@@ -53,6 +54,7 @@ public sealed class NPCUtilitySystem : EntitySystem
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly MobThresholdSystem _thresholdSystem = default!;
[Dependency] private readonly TurretTargetSettingsSystem _turretTargetSettings = default!;
private EntityQuery<PuddleComponent> _puddleQuery;
private EntityQuery<TransformComponent> _xformQuery;
@@ -358,6 +360,14 @@ public sealed class NPCUtilitySystem : EntitySystem
return 1f;
return 0f;
}
case TurretTargetingCon:
{
if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||
_turretTargetSettings.EntityIsTargetForTurret((owner, turretTargetSettings), targetUid))
return 1f;
return 0f;
}
default:
throw new NotImplementedException();
}

View File

@@ -1,12 +1,8 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random head.
/// If there are no heads it will fallback to any person.
/// </summary>
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
public sealed partial class PickRandomHeadComponent : Component
{
}
[RegisterComponent]
public sealed partial class PickRandomHeadComponent : Component;

View File

@@ -1,11 +1,7 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person.
/// </summary>
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
public sealed partial class PickRandomPersonComponent : Component
{
}
[RegisterComponent]
public sealed partial class PickRandomPersonComponent : Component;

View File

@@ -0,0 +1,8 @@
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets this objective's target to the one given in <see cref="TargetOverrideComponent"/>, if the entity has it.
/// This component needs to be added to objective entity itself.
/// </summary>
[RegisterComponent]
public sealed partial class PickSpecificPersonComponent : Component;

View File

@@ -1,11 +1,7 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="KeepAliveConditionComponent"/> to a random traitor.
/// </summary>
[RegisterComponent, Access(typeof(KeepAliveConditionSystem))]
public sealed partial class RandomTraitorAliveComponent : Component
{
}
[RegisterComponent]
public sealed partial class RandomTraitorAliveComponent : Component;

View File

@@ -1,11 +1,7 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="HelpProgressConditionComponent"/> to a random traitor.
/// </summary>
[RegisterComponent, Access(typeof(HelpProgressConditionSystem))]
public sealed partial class RandomTraitorProgressComponent : Component
{
}
[RegisterComponent]
public sealed partial class RandomTraitorProgressComponent : Component;

View File

@@ -1,18 +0,0 @@
using Content.Server.Objectives.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Objectives.Components.Targets;
/// <summary>
/// Allows an object to become the target of a StealCollection objection
/// </summary>
[RegisterComponent]
public sealed partial class StealTargetComponent : Component
{
/// <summary>
/// The theft group to which this item belongs.
/// </summary>
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public string StealGroup;
}

View File

@@ -0,0 +1,16 @@
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets a target objective to a specific target when receiving it.
/// The objective entity needs to have <see cref="PickSpecificPersonComponent"/>.
/// This component needs to be added to entity receiving the objective.
/// </summary>
[RegisterComponent]
public sealed partial class TargetOverrideComponent : Component
{
/// <summary>
/// The entity that should be targeted.
/// </summary>
[DataField]
public EntityUid? Target;
}

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