merge master and hope things don't break

This commit is contained in:
Aera Aulin 2025-03-13 11:28:14 -07:00
parent a1480c1ffd
commit 2261ec8f5f
540 changed files with 17903 additions and 14800 deletions

View File

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

View File

@ -36,6 +36,9 @@ namespace Content.Client.Administration.UI.Bwoink
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
var newPlayerThreshold = 0;
_cfg.OnValueChanged(CCVars.NewPlayerThreshold, (val) => { newPlayerThreshold = val; }, true);
var uiController = _ui.GetUIController<AHelpUIController>();
if (uiController.UIHelper is not AdminAHelpUIHandler helper)
return;
@ -59,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)
@ -73,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 (info.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold)))
// Mark new players with symbol
if (IsNewPlayer(info))
sb.Append(new Rune(0x23F2)); // ⏲
sb.AppendFormat("\"{0}\"", text);
@ -84,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);
@ -93,31 +111,37 @@ namespace Content.Client.Administration.UI.Bwoink
if (a.IsPinned != b.IsPinned)
return a.IsPinned ? -1 : 1;
// First, sort by unread. Any chat with unread messages appears first.
// Then, any chat with unread messages.
var aUnread = ach.Unread > 0;
var bUnread = bch.Unread > 0;
if (aUnread != bUnread)
return aUnread ? -1 : 1;
// Sort by recent messages during the current round.
// Then, any chat with recent messages from the current round
var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue;
var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue;
if (aRecent != bRecent)
return aRecent ? -1 : 1;
// Next, sort by connection status. Any disconnected players are grouped towards the end.
// Sort by connection status. Disconnected players will be last.
if (a.Connected != b.Connected)
return a.Connected ? -1 : 1;
// Sort connected players by New Player status, then by Antag status
// Sort connected players by whether they have joined the round, then by New Player status, then by Antag status
if (a.Connected && b.Connected)
{
var aNewPlayer = a.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
var bNewPlayer = b.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
var aNewPlayer = IsNewPlayer(a);
var bNewPlayer = IsNewPlayer(b);
// Players who have joined the round will be listed before players in the lobby
if (a.ActiveThisRound != b.ActiveThisRound)
return a.ActiveThisRound ? -1 : 1;
// Within both the joined group and lobby group, new players will be grouped and listed first
if (aNewPlayer != bNewPlayer)
return aNewPlayer ? -1 : 1;
// Within all four previous groups, antagonists will be listed first.
if (a.Antag != b.Antag)
return a.Antag ? -1 : 1;
}

View File

@ -22,12 +22,9 @@ namespace Content.Client.Administration.UI.Bwoink
return;
}
Title = $"{sel.CharacterName} / {sel.Username}";
Title = $"{sel.CharacterName} / {sel.Username} | {Loc.GetString("generic-playtime-title")}: ";
if (sel.OverallPlaytime != null)
{
Title += $" | {Loc.GetString("generic-playtime-title")}: {sel.PlaytimeString}";
}
Title += sel.OverallPlaytime != null ? sel.PlaytimeString : Loc.GetString("generic-unknown-title");
};
OnOpen += () =>

View File

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

View File

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

View File

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

View File

@ -1,8 +1,10 @@
using Content.Shared.CCVar;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Robust.Client.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Numerics; // CD - Character Records
@ -14,12 +16,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)
@ -27,6 +32,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);
@ -219,16 +233,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)
@ -276,6 +304,31 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
spriteComp.RemoveLayer(index);
}
}
private void AddUndergarments(HumanoidAppearanceComponent humanoid, SpriteComponent sprite, bool undergarmentTop, bool undergarmentBottom)
{
if (undergarmentTop && humanoid.UndergarmentTop != null)
{
var marking = new Marking(humanoid.UndergarmentTop, new List<Color> { new Color() });
if (_markingManager.TryGetMarking(marking, out var prototype))
{
// Markings are added to ClientOldMarkings because otherwise it causes issues when toggling the feature on/off.
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentTop, new List<Marking>{ marking });
ApplyMarking(prototype, null, true, humanoid, sprite);
}
}
if (undergarmentBottom && humanoid.UndergarmentBottom != null)
{
var marking = new Marking(humanoid.UndergarmentBottom, new List<Color> { new Color() });
if (_markingManager.TryGetMarking(marking, out var prototype))
{
humanoid.ClientOldMarkings.Markings.Add(MarkingCategories.UndergarmentBottom, new List<Marking>{ marking });
ApplyMarking(prototype, null, true, humanoid, sprite);
}
}
}
private void ApplyMarking(MarkingPrototype markingPrototype,
IReadOnlyList<Color>? colors,
bool visible,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,154 @@
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 worldMatrix = _xformSys.GetWorldMatrix(xform);
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++)
{
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, _target, eye, 1f);
// Draw stencil (see roofoverlay).
args.WorldHandle.RenderInRenderTarget(viewport.LightRenderTarget,
() =>
{
var invMatrix =
viewport.LightRenderTarget.GetWorldToLocalMatrix(eye, scale);
worldHandle.SetTransform(invMatrix);
var maskShader = _protoManager.Index<ShaderPrototype>("Mix").Instance();
worldHandle.UseShader(maskShader);
worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));
}, null);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,55 +0,0 @@
using Content.Server._DV.Cloning;
using Content.Shared.Humanoid.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests._DV;
[TestFixture]
public sealed class MetempsychosisTest
{
[Test]
public async Task AllHumanoidPoolSpeciesExist()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
// Per RobustIntegrationTest.cs, wait until state is settled to access it.
await server.WaitIdleAsync();
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
var metemComponent = new MetempsychoticMachineComponent();
await server.WaitAssertion(() =>
{
prototypeManager.TryIndex(metemComponent.MetempsychoticHumanoidPool,
out var humanoidPool);
prototypeManager.TryIndex(metemComponent.MetempsychoticNonHumanoidPool,
out var nonHumanoidPool);
Assert.Multiple(() =>
{
Assert.That(humanoidPool, Is.Not.Null, "MetempsychoticHumanoidPool is null!");
Assert.That(nonHumanoidPool, Is.Not.Null, "MetempsychoticNonHumanoidPool is null!");
Assert.That(humanoidPool.Weights,
Is.Not.Empty,
"MetempsychoticHumanoidPool has no valid prototypes!");
Assert.That(nonHumanoidPool.Weights,
Is.Not.Empty,
"MetempsychoticNonHumanoidPool has no valid prototypes!");
});
foreach (var key in humanoidPool.Weights.Keys)
{
Assert.That(prototypeManager.TryIndex<SpeciesPrototype>(key, out _),
$"MetempsychoticHumanoidPool has invalid prototype {key}!");
}
foreach (var key in nonHumanoidPool.Weights.Keys)
{
Assert.That(prototypeManager.TryIndex<EntityPrototype>(key, out _),
$"MetempsychoticNonHumanoidPool has invalid prototype {key}!");
}
});
await pair.CleanReturnAsync();
}
}

View File

@ -78,7 +78,12 @@ public sealed class IdCardSystem : SharedIdCardSystem
}
// Give them a wonderful new access to compensate for everything
var random = _random.Pick(_prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().ToArray());
var ids = _prototypeManager.EnumeratePrototypes<AccessLevelPrototype>().Where(x => x.CanAddToIdCard).ToArray();
if (ids.Length == 0)
return;
var random = _random.Pick(ids);
access.Tags.Add(random.ID);
Dirty(uid, access);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Cloning.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.Medical.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.UserInterface;
using Content.Shared.Cloning;
@ -16,24 +15,22 @@ 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!;
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
public override void Initialize()
{
base.Initialize();
@ -170,8 +167,7 @@ namespace Content.Server.Cloning
if (mind.UserId.HasValue == false || mind.Session == null)
return;
// Nyano: Adds scannerComp.MetemKarmaBonus
if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier, scannerComp.MetemKarmaBonus))
if (_cloningPodSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
}

View File

