forked from space-syndicate/space-station-14
Merge remote-tracking branch 'upstream/stable' into april-fools-stable-merge
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Advertise.Systems;
|
||||
|
||||
namespace Content.Client.Advertise.Systems;
|
||||
|
||||
public sealed class SpeakOnUIClosedSystem : SharedSpeakOnUIClosedSystem;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
5
Content.Client/Delivery/DeliverySystem.cs
Normal file
5
Content.Client/Delivery/DeliverySystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Delivery;
|
||||
|
||||
namespace Content.Client.Delivery;
|
||||
|
||||
public sealed class DeliverySystem : SharedDeliverySystem;
|
||||
45
Content.Client/Delivery/DeliveryVisualizerSystem.cs
Normal file
45
Content.Client/Delivery/DeliveryVisualizerSystem.cs
Normal 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,
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -54,6 +54,6 @@ public sealed class AfterLightTargetOverlay : Overlay
|
||||
|
||||
worldHandle.SetTransform(localMatrix);
|
||||
worldHandle.DrawTextureRectRegion(lightOverlay.EnlargedLightTarget.Texture, bounds, subRegion: subRegion);
|
||||
}, null);
|
||||
}, Color.Transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
92
Content.Client/Light/EntitySystems/SunShadowSystem.cs
Normal file
92
Content.Client/Light/EntitySystems/SunShadowSystem.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
160
Content.Client/Light/SunShadowOverlay.cs
Normal file
160
Content.Client/Light/SunShadowOverlay.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,4 +16,9 @@ public sealed partial class VendingMachineItem : BoxContainer
|
||||
|
||||
NameLabel.Text = text;
|
||||
}
|
||||
|
||||
public void SetText(string text)
|
||||
{
|
||||
NameLabel.Text = text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
61
Content.Server/Administration/Commands/ForceGhostCommand.cs
Normal file
61
Content.Server/Administration/Commands/ForceGhostCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}.");
|
||||
}
|
||||
|
||||
|
||||
323
Content.Server/Cloning/CloningPodSystem.cs
Normal file
323
Content.Server/Cloning/CloningPodSystem.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
47
Content.Server/Cloning/RandomCloneSpawnerSystem.cs
Normal file
47
Content.Server/Cloning/RandomCloneSpawnerSystem.cs
Normal 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 _);
|
||||
}
|
||||
}
|
||||
51
Content.Server/Delivery/CargoDeliveryDataComponent.cs
Normal file
51
Content.Server/Delivery/CargoDeliveryDataComponent.cs
Normal 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;
|
||||
}
|
||||
131
Content.Server/Delivery/DeliverySystem.Spawning.cs
Normal file
131
Content.Server/Delivery/DeliverySystem.Spawning.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Content.Server/Delivery/DeliverySystem.cs
Normal file
85
Content.Server/Delivery/DeliverySystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Content.Server.Explosion.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Triggers on use in hand.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class TriggerOnUseComponent : Component { }
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
83
Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs
Normal file
83
Content.Server/GameTicking/Rules/ParadoxCloneRuleSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
55
Content.Server/Gibbing/Systems/GibOnRoundEndSystem.cs
Normal file
55
Content.Server/Gibbing/Systems/GibOnRoundEndSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
Content.Server/Light/EntitySystems/SunShadowSystem.cs
Normal file
8
Content.Server/Light/EntitySystems/SunShadowSystem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Light.EntitySystems;
|
||||
|
||||
namespace Content.Server.Light.EntitySystems;
|
||||
|
||||
public sealed class SunShadowSystem : SharedSunShadowSystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user