@ -0,0 +1,330 @@
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.Server.Psionics; // DeltaV
using Content.Shared._EE.Silicon.Components; // Goobstation
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;
if (HasComp<SiliconComponent>(bodyToClone))
return false; // Goobstation: Don't clone IPCs.
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;
}
EnsureComp<PotentialPsionicComponent>(mob.Value); // DeltaV
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob.Value);
cloneMindReturn.Mind = mind;
cloneMindReturn.Parent = uid;
_containerSystem.Insert(mob.Value, clonePod.BodyContainer);
ClonesWaitingForMind.Add(mind, mob.Value);
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
AddComp<ActiveCloningPodComponent>(uid);
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
clonePod.UsedBiomass = cloningCost;
return true;
}
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
{
cloningPod.Status = status;
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
while (query.MoveNext(out var uid, out var _, out var cloning))
{
if (!_powerReceiverSystem.IsPowered(uid))
continue;
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
continue;
cloning.CloningProgress += frameTime;
if (cloning.CloningProgress < cloning.CloningTime)
continue;
if (cloning.FailedClone)
EndFailedCloning(uid, cloning);
else
Eject(uid, cloning);
}
}
/// <summary>
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
/// </summary>
private void OnEmagged(Entity<CloningPodComponent> ent, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(ent.Owner, EmagType.Interaction))
return;
if (!this.IsPowered(ent.Owner, EntityManager))
return;
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), ent.Owner);
args.Handled = true;
}
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
{
if (!Resolve(uid, ref clonePod))
return;
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
return;
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
_containerSystem.Remove(entity, clonePod.BodyContainer);
clonePod.CloningProgress = 0f;
clonePod.UsedBiomass = 0;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
{
clonePod.FailedClone = false;
clonePod.CloningProgress = 0f;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
var transform = Transform(uid);
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
if (HasComp<EmaggedComponent>(uid))
{
_audio.PlayPvs(clonePod.ScreamSound, uid);
Spawn(clonePod.MobSpawnId, transform.Coordinates);
}
Solution bloodSolution = new();
var i = 0;
while (i < 1)
{
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
bloodSolution.AddReagent("Blood", 50);
if (_robustRandom.Prob(0.2f))
i++;
}
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
if (!HasComp<EmaggedComponent>(uid))
{
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int)(clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
}
clonePod.UsedBiomass = 0;
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
public void Reset(RoundRestartCleanupEvent ev)
{
ClonesWaitingForMind.Clear();
}
}

View File

@ -1,382 +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._EE.Silicon.Components; // Goobstation
using Content.Server.Psionics; // DeltaV
using Content.Server.Traits.Assorted; // DeltaV
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 partial class CloningSystem : EntitySystem // DeltaV - Set to partial, see CloningSystem.Metempsychosis.cs
[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))));
}
// Nyano: Adds float karmaBonus
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1, float karmaBonus = 0.25f)
{
if (!Resolve(uid, ref clonePod))
return false;
// DeltaV - This method should use Entity<CloningPodComponent> pod instead
// But I don't want to completely mangle it so we do this here
var podEnt = new Entity<CloningPodComponent>(uid, clonePod);
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 (HasComp<SiliconComponent>(bodyToClone))
return false; // Goobstation: Don't clone IPCs.
// Begin Nyano-code: allow paradox anomalies to be cloned.
var pref = humanoid.LastProfileLoaded;
if (pref == null)
return false;
// End Nyano-code
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);
// Check if they have the uncloneable trait
if (TryComp<UncloneableComponent>(bodyToClone, out _))
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-uncloneable-trait-error"),
InGameICChatType.Speak, false);
}
return false;
}
// 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;
}
_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
// DeltaV - Replaces CloneAppearance with Metem/Clone via FetchAndSpawnMob
var mob = FetchAndSpawnMob(podEnt, pref, speciesPrototype, humanoid, bodyToClone, karmaBonus);
// Nyano - Summary: adds the potential psionic trait to the reanimated mob.
EnsureComp<PotentialPsionicComponent>(mob);
var ev = new CloningEvent(bodyToClone, mob);
RaiseLocalEvent(bodyToClone, ref ev);
if (!ev.NameHandled)
_metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob);
cloneMindReturn.Mind = mind;
cloneMindReturn.Parent = uid;
_containerSystem.Insert(mob, clonePod.BodyContainer);
ClonesWaitingForMind.Add(mind, mob);
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
AddComp<ActiveCloningPodComponent>(uid);
// TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
// Add on special job components to the mob.
if (_jobs.MindTryGetJob(mindEnt, out var prototype))
{
foreach (var special in prototype.Special)
{
if (special is AddComponentSpecial)
special.AfterEquip(mob);
}
}
return true;
}
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
{
cloningPod.Status = status;
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
while (query.MoveNext(out var uid, out var _, out var cloning))
{
if (!_powerReceiverSystem.IsPowered(uid))
continue;
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
continue;
cloning.CloningProgress += frameTime;
if (cloning.CloningProgress < cloning.CloningTime)
continue;
if (cloning.FailedClone)
EndFailedCloning(uid, cloning);
else
Eject(uid, cloning);
if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
RemComp(clone.Value, componentRegistration.Type);
CopyComp(original, clone.Value, sourceComp);
}
}
/// <summary>
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
/// </summary>
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
var cloningEv = new CloningEvent(settings, clone.Value);
RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
// Add equipment first so that SetEntityName also renames the ID card.
if (settings.CopyEquipment != null)
CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
var originalName = Name(original);
if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
originalName = nameModComp.BaseName;
// This will properly set the BaseName and EntityName for the clone.
// Adding the component first before renaming will make sure RefreshNameModifers is called.
// Without this the name would get reverted to Urist.
// If the clone has no name modifiers, NameModifierComponent will be removed again.
EnsureComp<NameModifierComponent>(clone.Value);
_metaData.SetEntityName(clone.Value, originalName);
_adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
return true;
}
/// <summary>
/// Copies the equipment the original has to the clone.
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
/// </summary>
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
{
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
return;
// Iterate over all inventory slots
var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
while (slotEnumerator.NextItem(out var item, out var slot))
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
// Spawn a copy of the item using the original prototype.
// This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
// we use a whitelist and blacklist to be sure to exclude any problematic entities
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
continue;
if (!this.IsPowered(uid, EntityManager))
return;
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
args.Handled = true;
}
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
{
if (!Resolve(uid, ref clonePod))
return;
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
return;
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
_containerSystem.Remove(entity, clonePod.BodyContainer);
clonePod.CloningProgress = 0f;
clonePod.UsedBiomass = 0;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
{
clonePod.FailedClone = false;
clonePod.CloningProgress = 0f;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
var transform = Transform(uid);
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
if (_emag.CheckFlag(uid, EmagType.Interaction))
{
_audio.PlayPvs(clonePod.ScreamSound, uid);
Spawn(clonePod.MobSpawnId, transform.Coordinates);
}
Solution bloodSolution = new();
var i = 0;
while (i < 1)
{
tileMix?.AdjustMoles(Gas.Ammonia, 6f);
bloodSolution.AddReagent("Blood", 50);
if (_robustRandom.Prob(0.2f))
i++;
}
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
if (!_emag.CheckFlag(uid, EmagType.Interaction))
{
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
}
clonePod.UsedBiomass = 0;
RemCompDeferred<ActiveCloningPodComponent>(uid);
}
public void Reset(RoundRestartCleanupEvent ev)
{
ClonesWaitingForMind.Clear();
var prototype = MetaData(item).EntityPrototype;
if (prototype != null)
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
}
}
}

View File

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

View File

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

View File

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

View File

@ -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;
@ -33,9 +34,10 @@ namespace Content.Server.Forensics
{
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
SubscribeLocalEvent<FiberComponent, ContactInteractionEvent>(OnFiberInteract); // DeltaV
SubscribeLocalEvent<FiberComponent, MapInitEvent>(OnFiberInit); // DeltaV #1455 - unique glove fibers
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit);
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit);
SubscribeLocalEvent<FiberComponent, MapInitEvent>(OnFiberInit, after: [typeof(BloodstreamSystem)]); // DeltaV #1455 - unique glove fibers
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);
@ -81,18 +83,20 @@ 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)
Log.Debug($"Init DNA {Name(ent.Owner)} {ent.Comp.DNA}");
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);
}
}
@ -100,7 +104,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)
@ -119,7 +123,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);
}
}
@ -331,6 +335,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;
@ -338,6 +345,36 @@ 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);
Log.Debug($"Randomize DNA {Name(ent.Owner)} {ent.Comp.DNA}");
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>
@ -346,7 +383,7 @@ namespace Content.Server.Forensics
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
{
if (TryComp<DnaComponent>(donor, out var donorComp))
if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
{
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
recipientComp.DNAs.Add(donorComp.DNA);

View File

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

View File

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

View File

@ -279,7 +279,7 @@ public sealed partial class GhostRoleSystem : EntitySystem // Converted to parti
if (player.AttachedEntity == null || !HasComp<GhostComponent>(player.AttachedEntity))
return false;
if (Takeover(player, identifier))
if (TryTakeover(player, identifier)) // DeltaV - prevent taking ghost roles in lobby
{
// takeover successful, we have a winner! remove the winner from other raffles they might be in
LeaveAllRaffles(player);

View File

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

View File

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

View File

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

View File

@ -15,8 +15,5 @@ namespace Content.Server.Medical.Components
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float CloningFailChanceMultiplier = 1f;
// Nyano, needed for Metem Machine.
public float MetemKarmaBonus = 0.25f;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
using Content.Server.GameTicking;
using Content.Server.Shuttles.Systems;
using Content.Shared._DV.CustomObjectiveSummary; // DeltaV
using Content.Shared.Cuffs.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
@ -12,10 +13,11 @@ using Robust.Shared.Random;
using System.Linq;
using System.Text;
using Content.Server.Objectives.Commands;
using Content.Shared._DV.CustomObjectiveSummary; // DeltaV
using Content.Shared.CCVar;
using Content.Shared.Prototypes;
using Content.Shared.Roles.Jobs;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
namespace Content.Server.Objectives;
@ -28,15 +30,20 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly SharedJobSystem _job = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private IEnumerable<string>? _objectives;
private bool _showGreentext;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
Subs.CVar(_cfg, CCVars.GameShowGreentext, value => _showGreentext = value, true);
_prototypeManager.PrototypesReloaded += CreateCompletions;
}
@ -163,8 +170,11 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
totalObjectives++;
agentSummary.Append("- ");
/* Begin DeltaV removal - Removed greentext
if (progress > 0.99f)
if (!_showGreentext)
{
agentSummary.AppendLine(objectiveTitle);
}
else if (progress > 0.99f)
{
agentSummary.AppendLine(Loc.GetString(
"objectives-objective-success",
@ -182,13 +192,6 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
("markupColor", "red")
));
}
End DeltaV removal */
// Begin DeltaV Additions - Generic objective
agentSummary.AppendLine(Loc.GetString(
"objectives-objective",
("objective", objectiveTitle)
));
// End DeltaV Additions
}
}

View File

@ -1033,6 +1033,9 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
EnsureComp<LightCycleComponent>(mapUid);
EnsureComp<SunShadowComponent>(mapUid);
EnsureComp<SunShadowCycleComponent>(mapUid);
var moles = new float[Atmospherics.AdjustedNumberOfGases];
moles[(int) Gas.Oxygen] = 21.824779f;
moles[(int) Gas.Nitrogen] = 82.10312f;

View File

@ -6,8 +6,20 @@ namespace Content.Server.Singularity.Components
[RegisterComponent]
public sealed partial class SinguloFoodComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("energy")]
public float Energy { get; set; } = 1f;
/// <summary>
/// Flat adjustment to the singularity's energy when this entity is eaten by the event horizon.
/// </summary>
[DataField]
public float Energy = 1f;
/// <summary>
/// Multiplier applied to singularity's energy.
/// 1.0 = no change, 0.97 = 3% reduction, 1.05 = 5% increase
/// </summary>
/// /// <remarks>
/// This is calculated using the singularity's energy level before <see cref="Energy"/> has been added.
/// </remarks>
[DataField]
public float EnergyFactor = 1f;
}
}

View File

@ -306,7 +306,12 @@ public sealed class SingularitySystem : SharedSingularitySystem
public void OnConsumed(EntityUid uid, SinguloFoodComponent comp, ref EventHorizonConsumedEntityEvent args)
{
if (EntityManager.TryGetComponent<SingularityComponent>(args.EventHorizonUid, out var singulo))
AdjustEnergy(args.EventHorizonUid, comp.Energy, singularity: singulo);
{
// Calculate the percentage change (positive or negative)
var percentageChange = singulo.Energy * (comp.EnergyFactor - 1f);
// Apply both the flat and percentage changes
AdjustEnergy(args.EventHorizonUid, comp.Energy + percentageChange, singularity: singulo);
}
}
/// <summary>

View File

@ -14,6 +14,8 @@ public sealed class AddAccentClothingSystem : EntitySystem
SubscribeLocalEvent<AddAccentClothingComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
}
// TODO: Turn this into a relay event.
private void OnGotEquipped(EntityUid uid, AddAccentClothingComponent component, ref ClothingGotEquippedEvent args)
{
// does the user already has this accent?

View File

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Mind;
using Content.Shared.Store;
using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;
@ -117,7 +118,7 @@ public sealed partial class StoreSystem
if (listing.Conditions != null)
{
var args = new ListingConditionArgs(buyer, storeEntity, listing, EntityManager);
var args = new ListingConditionArgs(GetBuyerMind(buyer), storeEntity, listing, EntityManager);
var conditionsMet = true;
foreach (var condition in listing.Conditions)
@ -137,6 +138,19 @@ public sealed partial class StoreSystem
}
}
/// <summary>
/// Returns the entity's mind entity, if it has one, to be used for listing conditions.
/// If it doesn't have one, or is a mind entity already, it returns itself.
/// </summary>
/// <param name="buyer">The buying entity.</param>
public EntityUid GetBuyerMind(EntityUid buyer)
{
if (!HasComp<MindComponent>(buyer) && _mind.TryGetMind(buyer, out var buyerMind, out var _))
return buyerMind;
return buyer;
}
/// <summary>
/// Checks if a listing appears in a list of given categories
/// </summary>

View File

@ -146,7 +146,7 @@ public sealed partial class StoreSystem
//condition checking because why not
if (listing.Conditions != null)
{
var args = new ListingConditionArgs(component.AccountOwner ?? buyer, uid, listing, EntityManager);
var args = new ListingConditionArgs(component.AccountOwner ?? GetBuyerMind(buyer), uid, listing, EntityManager);
var conditionsMet = listing.Conditions.All(condition => condition.Condition(args));
if (!conditionsMet)

View File

@ -0,0 +1,20 @@
using Content.Shared.Cloning.Events;
using Content.Shared.Traits.Assorted;
namespace Content.Server.Traits.Assorted;
public sealed class UnrevivableSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<UnrevivableComponent, CloningAttemptEvent>(OnCloningAttempt);
}
private void OnCloningAttempt(Entity<UnrevivableComponent> ent, ref CloningAttemptEvent args)
{
if (!ent.Comp.Cloneable)
args.Cancelled = true;
}
}

View File

@ -1,19 +1,12 @@
using System.Linq;
using System.Numerics;
using Content.Server.Advertise;
using Content.Server.Advertise.Components;
using Content.Server.Cargo.Systems;
using Content.Server.Emp;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Actions;
using Content.Shared.Damage;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Emp;
using Content.Shared.Popups;
using Content.Shared.Power;
@ -21,7 +14,6 @@ using Content.Shared.Throwing;
using Content.Shared.UserInterface;
using Content.Shared.VendingMachines;
using Content.Shared.Wall;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@ -32,14 +24,9 @@ namespace Content.Server.VendingMachines
public sealed class VendingMachineSystem : SharedVendingMachineSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly PricingSystem _pricing = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
[Dependency] private readonly EmagSystem _emag = default!;
private const float WallVendEjectDistanceFromWall = 1f;
@ -55,11 +42,6 @@ namespace Content.Server.VendingMachines
SubscribeLocalEvent<VendingMachineComponent, ActivatableUIOpenAttemptEvent>(OnActivatableUIOpenAttempt);
Subs.BuiEvents<VendingMachineComponent>(VendingMachineUiKey.Key, subs =>
{
subs.Event<VendingMachineEjectMessage>(OnInventoryEjectMessage);
});
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
SubscribeLocalEvent<VendingMachineComponent, RestockDoAfterEvent>(OnDoAfter);
@ -91,7 +73,7 @@ namespace Content.Server.VendingMachines
if (HasComp<ApcPowerReceiverComponent>(uid))
{
TryUpdateVisualState(uid, component);
TryUpdateVisualState((uid, component));
}
}
@ -101,26 +83,15 @@ namespace Content.Server.VendingMachines
args.Cancel();
}
private void OnInventoryEjectMessage(EntityUid uid, VendingMachineComponent component, VendingMachineEjectMessage args)
{
if (!this.IsPowered(uid, EntityManager))
return;
if (args.Actor is not { Valid: true } entity || Deleted(entity))
return;
AuthorizedVend(uid, entity, args.Type, args.ID, component);
}
private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args)
{
TryUpdateVisualState(uid, component);
TryUpdateVisualState((uid, component));
}
private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs)
{
vendComponent.Broken = true;
TryUpdateVisualState(uid, vendComponent);
TryUpdateVisualState((uid, vendComponent));
}
private void OnDamageChanged(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args)
@ -128,7 +99,7 @@ namespace Content.Server.VendingMachines
if (!args.DamageIncreased && component.Broken)
{
component.Broken = false;
TryUpdateVisualState(uid, component);
TryUpdateVisualState((uid, component));
return;
}
@ -139,8 +110,11 @@ namespace Content.Server.VendingMachines
if (args.DamageIncreased && args.DamageDelta.GetTotal() >= component.DispenseOnHitThreshold &&
_random.Prob(component.DispenseOnHitChance.Value))
{
if (component.DispenseOnHitCooldown > 0f)
component.DispenseOnHitCoolingDown = true;
if (component.DispenseOnHitCooldown != null)
{
component.DispenseOnHitEnd = Timing.CurTime + component.DispenseOnHitCooldown.Value;
}
EjectRandom(uid, throwItem: true, forceEject: true, component);
}
}
@ -199,145 +173,6 @@ namespace Content.Server.VendingMachines
Dirty(uid, component);
}
public void Deny(EntityUid uid, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
if (vendComponent.Denying)
return;
vendComponent.Denying = true;
Audio.PlayPvs(vendComponent.SoundDeny, uid, AudioParams.Default.WithVolume(-2f));
TryUpdateVisualState(uid, vendComponent);
}
/// <summary>
/// Checks if the user is authorized to use this vending machine
/// </summary>
/// <param name="uid"></param>
/// <param name="sender">Entity trying to use the vending machine</param>
/// <param name="vendComponent"></param>
public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return false;
if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
return true;
if (_accessReader.IsAllowed(sender, uid, accessReader))
return true;
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid);
Deny(uid, vendComponent);
return false;
}
/// <summary>
/// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting
/// or the item doesn't exist in its inventory.
/// </summary>
/// <param name="uid"></param>
/// <param name="type">The type of inventory the item is from</param>
/// <param name="itemId">The prototype ID of the item</param>
/// <param name="throwItem">Whether the item should be thrown in a random direction after ejection</param>
/// <param name="vendComponent"></param>
public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
if (vendComponent.Ejecting || vendComponent.Broken || !this.IsPowered(uid, EntityManager))
{
return;
}
var entry = GetEntry(uid, itemId, type, vendComponent);
if (entry == null)
{
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid);
Deny(uid, vendComponent);
return;
}
if (entry.Amount <= 0)
{
Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid);
Deny(uid, vendComponent);
return;
}
if (string.IsNullOrEmpty(entry.ID))
return;
// Start Ejecting, and prevent users from ordering while anim playing
vendComponent.Ejecting = true;
vendComponent.NextItemToEject = entry.ID;
vendComponent.ThrowNextItem = throwItem;
if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent))
_speakOnUIClosed.TrySetFlag((uid, speakComponent));
entry.Amount--;
Dirty(uid, vendComponent);
TryUpdateVisualState(uid, vendComponent);
Audio.PlayPvs(vendComponent.SoundVend, uid);
}
/// <summary>
/// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true
/// </summary>
/// <param name="uid"></param>
/// <param name="sender">Entity that is trying to use the vending machine</param>
/// <param name="type">The type of inventory the item is from</param>
/// <param name="itemId">The prototype ID of the item</param>
/// <param name="component"></param>
public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component)
{
if (IsAuthorized(uid, sender, component))
{
TryEjectVendorItem(uid, type, itemId, component.CanShoot, component);
}
}
/// <summary>
/// Tries to update the visuals of the component based on its current state.
/// </summary>
public void TryUpdateVisualState(EntityUid uid, VendingMachineComponent? vendComponent = null)
{
if (!Resolve(uid, ref vendComponent))
return;
var finalState = VendingMachineVisualState.Normal;
if (vendComponent.Broken)
{
finalState = VendingMachineVisualState.Broken;
}
else if (vendComponent.Ejecting)
{
finalState = VendingMachineVisualState.Eject;
}
else if (vendComponent.Denying)
{
finalState = VendingMachineVisualState.Deny;
}
else if (!this.IsPowered(uid, EntityManager))
{
finalState = VendingMachineVisualState.Off;
}
if (_light.TryGetLight(uid, out var pointlight))
{
var lightState = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off;
_light.SetEnabled(uid, lightState, pointlight);
}
_appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState);
}
/// <summary>
/// Ejects a random item from the available stock. Will do nothing if the vending machine is empty.
/// </summary>
@ -367,18 +202,18 @@ namespace Content.Server.VendingMachines
}
else
{
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent);
TryEjectVendorItem(uid, item.Type, item.ID, throwItem, user: null, vendComponent: vendComponent);
}
}
private void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
protected override void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false)
{
if (!Resolve(uid, ref vendComponent))
return;
// No need to update the visual state because we never changed it during a forced eject
if (!forceEject)
TryUpdateVisualState(uid, vendComponent);
TryUpdateVisualState((uid, vendComponent));
if (string.IsNullOrEmpty(vendComponent.NextItemToEject))
{
@ -411,68 +246,17 @@ namespace Content.Server.VendingMachines
vendComponent.ThrowNextItem = false;
}
private VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null)
{
if (!Resolve(uid, ref component))
return null;
if (type == InventoryType.Emagged && _emag.CheckFlag(uid, EmagType.Interaction))
return component.EmaggedInventory.GetValueOrDefault(entryId);
if (type == InventoryType.Contraband && component.Contraband)
return component.ContrabandInventory.GetValueOrDefault(entryId);
return component.Inventory.GetValueOrDefault(entryId);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<VendingMachineComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Ejecting)
{
comp.EjectAccumulator += frameTime;
if (comp.EjectAccumulator >= comp.EjectDelay)
{
comp.EjectAccumulator = 0f;
comp.Ejecting = false;
EjectItem(uid, comp);
}
}
if (comp.Denying)
{
comp.DenyAccumulator += frameTime;
if (comp.DenyAccumulator >= comp.DenyDelay)
{
comp.DenyAccumulator = 0f;
comp.Denying = false;
TryUpdateVisualState(uid, comp);
}
}
if (comp.DispenseOnHitCoolingDown)
{
comp.DispenseOnHitAccumulator += frameTime;
if (comp.DispenseOnHitAccumulator >= comp.DispenseOnHitCooldown)
{
comp.DispenseOnHitAccumulator = 0f;
comp.DispenseOnHitCoolingDown = false;
}
}
}
var disabled = EntityQueryEnumerator<EmpDisabledComponent, VendingMachineComponent>();
while (disabled.MoveNext(out var uid, out _, out var comp))
{
if (comp.NextEmpEject < _timing.CurTime)
{
EjectRandom(uid, true, false, comp);
comp.NextEmpEject += TimeSpan.FromSeconds(5 * comp.EjectDelay);
comp.NextEmpEject += (5 * comp.EjectDelay);
}
}
}
@ -485,7 +269,7 @@ namespace Content.Server.VendingMachines
RestockInventoryFromPrototype(uid, vendComponent);
Dirty(uid, vendComponent);
TryUpdateVisualState(uid, vendComponent);
TryUpdateVisualState((uid, vendComponent));
}
private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)

View File

@ -0,0 +1,19 @@
using Content.Shared.Inventory;
namespace Content.Server.VoiceTrigger;
/// <summary>
/// Entities with this component, Containers, and TriggerOnVoiceComponent will insert any item or extract the spoken item after the TriggerOnVoiceComponent has been activated
/// </summary>
[RegisterComponent]
public sealed partial class StorageVoiceControlComponent : Component
{
/// <summary>
/// Used to determine which slots the component can be used in.
/// <remarks>
/// If not set, the component can be used anywhere, even while inside other containers.
/// </remarks>
/// </summary>
[DataField]
public SlotFlags? AllowedSlots;
}

View File

@ -0,0 +1,98 @@
using Content.Server.Hands.Systems;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Content.Shared.Storage;
using Robust.Server.Containers;
namespace Content.Server.VoiceTrigger;
/// <summary>
/// Allows storages to be manipulated using voice commands.
/// </summary>
public sealed class StorageVoiceControlSystem : EntitySystem
{
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly HandsSystem _hands = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly StorageSystem _storage = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StorageVoiceControlComponent, VoiceTriggeredEvent>(VoiceTriggered);
}
private void VoiceTriggered(Entity<StorageVoiceControlComponent> ent, ref VoiceTriggeredEvent args)
{
// Check if the component has any slot restrictions via AllowedSlots
// If it has slot restrictions, check if the item is in a slot that is allowed
if (ent.Comp.AllowedSlots != null && _inventory.TryGetContainingSlot(ent.Owner, out var itemSlot) &&
(itemSlot.SlotFlags & ent.Comp.AllowedSlots) == 0)
return;
// Don't do anything if there is no message
if (args.Message == null)
return;
// Get the storage component
if (!TryComp<StorageComponent>(ent, out var storage))
return;
// Get the hands component
if (!TryComp<HandsComponent>(args.Source, out var hands))
return;
// If the player has something in their hands, try to insert it into the storage
if (hands.ActiveHand != null && hands.ActiveHand.HeldEntity.HasValue)
{
// Disallow insertion and provide a reason why if the person decides to insert the item into itself
if (ent.Owner.Equals(hands.ActiveHand.HeldEntity.Value))
{
_popup.PopupEntity(Loc.GetString("comp-storagevoicecontrol-self-insert", ("entity", hands.ActiveHand.HeldEntity.Value)), ent, args.Source);
return;
}
if (_storage.CanInsert(ent, hands.ActiveHand.HeldEntity.Value, out var failedReason))
{
// We adminlog before insertion, otherwise the logger will attempt to pull info on an entity that no longer is present and throw an exception
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Source)} inserted {ToPrettyString(hands.ActiveHand.HeldEntity.Value)} into {ToPrettyString(ent)} via voice control");
_storage.Insert(ent, hands.ActiveHand.HeldEntity.Value, out _);
return;
}
{
// Tell the player the reason why the item couldn't be inserted
if (failedReason == null)
return;
_popup.PopupEntity(Loc.GetString(failedReason), ent, args.Source);
_adminLogger.Add(LogType.Action,
LogImpact.Low,
$"{ToPrettyString(args.Source)} failed to insert {ToPrettyString(hands.ActiveHand.HeldEntity.Value)} into {ToPrettyString(ent)} via voice control");
}
return;
}
// If otherwise, we're retrieving an item, so check all the items currently in the attached storage
foreach (var item in storage.Container.ContainedEntities)
{
// Get the item's name
var itemName = MetaData(item).EntityName;
// The message doesn't match the item name the requestor requested, skip and move on to the next item
if (!args.Message.Contains(itemName, StringComparison.InvariantCultureIgnoreCase))
continue;
// We found the item we want, so draw it from storage and place it into the player's hands
if (storage.Container.ContainedEntities.Count != 0)
{
_container.RemoveEntity(ent, item);
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.Source)} retrieved {ToPrettyString(item)} from {ToPrettyString(ent)} via voice control");
_hands.TryPickup(args.Source, item, handsComp: hands);
break;
}
}
}
}

View File

@ -1,4 +1,3 @@
using Content.Server.Actions;
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
using Content.Server.Chat;
@ -14,7 +13,7 @@ using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
using Content.Server.Speech.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Abilities.Psionics;
using Content.Shared.Abilities.Psionics; // DeltaV
using Content.Shared.CombatMode;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Damage;
@ -26,12 +25,12 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.NPC.Components;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.NPC.Components; // DeltaV
using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.AnimalHusbandry;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Weapons.Melee;
using Content.Shared.Zombies;
using Content.Shared.Prying.Components;
@ -61,8 +60,8 @@ public sealed partial class ZombieSystem
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
/// <summary>
/// Handles an entity turning into a zombie when they die or go into crit
@ -235,7 +234,7 @@ public sealed partial class ZombieSystem
_faction.ClearFactions(target, dirty: false);
_faction.AddFaction(target, "Zombie");
EnsureComp<NoFriendlyFireComponent>(target); // prevent shitters biting other zombies
EnsureComp<NoFriendlyFireComponent>(target); // DeltaV - prevent shitters biting other zombies
//gives it the funny "Zombie ___" name.
_nameMod.RefreshNameModifiers(target);
@ -252,7 +251,7 @@ public sealed partial class ZombieSystem
if (hasMind && _mind.TryGetSession(mindId, out var session))
{
//Zombie role for player manifest
_roles.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
_role.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
//Greeting message for new bebe zombers
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));

View File

@ -5,18 +5,20 @@ using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Emoting.Systems;
using Content.Server.Speech.EntitySystems;
using Content.Server.Roles;
using Content.Shared.Anomaly.Components;
using Content.Shared.Bed.Sleep;
using Content.Shared.Cloning;
using Content.Shared.Cloning.Events;
using Content.Shared.Damage;
using Content.Shared.Humanoid;
using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
@ -38,7 +40,7 @@ namespace Content.Server.Zombies
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
public const SlotFlags ProtectiveSlots =
SlotFlags.FEET |
@ -63,6 +65,8 @@ namespace Content.Server.Zombies
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
SubscribeLocalEvent<ZombieComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<ZombieComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
@ -264,7 +268,7 @@ namespace Content.Server.Zombies
/// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param>
/// <param name="zombiecomp"></param>
/// <remarks>
/// this currently only restore the name and skin/eye color from before zombified
/// this currently only restore the skin/eye color from before zombified
/// TODO: completely rethink how zombies are done to allow reversal.
/// </remarks>
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
@ -284,14 +288,25 @@ namespace Content.Server.Zombies
_humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
_bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
_nameMod.RefreshNameModifiers(target);
return true;
}
private void OnZombieCloning(EntityUid uid, ZombieComponent zombiecomp, ref CloningEvent args)
private void OnZombieCloning(Entity<ZombieComponent> ent, ref CloningEvent args)
{
if (UnZombify(args.Source, args.Target, zombiecomp))
args.NameHandled = true;
UnZombify(ent.Owner, args.CloneUid, ent.Comp);
}
// Make sure players that enter a zombie (for example via a ghost role or the mind swap spell) count as an antagonist.
private void OnMindAdded(Entity<ZombieComponent> ent, ref MindAddedMessage args)
{
if (!_role.MindHasRole<ZombieRoleComponent>(args.Mind))
_role.MindAddRole(args.Mind, "MindRoleZombie", mind: args.Mind.Comp);
}
// Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
{
_role.MindTryRemoveRole<ZombieRoleComponent>(args.Mind);
}
}
}

View File

@ -1,5 +1,4 @@
using Content.Server.Bible.Components;
using Content.Server._DV.Cloning;
using Content.Shared.Abilities.Psionics;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
@ -118,7 +117,7 @@ public sealed class SacrificialAltarSystem : SharedSacrificialAltarSystem
return;
}
if (!HasComp<HumanoidAppearanceComponent>(target) && !HasComp<MetempsychosisKarmaComponent>(target))
if (!HasComp<HumanoidAppearanceComponent>(target))
{
_popup.PopupEntity(Loc.GetString("altar-failure-reason-target-humanoid", ("target", target)), ent, user, PopupType.SmallCaution);
return;

View File

@ -1,172 +0,0 @@
using Content.Server._DV.Cloning;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Speech;
using Content.Shared.Emoting;
using Content.Shared.Damage.ForceSay;
using Content.Shared.SSDIndicator;
using Content.Server.Speech.Components;
using Content.Server.Ghost.Roles.Components;
using Content.Server.StationEvents.Components;
using Content.Server.Psionics;
using Robust.Shared.Random;
using Content.Shared.Mind.Components;
using Content.Shared.Tag;
using Content.Shared.Cloning;
using Content.Shared.Random.Helpers;
using Robust.Shared.GameObjects.Components.Localization;
namespace Content.Server.Cloning;
public sealed partial class CloningSystem
{
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly GrammarSystem _grammar = default!;
/// <summary>
/// Gets the entity prototype to spawn for a clone based on karma and chance calculations.
/// </summary>
private string GetSpawnEntity(Entity<MetempsychoticMachineComponent> ent, float karmaBonus, SpeciesPrototype oldSpecies, out SpeciesPrototype? species, int karma = 0)
{
// First time being cloned - return original species
if (karma == 0)
{
species = oldSpecies;
return oldSpecies.Prototype;
}
var chance = ent.Comp.HumanoidBaseChance + karmaBonus;
chance -= (1 - ent.Comp.HumanoidBaseChance) * karma;
// Perfect clone chance
if (chance > 1 && _robustRandom.Prob(chance - 1))
{
species = oldSpecies;
return oldSpecies.Prototype;
}
// Roll for humanoid vs non-humanoid
chance = Math.Clamp(chance, 0, 1);
if (_robustRandom.Prob(chance))
{
if (_prototype.TryIndex(ent.Comp.MetempsychoticHumanoidPool, out var humanoidPool))
{
var protoId = humanoidPool.Pick();
if (_prototype.TryIndex<SpeciesPrototype>(protoId, out var speciesPrototype))
{
species = speciesPrototype;
return speciesPrototype.Prototype;
}
}
}
else if (_prototype.TryIndex(ent.Comp.MetempsychoticNonHumanoidPool, out var nonHumanoidPool))
{
// For non-humanoids, return the entity prototype directly
species = null;
return nonHumanoidPool.Pick();
}
// Fallback to original species if prototype indexing fails
Log.Error("Failed to get valid clone type - falling back to original species");
species = oldSpecies;
return oldSpecies.Prototype;
}
/// <summary>
/// Handles fetching the mob and managing appearance for cloning with metempsychosis mechanics
/// </summary>
private EntityUid FetchAndSpawnMob(
Entity<CloningPodComponent> pod,
HumanoidCharacterProfile pref,
SpeciesPrototype speciesPrototype,
HumanoidAppearanceComponent humanoid,
EntityUid bodyToClone,
float karmaBonus)
{
List<Sex> sexes = [];
var switchingSpecies = false;
var applyKarma = false;
var toSpawn = speciesPrototype.Prototype;
// Get existing karma score or start at 0
var karmaScore = 0;
if (TryComp<MetempsychosisKarmaComponent>(bodyToClone, out var oldKarma))
{
karmaScore = oldKarma.Score;
}
if (TryComp<MetempsychoticMachineComponent>(pod.Owner, out var metem))
{
var metemEntity = new Entity<MetempsychoticMachineComponent>(pod.Owner, metem);
toSpawn = GetSpawnEntity(metemEntity, karmaBonus, speciesPrototype, out var newSpecies, karmaScore);
applyKarma = true;
if (newSpecies != null)
{
sexes = newSpecies.Sexes;
speciesPrototype = newSpecies;
if (speciesPrototype.ID != newSpecies.ID)
switchingSpecies = true;
}
}
var mob = Spawn(toSpawn, _transformSystem.GetMapCoordinates(pod.Owner));
// Only try to handle humanoid appearance if we have a humanoid component
if (TryComp<HumanoidAppearanceComponent>(mob, out var newHumanoid))
{
if (switchingSpecies || HasComp<MetempsychosisKarmaComponent>(bodyToClone))
{
pref = HumanoidCharacterProfile.RandomWithSpecies(newHumanoid.Species);
if (sexes.Contains(humanoid.Sex))
pref = pref.WithSex(humanoid.Sex);
pref = pref.WithGender(humanoid.Gender);
pref = pref.WithAge(humanoid.Age);
}
_humanoidSystem.LoadProfile(mob, pref);
}
if (applyKarma)
{
var karma = EnsureComp<MetempsychosisKarmaComponent>(mob);
karma.Score = karmaScore + 1; // Increment karma score
}
var ev = new CloningEvent(bodyToClone, mob);
RaiseLocalEvent(bodyToClone, ref ev);
if (!ev.NameHandled)
_metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
var grammar = EnsureComp<GrammarComponent>(mob);
var grammarEnt = new Entity<GrammarComponent>(mob, grammar);
_grammar.SetProperNoun(grammarEnt, true);
_grammar.SetGender(grammarEnt, humanoid.Gender);
Dirty(mob, grammar);
SetupBasicComponents(mob);
return mob;
}
// I hate this
private void SetupBasicComponents(EntityUid mob)
{
EnsureComp<PotentialPsionicComponent>(mob);
EnsureComp<SpeechComponent>(mob);
EnsureComp<DamageForceSayComponent>(mob);
EnsureComp<EmotingComponent>(mob);
EnsureComp<MindContainerComponent>(mob);
EnsureComp<SSDIndicatorComponent>(mob);
RemComp<ReplacementAccentComponent>(mob);
RemComp<MonkeyAccentComponent>(mob);
RemComp<SentienceTargetComponent>(mob);
RemComp<GhostTakeoverAvailableComponent>(mob);
_tag.AddTag(mob, "DoorBumpOpener");
}
}

View File

@ -1,11 +0,0 @@
namespace Content.Server._DV.Cloning;
/// <summary>
/// This tracks how many times you have already been cloned and lowers your chance of getting a humanoid each time.
/// </summary>
[RegisterComponent]
public sealed partial class MetempsychosisKarmaComponent : Component
{
[DataField]
public int Score;
}

View File

@ -1,27 +0,0 @@
using Content.Shared.Random;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Cloning;
[RegisterComponent]
public sealed partial class MetempsychoticMachineComponent : Component
{
/// <summary>
/// Base probability of remaining humanoid during cloning. Higher karma reduces this chance.
/// </summary>
[DataField]
public float HumanoidBaseChance = 0.75f;
/// <summary>
/// Species prototypes pool to use for humanoids.
/// </summary>
[DataField]
public ProtoId<WeightedRandomPrototype> MetempsychoticHumanoidPool = "MetempsychoticHumanoidPool";
/// <summary>
/// Entitiy prototypes pool to use for non-humanoids.
/// </summary>
[DataField]
public ProtoId<WeightedRandomPrototype> MetempsychoticNonHumanoidPool = "MetempsychoticNonhumanoidPool";
}

View File

@ -1,7 +1,14 @@
using Content.Shared.Cloning;
using Robust.Shared.Prototypes;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// Creates a paradox anomaly of a random person when taken by a player.
/// </summary>
[RegisterComponent]
public sealed partial class ParadoxClonerRuleComponent : Component;
public sealed partial class ParadoxClonerRuleComponent : Component
{
[DataField]
public ProtoId<CloningSettingsPrototype> CloningSettings = "BaseClone";
}

View File

@ -1,10 +1,12 @@
using Content.Server.Antag;
using Content.Server.Cloning;
using Content.Server.GameTicking.Rules;
using Content.Server.Psionics;
using Content.Server.Station.Systems;
using Content.Server.StationEvents.Components;
using Content.Server.StationEvents.Events;
using Content.Server.Terminator.Systems;
using Content.Shared.Cloning;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Mind;
@ -25,11 +27,13 @@ namespace Content.Server.StationEvents.Events;
/// </summary>
public sealed class ParadoxClonerRule : StationEventSystem<ParadoxClonerRuleComponent>
{
[Dependency] private readonly CloningSystem _cloning = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PsionicsSystem _psionics = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
[Dependency] private readonly TerminatorSystem _terminator = default!;
@ -46,30 +50,30 @@ public sealed class ParadoxClonerRule : StationEventSystem<ParadoxClonerRuleComp
if (args.Session?.AttachedEntity is not {} spawner)
return;
var settings = _proto.Index(ent.Comp.CloningSettings);
Log.Debug($"Rule {ToPrettyString(ent)} creating a paradox anomaly using spawner {spawner}");
if (!TrySpawnParadoxAnomaly(spawner, out var clone))
if (!TrySpawnParadoxAnomaly(spawner, settings, out var clone))
return;
Log.Info($"Created paradox anomaly {ToPrettyString(clone):clone}");
args.Entity = clone;
}
private bool TrySpawnParadoxAnomaly(EntityUid spawner, [NotNullWhen(true)] out EntityUid? clone)
private bool TrySpawnParadoxAnomaly(EntityUid spawner, CloningSettingsPrototype settings, [NotNullWhen(true)] out EntityUid? clone)
{
clone = null;
// Get a list of potential candidates
var candidates = new List<(EntityUid, EntityUid, ProtoId<JobPrototype>, HumanoidCharacterProfile)>();
var candidates = new List<(EntityUid, EntityUid, ProtoId<JobPrototype>)>();
var query = EntityQueryEnumerator<MindContainerComponent, HumanoidAppearanceComponent>();
while (query.MoveNext(out var uid, out var mindContainer, out var humanoid))
{
if (humanoid.LastProfileLoaded is {} profile &&
mindContainer.Mind is {} mindId &&
if (mindContainer.Mind is {} mindId &&
!_role.MindIsAntagonist(mindId) &&
_role.MindHasRole<JobRoleComponent>(mindId, out var role) &&
role?.Comp1.JobPrototype is {} job)
{
candidates.Add((uid, mindId, job, profile));
candidates.Add((uid, mindId, job));
}
}
@ -79,21 +83,31 @@ public sealed class ParadoxClonerRule : StationEventSystem<ParadoxClonerRuleComp
return false;
}
clone = SpawnParadoxAnomaly(spawner, candidates);
return true;
// tries20 my beloved
for (int i = 0; i < 20; i++)
{
clone = SpawnParadoxAnomaly(spawner, settings, candidates);
if (clone != null)
return true;
}
Log.Error("Failed to clone any eligible player!");
return false;
}
private EntityUid SpawnParadoxAnomaly(EntityUid spawner, List<(EntityUid, EntityUid, ProtoId<JobPrototype>, HumanoidCharacterProfile)> candidates)
private EntityUid? SpawnParadoxAnomaly(EntityUid spawner, CloningSettingsPrototype settings, List<(EntityUid, EntityUid, ProtoId<JobPrototype>)> candidates)
{
// Select a candidate.
var (uid, mindId, job, profile) = _random.Pick(candidates);
var (uid, mindId, job) = _random.Pick(candidates);
// Spawn the clone.
var coords = Transform(spawner).Coordinates;
var coords = _transform.GetMapCoordinates(spawner);
var station = _station.GetOwningStation(uid);
var spawned = _stationSpawning.SpawnPlayerMob(coords, job, profile, station);
if (!_cloning.TryCloning(uid, coords, settings, out var mob))
return null;
// Set the kill target to the chosen player
var spawned = mob.Value;
_terminator.SetTarget(spawned, mindId);
// guaranteed psionic power

View File

@ -1,13 +1,9 @@
using Content.Server.Advertise;
using Content.Server.Advertise.Components;
using Content.Shared._DV.VendingMachines;
namespace Content.Server._DV.VendingMachines;
public sealed class ShopVendorSystem : SharedShopVendorSystem
{
[Dependency] private readonly SpeakOnUIClosedSystem _speakOnUIClosed = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
@ -38,10 +34,4 @@ public sealed class ShopVendorSystem : SharedShopVendorSystem
}
}
}
protected override void AfterPurchase(Entity<ShopVendorComponent> ent)
{
if (TryComp<SpeakOnUIClosedComponent>(ent, out var speak))
_speakOnUIClosed.TrySetFlag((ent.Owner, speak));
}
}

View File

@ -53,10 +53,13 @@ public sealed partial class GhettoSurgerySystem : EntitySystem
private void OnSharpShutdown(Entity<SharpComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.HadScalpel)
if (!ent.Comp.HadSurgeryTool)
RemComp<SurgeryToolComponent>(ent);
if (!ent.Comp.HadScalpel)
RemComp<ScalpelComponent>(ent);
if (ent.Comp.HadBoneSaw)
if (!ent.Comp.HadBoneSaw)
RemComp<BoneSawComponent>(ent);
}
}

View File

@ -185,7 +185,8 @@ public sealed class SurgerySystem : SharedSurgerySystem
}
}
dnas.Remove(target.Comp1.DNA);
if (target.Comp1.DNA is {} dna)
dnas.Remove(dna);
return total + dnas.Count * target.Comp2.CrossContaminationDirtinessLevel;
}
@ -210,8 +211,11 @@ public sealed class SurgerySystem : SharedSurgerySystem
Dirty(ent, dirtiness);
}
private void AddDNA(EntityUid ent, string dna)
private void AddDNA(EntityUid ent, string? dna)
{
if (dna == null)
return;
var contamination = EnsureComp<SurgeryCrossContaminationComponent>(ent);
contamination.DNAs.Add(dna);
}
@ -268,9 +272,11 @@ public sealed class SurgerySystem : SharedSurgerySystem
private void OnSurgerySpecialDamageChange(Entity<SurgerySpecialDamageChangeEffectComponent> ent, ref SurgeryStepDamageChangeEvent args)
{
// Begin DeltaV - this shit was killed
// Im killing this shit soon too, inshallah.
if (ent.Comp.DamageType == "Rot")
_rot.ReduceAccumulator(args.Body, TimeSpan.FromSeconds(2147483648)); // BEHOLD, SHITCODE THAT I JUST COPY PASTED. I'll redo it at some point, pinky swear :)
// if (ent.Comp.DamageType == "Rot")
// _rot.ReduceAccumulator(args.Body, TimeSpan.FromSeconds(2147483648)); // BEHOLD, SHITCODE THAT I JUST COPY PASTED. I'll redo it at some point, pinky swear :)
// End DeltaV - this shit was killed
/*else if (ent.Comp.DamageType == "Eye"
&& TryComp(ent, out BlindableComponent? blindComp)
&& blindComp.EyeDamage > 0)

View File

@ -15,9 +15,15 @@ namespace Content.Shared.Access
/// <summary>
/// The player-visible name of the access level, in the ID card console and such.
/// </summary>
[DataField("name")]
[DataField]
public string? Name { get; set; }
/// <summary>
/// Denotes whether this access level is intended to be assignable to a crew ID card.
/// </summary>
[DataField]
public bool CanAddToIdCard = true;
public string GetAccessLevelName()
{
if (Name is { } name)

View File

@ -1,13 +1,15 @@
using Content.Shared.Advertise.Systems;
using Content.Shared.Dataset;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Server.Advertise.Components;
namespace Content.Shared.Advertise.Components;
/// <summary>
/// Causes the entity to speak using the Chat system when its ActivatableUI is closed, optionally
/// requiring that a Flag be set as well.
/// </summary>
[RegisterComponent, Access(typeof(SpeakOnUIClosedSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedSpeakOnUIClosedSystem))]
public sealed partial class SpeakOnUIClosedComponent : Component
{
/// <summary>
@ -31,6 +33,6 @@ public sealed partial class SpeakOnUIClosedComponent : Component
/// <summary>
/// State variable only used if <see cref="RequireFlag"/> is true. Set with <see cref="SpeakOnUIClosedSystem.TrySetFlag"/>.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public bool Flag;
}

View File

@ -0,0 +1,16 @@
using SpeakOnUIClosedComponent = Content.Shared.Advertise.Components.SpeakOnUIClosedComponent;
namespace Content.Shared.Advertise.Systems;
public abstract class SharedSpeakOnUIClosedSystem : EntitySystem
{
public bool TrySetFlag(Entity<SpeakOnUIClosedComponent?> entity, bool value = true)
{
if (!Resolve(entity, ref entity.Comp))
return false;
entity.Comp.Flag = value;
Dirty(entity);
return true;
}
}

View File

@ -60,5 +60,17 @@ public sealed partial class CCVars
public static readonly CVarDef<float> SpeechBubbleBackgroundOpacity =
CVarDef.Create("accessibility.speech_bubble_background_opacity", 0.75f, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// If enabled, censors character nudity by forcing clothes markings on characters, selected by the client.
/// Both this and AccessibilityServerCensorNudity must be false to display nudity on the client.
/// </summary>
public static readonly CVarDef<bool> AccessibilityClientCensorNudity =
CVarDef.Create("accessibility.censor_nudity", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// If enabled, censors character nudity by forcing clothes markings on characters, selected by the server.
/// Both this and AccessibilityClientCensorNudity must be false to display nudity on the client.
/// </summary>
public static readonly CVarDef<bool> AccessibilityServerCensorNudity =
CVarDef.Create("accessibility.server_censor_nudity", false, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);
}

View File

@ -53,6 +53,12 @@ public sealed partial class CCVars
public static readonly CVarDef<bool>
GameLobbyEnableWin = CVarDef.Create("game.enablewin", true, CVar.ARCHIVE);
/// <summary>
/// Controls if round-end window shows whether the objective was completed or not.
/// </summary>
public static readonly CVarDef<bool>
GameShowGreentext = CVarDef.Create("game.showgreentext", true, CVar.ARCHIVE | CVar.SERVERONLY);
/// <summary>
/// Controls the maximum number of character slots a player is allowed to have.
/// </summary>

View File

@ -36,9 +36,6 @@ namespace Content.Shared.Chemistry.Components
[DataField]
public string? SolutionName;
[DataField]
public string InitialName = string.Empty;
[DataField]
public string InitialDescription = string.Empty;

View File

@ -0,0 +1,13 @@
namespace Content.Shared.Cloning.Events;
/// <summary>
/// Raised before a mob is cloned. Cancel to prevent cloning.
/// </summary>
[ByRefEvent]
public record struct CloningAttemptEvent(CloningSettingsPrototype Settings, bool Cancelled = false);
/// <summary>
/// Raised after a new mob got spawned when cloning a humanoid.
/// </summary>
[ByRefEvent]
public record struct CloningEvent(CloningSettingsPrototype Settings, EntityUid CloneUid);

View File

@ -10,8 +10,8 @@ namespace Content.Shared.Cloning;
[RegisterComponent]
public sealed partial class CloningPodComponent : Component
{
[ValidatePrototypeId<SinkPortPrototype>]
public const string PodPort = "CloningPodReceiver";
[DataField]
public ProtoId<SinkPortPrototype> PodPort = "CloningPodReceiver";
[ViewVariables]
public ContainerSlot BodyContainer = default!;
@ -31,23 +31,25 @@ public sealed partial class CloningPodComponent : Component
/// <summary>
/// The material that is used to clone entities.
/// </summary>
[DataField("requiredMaterial"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public ProtoId<MaterialPrototype> RequiredMaterial = "Biomass";
/// <summary>
/// The current amount of time it takes to clone a body
/// The current amount of time it takes to clone a body.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float CloningTime = 30f;
/// <summary>
/// The mob to spawn on emag
/// The mob to spawn on emag.
/// </summary>
[DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)]
[DataField]
public EntProtoId MobSpawnId = "MobAbomination";
// TODO: Remove this from here when cloning and/or zombies are refactored
[DataField("screamSound")]
/// <summary>
/// The sound played when a mob is spawned from an emagged cloning pod.
/// </summary>
[DataField]
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams")
{
Params = AudioParams.Default.WithVolume(4),
@ -74,21 +76,3 @@ public enum CloningPodStatus : byte
Gore,
NoMind
}
/// <summary>
/// Raised after a new mob got spawned when cloning a humanoid
/// </summary>
[ByRefEvent]
public struct CloningEvent
{
public bool NameHandled = false;
public readonly EntityUid Source;
public readonly EntityUid Target;
public CloningEvent(EntityUid source, EntityUid target)
{
Source = source;
Target = target;
}
}

View File

@ -0,0 +1,60 @@
using Content.Shared.Inventory;
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
namespace Content.Shared.Cloning;
/// <summary>
/// Settings for cloning a humanoid.
/// Used to decide which components should be copied.
/// </summary>
[Prototype]
public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; private set; } = default!;
[ParentDataField(typeof(PrototypeIdArraySerializer<CloningSettingsPrototype>))]
public string[]? Parents { get; }
[AbstractDataField]
[NeverPushInheritance]
public bool Abstract { get; }
/// <summary>
/// Determines if cloning can be prevented by traits etc.
/// </summary>
[DataField]
public bool ForceCloning = true;
/// <summary>
/// Which inventory slots will receive a copy of the original's clothing.
/// Disabled when null.
/// </summary>
[DataField]
public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET;
/// <summary>
/// Whitelist for the equipment allowed to be copied.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Blacklist for the equipment allowed to be copied.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// TODO: Make this not a string https://github.com/space-wizards/RobustToolbox/issues/5709
/// <summary>
/// Components to copy from the original to the clone.
/// This only makes a shallow copy of datafields!
/// If you need a deep copy or additional component initialization, then subscribe to CloningEvent instead!
/// </summary>
[DataField]
[AlwaysPushInheritance]
public HashSet<string> Components = new();
}

View File

@ -65,6 +65,13 @@ namespace Content.Shared.Damage
[DataField("radiationDamageTypes")]
public List<ProtoId<DamageTypePrototype>> RadiationDamageTypeIDs = new() { "Radiation" };
/// <summary>
/// Group types that affect the pain overlay.
/// </summary>
/// TODO: Add support for adding damage types specifically rather than whole damage groups
[DataField]
public List<ProtoId<DamageGroupPrototype>> PainDamageGroups = new() { "Brute", "Burn" };
[DataField]
public Dictionary<MobState, ProtoId<HealthIconPrototype>> HealthIcons = new()
{

View File

@ -0,0 +1,35 @@
using Robust.Shared.GameStates;
namespace Content.Shared.FingerprintReader;
/// <summary>
/// Component for checking if a user's fingerprint matches allowed fingerprints
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(FingerprintReaderSystem))]
public sealed partial class FingerprintReaderComponent : Component
{
/// <summary>
/// The fingerprints that are allowed to access this entity.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<string> AllowedFingerprints = new();
/// <summary>
/// Whether to ignore gloves when checking fingerprints.
/// </summary>
[DataField, AutoNetworkedField]
public bool IgnoreGloves;
/// <summary>
/// The popup to show when access is denied due to fingerprint mismatch.
/// </summary>
[DataField]
public LocId? FailPopup;
/// <summary>
/// The popup to show when access is denied due to wearing gloves.
/// </summary>
[DataField]
public LocId? FailGlovesPopup;
}

View File

@ -0,0 +1,100 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Forensics.Components;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using JetBrains.Annotations;
namespace Content.Shared.FingerprintReader;
// TODO: This has a lot of overlap with the AccessReaderSystem, maybe merge them in the future?
public sealed class FingerprintReaderSystem : EntitySystem
{
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
/// <summary>
/// Checks if the given user has fingerprint access to the target entity.
/// </summary>
/// <param name="target">The target entity.</param>
/// <param name="user">User trying to gain access.</param>
/// <returns>True if access was granted, otherwise false.</returns>
[PublicAPI]
public bool IsAllowed(Entity<FingerprintReaderComponent?> target, EntityUid user)
{
if (!Resolve(target, ref target.Comp, false))
return true;
if (target.Comp.AllowedFingerprints.Count == 0)
return true;
// Check for gloves first
if (!target.Comp.IgnoreGloves && TryGetBlockingGloves(user, out var gloves))
{
if (target.Comp.FailGlovesPopup != null)
_popup.PopupEntity(Loc.GetString(target.Comp.FailGlovesPopup, ("blocker", gloves)), target, user);
return false;
}
// Check fingerprint match
if (!TryComp<FingerprintComponent>(user, out var fingerprint) || fingerprint.Fingerprint == null ||
!target.Comp.AllowedFingerprints.Contains(fingerprint.Fingerprint))
{
if (target.Comp.FailPopup != null)
_popup.PopupEntity(Loc.GetString(target.Comp.FailPopup), target, user);
return false;
}
return true;
}
/// <summary>
/// Gets the blocking gloves of a user. Gloves count as blocking if they hide fingerprints.
/// </summary>
/// <param name="user">Entity wearing the gloves.</param>
/// <param name="blocker">The returned gloves, if they exist.</param>
/// <returns>True if blocking gloves were found, otherwise False.</returns>
[PublicAPI]
public bool TryGetBlockingGloves(EntityUid user, [NotNullWhen(true)] out EntityUid? blocker)
{
blocker = null;
if (_inventory.TryGetSlotEntity(user, "gloves", out var gloves) && HasComp<FingerprintMaskComponent>(gloves))
{
blocker = gloves;
return true;
}
return false;
}
/// <summary>
/// Sets the allowed fingerprints for a fingerprint reader
/// </summary>
[PublicAPI]
public void SetAllowedFingerprints(Entity<FingerprintReaderComponent> target, HashSet<string> fingerprints)
{
target.Comp.AllowedFingerprints = fingerprints;
Dirty(target);
}
/// <summary>
/// Adds an allowed fingerprint to a fingerprint reader
/// </summary>
[PublicAPI]
public void AddAllowedFingerprint(Entity<FingerprintReaderComponent> target, string fingerprint)
{
target.Comp.AllowedFingerprints.Add(fingerprint);
Dirty(target);
}
/// <summary>
/// Removes an allowed fingerprint from a fingerprint reader
/// </summary>
[PublicAPI]
public void RemoveAllowedFingerprint(Entity<FingerprintReaderComponent> target, string fingerprint)
{
target.Comp.AllowedFingerprints.Remove(fingerprint);
Dirty(target);
}
}

View File

@ -1,3 +1,4 @@
using Content.Shared.Body.Components;
using Content.Shared.Buckle;
using Content.Shared.Buckle.Components;
using Content.Shared.Storage.Components;
@ -23,7 +24,7 @@ public sealed class FoldableSystem : EntitySystem
SubscribeLocalEvent<FoldableComponent, ComponentInit>(OnFoldableInit);
SubscribeLocalEvent<FoldableComponent, ContainerGettingInsertedAttemptEvent>(OnInsertEvent);
SubscribeLocalEvent<FoldableComponent, StoreMobInItemContainerAttemptEvent>(OnStoreThisAttempt);
SubscribeLocalEvent<FoldableComponent, InsertIntoEntityStorageAttemptEvent>(OnStoreThisAttempt);
SubscribeLocalEvent<FoldableComponent, StorageOpenAttemptEvent>(OnFoldableOpenAttempt);
SubscribeLocalEvent<FoldableComponent, StrapAttemptEvent>(OnStrapAttempt);
@ -45,10 +46,8 @@ public sealed class FoldableSystem : EntitySystem
args.Cancelled = true;
}
public void OnStoreThisAttempt(EntityUid uid, FoldableComponent comp, ref StoreMobInItemContainerAttemptEvent args)
public void OnStoreThisAttempt(EntityUid uid, FoldableComponent comp, ref InsertIntoEntityStorageAttemptEvent args)
{
args.Handled = true;
if (comp.IsFolded)
args.Cancelled = true;
}

View File

@ -9,5 +9,5 @@ namespace Content.Shared.Forensics.Components;
public sealed partial class DnaComponent : Component
{
[DataField("dna"), AutoNetworkedField]
public string DNA = String.Empty;
public string? DNA;
}

View File

@ -54,7 +54,7 @@ public record struct TransferDnaEvent()
}
/// <summary>
/// An event to generate and act upon new DNA for an entity.
/// Raised on an entity when its DNA has been changed.
/// </summary>
[ByRefEvent]
public record struct GenerateDnaEvent()

View File

@ -1,12 +1,10 @@
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences; // DeltaV
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using Content.Shared.Preferences; // DeltaV, used for Metempsychosis, Fugitive, and Paradox Anomaly
namespace Content.Shared.Humanoid;
@ -100,10 +98,13 @@ public sealed partial class HumanoidAppearanceComponent : Component
public HashSet<HumanoidVisualLayers> HideLayersOnEquip = [HumanoidVisualLayers.Hair];
/// <summary>
/// DeltaV - let paradox anomaly be cloned
/// Which markings the humanoid defaults to when nudity is toggled off.
/// </summary>
[ViewVariables]
public HumanoidCharacterProfile? LastProfileLoaded;
[DataField]
public ProtoId<MarkingPrototype>? UndergarmentTop = new ProtoId<MarkingPrototype>("UndergarmentTopTanktop");
[DataField]
public ProtoId<MarkingPrototype>? UndergarmentBottom = new ProtoId<MarkingPrototype>("UndergarmentBottomBoxers");
}
[DataDefinition]

View File

@ -10,9 +10,9 @@ namespace Content.Shared.Humanoid
Tail,
Hair,
FacialHair,
UndergarmentTop,
UndergarmentBottom,
Chest,
Underwear, // DeltaV
Undershirt, // DeltaV
Head,
Snout,
HeadSide, // side parts (i.e., frills)
@ -21,7 +21,6 @@ namespace Content.Shared.Humanoid
RArm,
LArm,
RHand,
LHand,
RLeg,
LLeg,

View File

@ -13,8 +13,8 @@ namespace Content.Shared.Humanoid.Markings
HeadSide,
Snout,
Chest,
Underwear, // DeltaV
Undershirt, // DeltaV
UndergarmentTop,
UndergarmentBottom,
Arms,
Legs,
Tail,
@ -34,9 +34,9 @@ namespace Content.Shared.Humanoid.Markings
HumanoidVisualLayers.HeadTop => MarkingCategories.HeadTop,
HumanoidVisualLayers.HeadSide => MarkingCategories.HeadSide,
HumanoidVisualLayers.Snout => MarkingCategories.Snout,
HumanoidVisualLayers.Undershirt => MarkingCategories.Undershirt, // DeltaV
HumanoidVisualLayers.Underwear => MarkingCategories.Underwear, // DeltaV
HumanoidVisualLayers.Chest => MarkingCategories.Chest,
HumanoidVisualLayers.UndergarmentTop => MarkingCategories.UndergarmentTop,
HumanoidVisualLayers.UndergarmentBottom => MarkingCategories.UndergarmentBottom,
HumanoidVisualLayers.RArm => MarkingCategories.Arms,
HumanoidVisualLayers.LArm => MarkingCategories.Arms,
HumanoidVisualLayers.RHand => MarkingCategories.Arms,

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