Merge pull request #3474 from DIMMoon1/upstream12.2

Upstream12.2
This commit is contained in:
Dmitry
2025-12-22 04:37:39 +07:00
committed by GitHub
653 changed files with 18036 additions and 8318 deletions

11
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,11 @@
# Space Station 14 Contributing Guidelines
Thanks for contributing to Space Station 14.
When contributing, be sure to follow our [codebase conventions](https://docs.spacestation14.com/en/general-development/codebase-info/codebase-organization.html) and [PR guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
Following these guidelines helps us increase review turnaround time, so be sure to review the linked documents in full.
The last major guidelines update was on **December 6th, 2025**.
### Why is this here?
We put this here so that GitHub will notify you when submitting a pull request that the PR guidelines have changed, if you haven't read the latest version.

View File

@@ -51,6 +51,8 @@ public class DestructibleBenchmark
private readonly List<Entity<DamageableComponent>> _damageables = new();
private readonly List<Entity<DamageableComponent, DestructibleComponent>> _destructbiles = new();
private TestMapData _currentMapData = default!;
private DamageSpecifier _damage;
private TestPair _pair = default!;
@@ -70,8 +72,6 @@ public class DestructibleBenchmark
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
var mapdata = await _pair.CreateTestMap();
_entMan = server.ResolveDependency<IEntityManager>();
_protoMan = server.ResolveDependency<IPrototypeManager>();
_random = server.ResolveDependency<IRobustRandom>();
@@ -86,19 +86,25 @@ public class DestructibleBenchmark
_damage = new DamageSpecifier(type, DamageAmount);
_random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
}
[IterationSetup]
public void IterationSetup()
{
var plating = _tileDefMan[TileRef].TileId;
var server = _pair.Server;
_currentMapData = _pair.CreateTestMap().GetAwaiter().GetResult();
// We make a rectangular grid of destructible entities, and then damage them all simultaneously to stress test the system.
// Needed for managing the performance of destructive effects and damage application.
await server.WaitPost(() =>
server.WaitPost(() =>
{
// Set up a thin line of tiles to place our objects on. They should be anchored for a "realistic" scenario...
for (var x = 0; x < EntityCount; x++)
{
for (var y = 0; y < _prototypes.Length; y++)
{
_map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
_map.SetTile(_currentMapData.Grid, _currentMapData.Grid, new Vector2i(x, y), new Tile(plating));
}
}
@@ -107,7 +113,7 @@ public class DestructibleBenchmark
var y = 0;
foreach (var protoId in _prototypes)
{
var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
var coords = new EntityCoordinates(_currentMapData.Grid, x + 0.5f, y + 0.5f);
_entMan.SpawnEntity(protoId, coords);
y++;
}
@@ -115,12 +121,17 @@ public class DestructibleBenchmark
var query = _entMan.EntityQueryEnumerator<DamageableComponent, DestructibleComponent>();
_destructbiles.EnsureCapacity(EntityCount);
_damageables.EnsureCapacity(EntityCount);
while (query.MoveNext(out var uid, out var damageable, out var destructible))
{
_damageables.Add((uid, damageable));
_destructbiles.Add((uid, damageable, destructible));
}
});
})
.GetAwaiter()
.GetResult();
}
[Benchmark]
@@ -150,6 +161,26 @@ public class DestructibleBenchmark
});
}
[IterationCleanup]
public void IterationCleanupAsync()
{
// We need to nuke the entire map and respawn everything as some destructible effects
// spawn entities and whatnot.
_pair.Server.WaitPost(() =>
{
_map.QueueDeleteMap(_currentMapData.MapId);
})
.Wait();
// Deletion of entities is often queued (QueueDel) which must be processed by running ticks
// or else it will grow infinitely and leak memory.
_pair.Server.WaitRunTicks(2)
.GetAwaiter()
.GetResult();
_destructbiles.Clear();
_damageables.Clear();
}
[GlobalCleanup]
public async Task CleanupAsync()

View File

@@ -5,7 +5,7 @@ using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Maps;
using Content.Shared.Maps;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization.Systems;

View File

@@ -1,4 +1,4 @@
<GridContainer xmlns="https://spacestation14.io"
Columns="5"
Columns="4"
HorizontalAlignment="Center">
</GridContainer>

View File

@@ -2,7 +2,7 @@
MinSize="650 290">
<BoxContainer Orientation="Vertical">
<GridContainer Columns="2">
<GridContainer Columns="3" HorizontalExpand="True">
<GridContainer Name="PrivilegedIdGrid" Columns="3" HorizontalExpand="True">
<Label Text="{Loc 'access-overrider-window-privileged-id'}" />
<Button Name="PrivilegedIdButton" Access="Public"/>
<Label Name="PrivilegedIdLabel" />

View File

@@ -53,6 +53,8 @@ namespace Content.Client.Access.UI
public void UpdateState(IPrototypeManager protoManager, AccessOverriderBoundUserInterfaceState state)
{
PrivilegedIdGrid.Visible = state.ShowPrivilegedIdGrid;
PrivilegedIdLabel.Text = state.PrivilegedIdName;
PrivilegedIdButton.Text = state.IsPrivilegedIdPresent
? Loc.GetString("access-overrider-window-eject-button")
@@ -77,7 +79,9 @@ namespace Content.Client.Access.UI
missingPrivileges.Add(privilege);
}
MissingPrivilegesLabel.Text = Loc.GetString("access-overrider-window-missing-privileges");
MissingPrivilegesLabel.Text = state.ShowPrivilegedIdGrid ?
Loc.GetString("access-overrider-window-missing-privileges") :
Loc.GetString("access-overrider-window-missing-privileges-no-id");
MissingPrivilegesText.Text = string.Join(", ", missingPrivileges);
}

View File

@@ -1,46 +1,77 @@
using System.Numerics;
using Content.Shared.Administration.Components;
using Robust.Client.GameObjects;
using Robust.Shared.Utility;
using Robust.Client.Player;
namespace Content.Client.Administration.Systems;
public sealed class KillSignSystem : EntitySystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public override void Initialize()
{
SubscribeLocalEvent<KillSignComponent, ComponentStartup>(KillSignAdded);
SubscribeLocalEvent<KillSignComponent, ComponentShutdown>(KillSignRemoved);
SubscribeLocalEvent<KillSignComponent, AfterAutoHandleStateEvent>(AfterAutoHandleState);
}
private void KillSignRemoved(EntityUid uid, KillSignComponent component, ComponentShutdown args)
private void KillSignRemoved(Entity<KillSignComponent> ent, ref ComponentShutdown args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
if (!_sprite.LayerMapTryGet((uid, sprite), KillSignKey.Key, out var layer, false))
return;
_sprite.RemoveLayer((uid, sprite), layer);
RemoveKillsign(ent);
}
private void KillSignAdded(EntityUid uid, KillSignComponent component, ComponentStartup args)
private void KillSignAdded(Entity<KillSignComponent> ent, ref ComponentStartup args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
AddKillsign(ent);
}
private void AfterAutoHandleState(Entity<KillSignComponent> ent, ref AfterAutoHandleStateEvent args)
{
// After receiving a new state for the component, we remove the old killsign and build a new one.
// This is so changes to the sprite can be displayed live and allowing them to be edited via ViewVariables.
// This could just update an existing sprite, but this is both easier and runs rarely anyway.
RemoveKillsign(ent);
AddKillsign(ent);
}
private void AddKillsign(Entity<KillSignComponent> ent)
{
// If we hide from owner and we ARE the owner, don't add a killsign.
// This could use session specific networking to FULLY hide it, but I am too lazy right now.
if (ent.Comp.HideFromOwner && _player.LocalEntity == ent)
return;
if (_sprite.LayerMapTryGet((uid, sprite), KillSignKey.Key, out var _, false))
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
var adj = _sprite.GetLocalBounds((uid, sprite)).Height / 2 + ((1.0f / 32) * 6.0f);
if (_sprite.LayerMapTryGet((ent, sprite), KillSignKey.Key, out var _, false))
return;
var layer = _sprite.AddLayer((uid, sprite), new SpriteSpecifier.Rsi(new ResPath("Objects/Misc/killsign.rsi"), "sign"));
_sprite.LayerMapSet((uid, sprite), KillSignKey.Key, layer);
if (ent.Comp.Sprite == null)
return;
_sprite.LayerSetOffset((uid, sprite), layer, new Vector2(0.0f, adj));
sprite.LayerSetShader(layer, "unshaded");
var adj = _sprite.GetLocalBounds((ent, sprite)).Height / 2 + ((1.0f / 32) * 6.0f);
var layer = _sprite.AddLayer((ent, sprite), ent.Comp.Sprite);
_sprite.LayerMapSet((ent, sprite), KillSignKey.Key, layer);
_sprite.LayerSetScale((ent, sprite), layer, ent.Comp.Scale);
_sprite.LayerSetOffset((ent, sprite), layer, ent.Comp.DoOffset ? new Vector2(0.0f, adj) : new Vector2(0.0f, 0.0f));
if (ent.Comp.ForceUnshaded)
sprite.LayerSetShader(layer, "unshaded");
}
private void RemoveKillsign(Entity<KillSignComponent> ent)
{
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
if (!_sprite.LayerMapTryGet((ent, sprite), KillSignKey.Key, out var layer, false))
return;
_sprite.RemoveLayer((ent, sprite), layer);
}
private enum KillSignKey

View File

@@ -15,7 +15,7 @@ public sealed class AdminLogLabel : RichTextLabel
OnVisibilityChanged += VisibilityChanged;
}
public SharedAdminLog Log { get; }
public new SharedAdminLog Log { get; }
public HSeparator Separator { get; }

View File

@@ -1,8 +1,12 @@
using Content.Client.Message;
using Content.Client.RichText;
using Content.Client.UserInterface.RichText;
using Content.Shared.MassMedia.Systems;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client.CartridgeLoader.Cartridges;
@@ -31,16 +35,17 @@ public sealed partial class NewsReaderUiFragment : BoxContainer
Author.Visible = true;
PageName.Text = article.Title;
PageText.SetMarkupPermissive(article.Content);
PageText.SetMessage(FormattedMessage.FromMarkupPermissive(article.Content), UserFormattableTags.BaseAllowedTags);
PageNum.Text = $"{targetNum}/{totalNum}";
NotificationSwitch.Text = Loc.GetString(notificationOn ? "news-read-ui-notification-on" : "news-read-ui-notification-off");
string shareTime = article.ShareTime.ToString(@"hh\:mm\:ss");
var shareTime = article.ShareTime.ToString(@"hh\:mm\:ss");
ShareTime.SetMarkup(Loc.GetString("news-read-ui-time-prefix-text") + " " + shareTime);
Author.SetMarkup(Loc.GetString("news-read-ui-author-prefix") + " " + (article.Author != null ? article.Author : Loc.GetString("news-read-ui-no-author")));
var author = Loc.GetString("news-read-ui-author-prefix") + " " + (article.Author ?? Loc.GetString("news-read-ui-no-author"));
Author.SetMessage(FormattedMessage.FromMarkupPermissive(author), UserFormattableTags.BaseAllowedTags);
Prev.Disabled = targetNum <= 1;
Next.Disabled = targetNum >= totalNum;

View File

@@ -0,0 +1,24 @@
using Content.Client.Wall;
using Robust.Client.Graphics;
using Robust.Shared.Console;
namespace Content.Client.Commands;
/// <summary>
/// Shows the area in which entities with <see cref="Content.Shared.Wall.WallMountComponent" /> can be interacted from.
/// </summary>
public sealed class ShowWallmountsCommand : LocalizedCommands
{
[Dependency] private readonly IOverlayManager _overlay = default!;
public override string Command => "showwallmounts";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var existing = _overlay.RemoveOverlay<WallmountDebugOverlay>();
if (!existing)
_overlay.AddOverlay(new WallmountDebugOverlay());
shell.WriteLine(Loc.GetString("cmd-showwallmounts-status", ("status", !existing)));
}
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.DeviceLinking.Systems;
namespace Content.Client.DeviceLinking.Systems;
public sealed class RandomGateSystem : SharedRandomGateSystem;

View File

@@ -0,0 +1,37 @@
using Content.Shared.DeviceLinking;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.DeviceLinking.UI;
[UsedImplicitly]
public sealed class RandomGateBoundUserInterface : BoundUserInterface
{
private RandomGateSetupWindow? _window;
public RandomGateBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
protected override void Open()
{
base.Open();
_window = this.CreateWindow<RandomGateSetupWindow>();
_window.OnApplyPressed += OnProbabilityChanged;
}
private void OnProbabilityChanged(string value)
{
if (!float.TryParse(value, out var probability))
return;
SendPredictedMessage(new RandomGateProbabilityChangedMessage(probability));
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not RandomGateBoundUserInterfaceState castState || _window == null)
return;
_window.SetProbability(castState.SuccessProbability * 100);
}
}

View File

@@ -0,0 +1,19 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'random-gate-menu-setup'}"
MinSize="260 115">
<BoxContainer Orientation="Vertical" Margin="5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Label Text="{Loc 'random-gate-menu-settings'}" VerticalAlignment="Center" />
<Control HorizontalExpand="True" />
<LineEdit Name="ProbabilityInput" MinSize="70 0" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Control HorizontalExpand="True" />
<Button Name="ApplyButton"
Text="{Loc 'random-gate-menu-apply'}"
HorizontalAlignment="Right" />
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,28 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.DeviceLinking.UI;
/// <summary>
/// Window for setting up the random gate probability.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class RandomGateSetupWindow : FancyWindow
{
/// <summary>
/// Event triggered when the "Apply" button is pressed.
/// </summary>
public event Action<string>? OnApplyPressed;
public RandomGateSetupWindow()
{
RobustXamlLoader.Load(this);
ApplyButton.OnPressed += _ => OnApplyPressed?.Invoke(ProbabilityInput.Text);
}
public void SetProbability(float probability)
{
ProbabilityInput.Text = probability.ToString("0.00");
}
}

View File

@@ -1,10 +1,13 @@
using Content.Client.Message;
using Content.Client.RichText;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.RichText;
using Content.Shared.MassMedia.Systems;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.RichText;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
@@ -81,7 +84,9 @@ public sealed partial class ArticleEditorPanel : Control
TextEditPanel.Visible = !_preview;
PreviewPanel.Visible = _preview;
PreviewLabel.SetMarkupPermissive(Rope.Collapse(ContentField.TextRope));
var articleBody = Rope.Collapse(ContentField.TextRope);
PreviewLabel.SetMessage(FormattedMessage.FromMarkupPermissive(articleBody), UserFormattableTags.BaseAllowedTags);
}
private void OnCancel(BaseButton.ButtonEventArgs eventArgs)

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Client.RichText;
using Content.Shared.Paper;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
@@ -38,16 +39,6 @@ namespace Content.Client.Paper.UI
// we're able to resize this UI or not. Default to everything enabled:
private DragMode _allowedResizeModes = ~DragMode.None;
private readonly Type[] _allowedTags = new Type[] {
typeof(BoldItalicTag),
typeof(BoldTag),
typeof(BulletTag),
typeof(ColorTag),
typeof(HeadingTag),
typeof(ItalicTag),
typeof(MonoTag)
};
public event Action<string>? OnSaved;
private int _MaxInputLength = -1;
@@ -280,7 +271,7 @@ namespace Content.Client.Paper.UI
{
msg.AddMarkupPermissive("\r\n");
}
WrittenTextLabel.SetMessage(msg, _allowedTags, DefaultTextColor);
WrittenTextLabel.SetMessage(msg, UserFormattableTags.BaseAllowedTags, DefaultTextColor);
WrittenTextLabel.Visible = !isEditing && state.Text.Length > 0;
BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;

View File

@@ -98,10 +98,13 @@ public sealed class ParallaxManager : IParallaxManager
}
else
{
layers = await Task.WhenAll(
// Explicitly allocate params array to avoid sandbox violation since C# 14.
var tasks = new[]
{
LoadParallaxLayers(parallaxPrototype.Layers, loadedLayers, cancel),
LoadParallaxLayers(parallaxPrototype.LayersLQ, loadedLayers, cancel)
);
LoadParallaxLayers(parallaxPrototype.LayersLQ, loadedLayers, cancel),
};
layers = await Task.WhenAll(tasks);
}
cancel.ThrowIfCancellationRequested();

View File

@@ -19,7 +19,7 @@
<!-- Power On/Off -->
<Label Text="{Loc 'apc-menu-breaker-label'}" HorizontalExpand="True"
StyleClasses="highlight" MinWidth="120"/>
<BoxContainer Orientation="Horizontal" MinWidth="90">
<BoxContainer Orientation="Horizontal" MinWidth="150">
<Button Name="BreakerButton" Text="{Loc 'apc-menu-breaker-button'}" HorizontalExpand="True" ToggleMode="True"/>
</BoxContainer>
<!--Charging Status-->

View File

@@ -40,7 +40,14 @@ namespace Content.Client.Power.APC.UI
if (PowerLabel != null)
{
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power));
if (castState.Tripped)
{
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-tripped");
}
else
{
PowerLabel.Text = Loc.GetString("apc-menu-power-state-label-text", ("power", castState.Power), ("maxLoad", castState.MaxLoad));
}
}
if (ExternalPowerStateLabel != null)

View File

@@ -35,6 +35,7 @@ public sealed class DoorRemoteStatusControl(Entity<DoorRemoteComponent> ent) : C
OperatingMode.OpenClose => "door-remote-open-close-text",
OperatingMode.ToggleBolts => "door-remote-toggle-bolt-text",
OperatingMode.ToggleEmergencyAccess => "door-remote-emergency-access-text",
OperatingMode.ToggleOvercharge => "door-remote-toggle-eletrify-text",
_ => "door-remote-invalid-text"
});

View File

@@ -0,0 +1,25 @@
using Content.Client.UserInterface.RichText;
using Robust.Client.UserInterface.RichText;
namespace Content.Client.RichText;
/// <summary>
/// Contains rules for what markup tags are allowed to be used by players.
/// </summary>
public static class UserFormattableTags
{
/// <summary>
/// The basic set of "rich text" formatting tags that shouldn't cause any issues.
/// Limit user rich text to these by default.
/// </summary>
public static readonly Type[] BaseAllowedTags =
[
typeof(BoldItalicTag),
typeof(BoldTag),
typeof(BulletTag),
typeof(ColorTag),
typeof(HeadingTag),
typeof(ItalicTag),
typeof(MonoTag),
];
}

View File

@@ -1,5 +0,0 @@
using Content.Shared.Rootable;
namespace Content.Client.Rootable;
public sealed class RootableSystem : SharedRootableSystem;

View File

@@ -124,7 +124,7 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
else
{
// We'll send the "adjusted" position and server will adjust it back when relevant.
var mapCoords = new MapCoordinates(InverseMapPosition(args.RelativePosition), ViewingMap);
var mapCoords = new MapCoordinates(InverseMapPosition(args.RelativePixelPosition), ViewingMap);
RequestFTL?.Invoke(mapCoords, _ftlAngle);
}
}
@@ -180,7 +180,7 @@ public sealed partial class ShuttleMapControl : BaseShuttleControl
// Remove offset so we can floor.
var botLeft = new Vector2(0f, 0f);
var topRight = botLeft + Size;
var topRight = botLeft + PixelSize;
var flooredBL = botLeft - originBL;

View File

@@ -14,10 +14,10 @@ public static class ColorExtensions
{
DebugTools.Assert(lightness is >= 0.0f and <= 1.0f);
var oklab = Color.ToLab(c);
var oklab = c.LabFromSrgb();
oklab.X = lightness;
return Color.FromLab(oklab);
return oklab.LabToSrgb();
}
/// <summary>
@@ -25,10 +25,10 @@ public static class ColorExtensions
/// </summary>
public static Color NudgeLightness(this Color c, float lightnessShift)
{
var oklab = Color.ToLab(c);
var oklab = c.LabFromSrgb();
oklab.X = Math.Clamp(oklab.X + lightnessShift, 0, 1);
return Color.FromLab(oklab);
return oklab.LabToSrgb();
}
/// <summary>
@@ -39,12 +39,12 @@ public static class ColorExtensions
/// </remarks>
public static Color NudgeChroma(this Color c, float chromaShift)
{
var oklab = Color.ToLab(c);
var oklab = c.LabFromSrgb();
var oklch = Color.ToLch(oklab);
oklch.Y = Math.Clamp(oklch.Y + chromaShift, 0, 1);
return Color.FromLab(Color.FromLch(oklch));
return Color.FromLch(oklch).LabToSrgb();
}
/// <summary>
@@ -54,10 +54,43 @@ public static class ColorExtensions
{
DebugTools.Assert(factor is >= 0.0f and <= 1.0f);
var okFrom = Color.ToLab(from);
var okTo = Color.ToLab(to);
var okFrom = from.LabFromSrgb();
var okTo = to.LabFromSrgb();
var blended = Vector4.Lerp(okFrom, okTo, factor);
return Color.FromLab(blended);
return blended.LabToSrgb();
}
/// <summary>
/// Converts a nonlinear sRGB ("normal") color to OkLAB.
/// </summary>
public static Vector4 LabFromSrgb(this Color from)
{
return Color.ToLab(Color.FromSrgb(from));
}
/// <summary>
/// Converts OkLAB to a nonlinear sRGB ("normal") color.
/// </summary>
public static Color LabToSrgb(this Vector4 from)
{
return Color.ToSrgb(Color.FromLab(from).SimpleClipGamut());
}
/// <summary>
/// Clips the gamut of the color so that all color channels are in the range 0 -> 1.
/// </summary>
/// <remarks>
/// This uses no clever perceptual techniques, it literally just clamps the individual channels.
/// </remarks>
public static Color SimpleClipGamut(this Color from)
{
return new Color
{
R = Math.Clamp(from.R, 0, 1),
G = Math.Clamp(from.G, 0, 1),
B = Math.Clamp(from.B, 0, 1),
A = from.A,
};
}
}

View File

@@ -10,7 +10,7 @@ namespace Content.Client.UserInterface.RichText;
/// <summary>
/// Sets the font to a monospaced variant
/// </summary>
public sealed class MonoTag : IMarkupTag
public sealed class MonoTag : IMarkupTagHandler
{
public static readonly ProtoId<FontPrototype> MonoFont = "Monospace";

View File

@@ -10,7 +10,7 @@ namespace Content.Client.UserInterface.RichText;
/// Adds a specified length of random characters that scramble at a set rate.
/// </summary>
[UsedImplicitly]
public sealed class ScrambleTag : IMarkupTag
public sealed class ScrambleTag : IMarkupTagHandler
{
[Dependency] private readonly IGameTiming _timing = default!;

View File

@@ -0,0 +1,58 @@
using Content.Shared.Interaction;
using Content.Shared.Wall;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using System.Numerics;
namespace Content.Client.Wall;
/// <summary>
/// Shows the area in which entities with <see cref="WallMountComponent" /> can be interacted from.
/// </summary>
public sealed class WallmountDebugOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entManager = default!;
private readonly SharedTransformSystem _transform;
private readonly EntityLookupSystem _lookup;
private readonly HashSet<Entity<WallMountComponent>> _intersecting = [];
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public WallmountDebugOverlay()
{
IoCManager.InjectDependencies(this);
_transform = _entManager.System<SharedTransformSystem>();
_lookup = _entManager.System<EntityLookupSystem>();
}
protected override void Draw(in OverlayDrawArgs args)
{
_intersecting.Clear();
_lookup.GetEntitiesIntersecting(args.MapId, args.WorldBounds, _intersecting);
foreach (var ent in _intersecting)
{
var (worldPos, worldRot) = _transform.GetWorldPositionRotation(ent.Owner);
DrawArc(args.WorldHandle, worldPos, SharedInteractionSystem.InteractionRange, worldRot + ent.Comp.Direction, ent.Comp.Arc);
}
}
private static void DrawArc(DrawingHandleWorld handle, Vector2 position, float radius, Angle rot, Angle arc)
{
// 32 segments for a full circle, but 2 at least
var segments = Math.Max((int)(arc.Theta / Math.Tau * 32), 2);
var step = arc.Theta / (segments - 1);
var verts = new Vector2[segments + 1];
verts[0] = position;
for (var i = 0; i < segments; i++)
{
var angle = (float)(-arc.Theta / 2 + i * step - rot.Theta + Math.PI);
var pos = new Vector2(MathF.Sin(angle), MathF.Cos(angle));
verts[i + 1] = position + pos * radius;
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts, Color.Green.WithAlpha(0.5f));
}
}

View File

@@ -0,0 +1,102 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Tests;
using Robust.Shared.GameObjects;
using Robust.Shared.Map.Components;
using Robust.Shared.Maths;
namespace Content.IntegrationTests.Tests.Atmos;
/// <summary>
/// Helper class for atmospherics tests.
/// See <see cref="TileAtmosphereTest"/> on how to add new tests with custom maps.
/// </summary>
[TestFixture]
public abstract class AtmosTest : InteractionTest
{
protected AtmosphereSystem SAtmos = default!;
protected EntityLookupSystem LookupSystem = default!;
protected Entity<GridAtmosphereComponent> RelevantAtmos = default!;
/// <summary>
/// Used in <see cref="AtmosphereSystem.RunProcessingFull"/>. Resolved during test setup.
/// </summary>
protected Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> ProcessEnt = default;
protected virtual float Moles => 1000.0f;
// 5% is a lot, but it can get this bad ATM...
protected virtual float Tolerance => 0.05f;
[SetUp]
public override async Task Setup()
{
await base.Setup();
SAtmos = SEntMan.System<AtmosphereSystem>();
LookupSystem = SEntMan.System<EntityLookupSystem>();
RelevantAtmos = (MapData.Grid, SEntMan.GetComponent<GridAtmosphereComponent>(MapData.Grid));
ProcessEnt = new Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent>(
MapData.Grid.Owner,
SEntMan.GetComponent<GridAtmosphereComponent>(MapData.Grid.Owner),
SEntMan.GetComponent<GasTileOverlayComponent>(MapData.Grid.Owner),
SEntMan.GetComponent<MapGridComponent>(MapData.Grid.Owner),
SEntMan.GetComponent<TransformComponent>(MapData.Grid.Owner));
}
/// <summary>
/// Tries to get a mapped <see cref="TestMarkerComponent"/> marker with a given name.
/// </summary>
/// <param name="markers">Marker entities to look through</param>
/// <param name="id">Marker name to look up (set during mapping)</param>
/// <param name="marker">Found marker EntityUid or Invalid</param>
/// <returns>True if found</returns>
protected static bool GetMarker(Entity<TestMarkerComponent>[] markers, string id, out EntityUid marker)
{
foreach (var ent in markers)
{
if (ent.Comp.Id == id)
{
marker = ent;
return true;
}
}
marker = EntityUid.Invalid;
return false;
}
protected static float GetGridMoles(Entity<GridAtmosphereComponent> grid)
{
var moles = 0.0f;
foreach (var tile in grid.Comp.Tiles.Values)
{
moles += tile.Air?.TotalMoles ?? 0.0f;
}
return moles;
}
/// <summary>
/// Asserts that test grid has this many moles, within tolerance percentage.
/// </summary>
protected void AssertGridMoles(float moles, float tolerance)
{
var gridMoles = GetGridMoles(RelevantAtmos);
Assert.That(MathHelper.CloseToPercent(moles, gridMoles, tolerance), $"Grid has {gridMoles} moles, but {moles} was expected");
}
/// <summary>
/// Asserts that provided GasMixtures have same total moles, within tolerance percentage.
/// </summary>
protected void AssertMixMoles(GasMixture mix1, GasMixture mix2, float tolerance)
{
Assert.That(MathHelper.CloseToPercent(mix1.TotalMoles, mix2.TotalMoles, tolerance),
$"GasMixtures do not match. Got {mix1.TotalMoles} and {mix2.TotalMoles} moles");
}
}

View File

@@ -0,0 +1,127 @@
using Content.Shared.Atmos;
using Content.Shared.Coordinates;
using Content.Shared.Tests;
using Robust.Shared.GameObjects;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Atmos;
public sealed class RoomSpacingTest : AtmosTest
{
protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_room.yml");
/// <summary>
/// Checks that deleting an outer wall spaces the room.
/// </summary>
[Test]
public async Task DeleteWall()
{
var markers = SEntMan.AllEntities<TestMarkerComponent>();
EntityUid source, floor, wallPos;
source = floor = wallPos = EntityUid.Invalid;
Assert.Multiple(() =>
{
Assert.That(GetMarker(markers, "source", out source));
Assert.That(GetMarker(markers, "floor", out floor));
Assert.That(GetMarker(markers, "wall", out wallPos));
});
var lookup = LookupSystem.GetEntitiesIntersecting(wallPos);
var wall = lookup.FirstOrNull();
Assert.That(wall, Is.Not.Null);
Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0));
var sourceMix = SAtmos.GetTileMixture(source, true);
Assert.That(sourceMix, Is.Not.EqualTo(null));
sourceMix.AdjustMoles(Gas.Frezon, Moles);
await Server.WaitRunTicks(500);
var mix1 = SAtmos.GetTileMixture(floor);
Assert.That(mix1, Is.Not.EqualTo(null));
AssertMixMoles(sourceMix, mix1, Tolerance);
AssertGridMoles(Moles, Tolerance);
// Space the room
await Server.WaitAssertion(() =>
{
SEntMan.DeleteEntity(wall);
});
await Server.WaitRunTicks(10);
await Server.WaitPost(() =>
{
for (var i = 0; i < 50; i++)
{
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
}
});
AssertMixMoles(sourceMix, mix1, Tolerance);
AssertGridMoles(0, Tolerance);
}
/// <summary>
/// Checks that exposing tile lattice spaces the room.
/// </summary>
[Test]
public async Task PryLattice()
{
var markers = SEntMan.AllEntities<TestMarkerComponent>();
EntityUid source, floor, wallPos;
source = floor = wallPos = EntityUid.Invalid;
Assert.Multiple(() =>
{
Assert.That(GetMarker(markers, "source", out source));
Assert.That(GetMarker(markers, "floor", out floor));
Assert.That(GetMarker(markers, "wall", out wallPos));
});
var lookup = LookupSystem.GetEntitiesIntersecting(wallPos);
var wall = lookup.FirstOrNull();
Assert.That(wall, Is.Not.Null);
Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0));
var sourceMix = SAtmos.GetTileMixture(source, true);
Assert.That(sourceMix, Is.Not.EqualTo(null));
sourceMix.AdjustMoles(Gas.Frezon, Moles);
await Server.WaitPost(() =>
{
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
});
var mix1 = SAtmos.GetTileMixture(floor);
Assert.That(mix1, Is.Not.EqualTo(null));
AssertMixMoles(sourceMix, mix1, Tolerance);
AssertGridMoles(Moles, Tolerance);
// Space the room
await SetTile(Lattice, SEntMan.GetNetCoordinates(floor.ToCoordinates()), MapData.Grid);
await Server.WaitRunTicks(10);
await Server.WaitPost(() =>
{
for (var i = 0; i < 50; i++)
{
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
}
});
mix1 = SAtmos.GetTileMixture(floor);
Assert.That(mix1, Is.Not.EqualTo(null));
AssertMixMoles(sourceMix, mix1, Tolerance);
AssertGridMoles(0, Tolerance);
}
}

View File

@@ -0,0 +1,159 @@
using Content.Shared.Atmos;
using Content.Shared.CCVar;
using Content.Shared.Coordinates;
using Content.Shared.Tests;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Atmos;
[TestOf(typeof(Atmospherics))]
public abstract class TileAtmosphereTest : AtmosTest
{
/// <summary>
/// Spawns gas in an enclosed space and checks that pressure equalizes within reasonable time.
/// Checks that mole count stays the same.
/// </summary>
[Test]
public async Task GasSpreading()
{
var markers = SEntMan.AllEntities<TestMarkerComponent>();
EntityUid source, point1, point2;
source = point1 = point2 = EntityUid.Invalid;
Assert.Multiple(() =>
{
Assert.That(GetMarker(markers, "source", out source));
Assert.That(GetMarker(markers, "point1", out point1));
Assert.That(GetMarker(markers, "point2", out point2));
});
Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0.0f));
var sourceMix = SAtmos.GetTileMixture(source, true);
Assert.That(sourceMix, Is.Not.EqualTo(null));
sourceMix.AdjustMoles(Gas.Frezon, Moles);
await Pair.Server.WaitPost(() =>
{
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
});
var mix1 = SAtmos.GetTileMixture(point1);
var mix2 = SAtmos.GetTileMixture(point2);
Assert.Multiple(() =>
{
Assert.That(mix1, Is.Not.EqualTo(null));
Assert.That(mix2, Is.Not.EqualTo(null));
});
AssertMixMoles(mix1, mix2, Tolerance);
AssertGridMoles(Moles, Tolerance);
}
/// <summary>
/// Spawns a combustible mixture and sets it ablaze.
/// Checks that fire propages through the entire grid.
/// </summary>
[Test]
public async Task FireSpreading()
{
var markers = SEntMan.AllEntities<TestMarkerComponent>();
EntityUid source, point1, point2;
source = point1 = point2 = EntityUid.Invalid;
Vector2i sourceXY, point1XY, point2XY;
sourceXY = point1XY = point2XY = Vector2i.Zero;
Assert.Multiple(() =>
{
Assert.That(GetMarker(markers, "source", out source));
Assert.That(GetMarker(markers, "point1", out point1));
Assert.That(GetMarker(markers, "point2", out point2));
Assert.That(Transform.TryGetGridTilePosition(source, out sourceXY, MapData.Grid));
Assert.That(Transform.TryGetGridTilePosition(source, out point1XY, MapData.Grid));
Assert.That(Transform.TryGetGridTilePosition(source, out point2XY, MapData.Grid));
});
Assert.That(GetGridMoles(RelevantAtmos), Is.EqualTo(0));
var sourceMix = SAtmos.GetTileMixture(source, true);
Assert.That(sourceMix, Is.Not.EqualTo(null));
sourceMix.AdjustMoles(Gas.Plasma, Moles / 10);
sourceMix.AdjustMoles(Gas.Oxygen, Moles - Moles / 10);
sourceMix.Temperature = Atmospherics.FireMinimumTemperatureToExist - 10;
Assert.Multiple(() =>
{
Assert.That(SAtmos.IsHotspotActive(MapData.Grid, sourceXY), Is.False);
Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point1XY), Is.False);
Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point2XY), Is.False);
});
await Server.WaitAssertion(() =>
{
var welder = SEntMan.SpawnEntity("Welder", source.ToCoordinates());
Assert.That(ItemToggleSys.TryActivate(welder));
});
await Server.WaitRunTicks(500);
Assert.Multiple(() =>
{
Assert.That(SAtmos.IsHotspotActive(MapData.Grid, sourceXY), Is.True);
Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point1XY), Is.True);
Assert.That(SAtmos.IsHotspotActive(MapData.Grid, point2XY), Is.True);
});
var mix1 = SAtmos.GetTileMixture(point1);
var mix2 = SAtmos.GetTileMixture(point2);
Assert.Multiple(() =>
{
Assert.That(mix1, Is.Not.EqualTo(null));
Assert.That(mix2, Is.Not.EqualTo(null));
});
AssertMixMoles(mix1, mix2, Tolerance);
AssertGridMoles(Moles, Tolerance);
}
}
// Declare separate fixtures to override the TestMap and configure CVars
public sealed class TileAtmosphereTest_X : TileAtmosphereTest
{
protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_x.yml");
}
public sealed class TileAtmosphereTest_Snake : TileAtmosphereTest
{
protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_snake.yml");
}
public sealed class TileAtmosphereTest_LINDA_X : TileAtmosphereTest
{
protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_x.yml");
public override async Task Setup()
{
await base.Setup();
Assert.That(Server.CfgMan.GetCVar(CCVars.MonstermosEqualization));
Server.CfgMan.SetCVar(CCVars.MonstermosEqualization, false);
}
}
public sealed class TileAtmosphereTest_LINDA_Snake : TileAtmosphereTest
{
protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/tile_atmosphere_test_snake.yml");
public override async Task Setup()
{
await base.Setup();
Assert.That(Server.CfgMan.GetCVar(CCVars.MonstermosEqualization));
Server.CfgMan.SetCVar(CCVars.MonstermosEqualization, false);
}
}

View File

@@ -315,10 +315,10 @@ namespace Content.IntegrationTests.Tests.Buckle
// Still buckled
Assert.That(buckle.Buckled);
// Now with no item in any hand
// Still with items in hand
foreach (var hand in hands.Hands.Keys)
{
Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Null);
Assert.That(handsSys.GetHeldItem((human, hands), hand), Is.Not.Null);
}
buckleSystem.Unbuckle(human, human);

View File

@@ -5,12 +5,12 @@ using System.Text.RegularExpressions;
using YamlDotNet.RepresentationModel;
using Content.Server.Administration.Systems;
using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Spawners.Components;
using Content.Server.Station.Components;
using Content.Shared.CCVar;
using Content.Shared.Maps;
using Content.Shared.Roles;
using Content.Shared.Station.Components;
using Robust.Shared.Configuration;

View File

@@ -1,17 +1,17 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Server.Power.Components;
using Content.Server.Power.NodeGroups;
using Content.Server.Power.Pow3r;
using Content.Shared.Maps;
using Content.Shared.Power.Components;
using Content.Shared.NodeContainer;
using Robust.Server.GameObjects;
using Robust.Shared.EntitySerialization;
namespace Content.IntegrationTests.Tests.Power;
[Explicit]
public sealed class StationPowerTests
{
/// <summary>
@@ -21,27 +21,21 @@ public sealed class StationPowerTests
private static readonly string[] GameMaps =
[
"Fland",
"Meta",
"Packed",
"Omega",
"Bagel",
"Box",
"Core",
"Marathon",
"Saltern",
"Reach",
"Train",
"Oasis",
"Gate",
"Amber",
"Loop",
"Plasma",
"Elkridge",
"Convex",
"Fland",
"Marathon",
"Oasis",
"Packed",
"Plasma",
"Relic",
"Snowball",
"Reach",
"Exo",
];
[Explicit]
[Test, TestCaseSource(nameof(GameMaps))]
public async Task TestStationStartingPowerWindow(string mapProtoId)
{
@@ -100,6 +94,54 @@ public sealed class StationPowerTests
$"Needs at least {requiredStoredPower - totalStartingCharge} more stored power!");
});
await pair.CleanReturnAsync();
}
[Test, TestCaseSource(nameof(GameMaps))]
public async Task TestApcLoad(string mapProtoId)
{
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Dirty = true,
});
var server = pair.Server;
var entMan = server.EntMan;
var protoMan = server.ProtoMan;
var ticker = entMan.System<GameTicker>();
var xform = entMan.System<TransformSystem>();
// Load the map
await server.WaitAssertion(() =>
{
Assert.That(protoMan.TryIndex<GameMapPrototype>(mapProtoId, out var mapProto));
var opts = DeserializationOptions.Default with { InitializeMaps = true };
ticker.LoadGameMap(mapProto, out var mapId, opts);
});
// Wait long enough for power to ramp up, but before anything can trip
await pair.RunSeconds(2);
// Check that no APCs start overloaded
var apcQuery = entMan.EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent>();
Assert.Multiple(() =>
{
while (apcQuery.MoveNext(out var uid, out var apc, out var battery))
{
// Uncomment the following line to log starting APC load to the console
//Console.WriteLine($"ApcLoad:{mapProtoId}:{uid}:{battery.CurrentSupply}");
if (xform.TryGetMapOrGridCoordinates(uid, out var coord))
{
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
$"APC {uid} on {mapProtoId} ({coord.Value.X}, {coord.Value.Y}) is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
}
else
{
Assert.That(apc.MaxLoad, Is.GreaterThanOrEqualTo(battery.CurrentSupply),
$"APC {uid} on {mapProtoId} is overloaded {battery.CurrentSupply} / {apc.MaxLoad}");
}
}
});
await pair.CleanReturnAsync();
}

View File

@@ -1,8 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using Content.Server.Maps;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.Maps;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.GameObjects;

View File

@@ -7,7 +7,7 @@ using System.Text.Json;
using System.Threading.Tasks;
using Content.IntegrationTests;
using Content.MapRenderer.Painters;
using Content.Server.Maps;
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.UnitTesting.Pool;
using SixLabors.ImageSharp;

View File

@@ -1,5 +1,5 @@
using System.IO;
using Content.Server.Maps;
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;

View File

@@ -19,8 +19,8 @@ public sealed class EntryPoint : GameClient
public override void Init()
{
base.Init();
IoCManager.BuildGraph();
IoCManager.InjectDependencies(this);
Dependencies.BuildGraph();
Dependencies.InjectDependencies(this);
}
public override void PostInit()

View File

@@ -148,7 +148,8 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
missingAccess,
privilegedIdName,
targetLabel,
targetLabelColor);
targetLabelColor,
component.ShowPrivilegedId);
_userInterface.SetUiState(uid, AccessOverriderUiKey.Key, newState);
}

View File

@@ -1,7 +1,7 @@
using System.Numerics;
using Content.Server.GameTicking;
using Content.Server.Maps;
using Content.Shared.Administration;
using Content.Shared.Maps;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;

View File

@@ -573,13 +573,32 @@ public sealed partial class AdminVerbSystem
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Misc/killsign.rsi"), "icon"),
Act = () =>
{
EnsureComp<KillSignComponent>(args.Target);
EnsureComp<KillSignComponent>(args.Target, out var comp);
comp.HideFromOwner = false; // We set it to false anyway, in case the hidden smite was used beforehand.
Dirty(args.Target, comp);
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", killSignName, Loc.GetString("admin-smite-kill-sign-description"))
};
args.Verbs.Add(killSign);
var hiddenKillSignName = Loc.GetString("admin-smite-kill-sign-hidden-name").ToLowerInvariant();
Verb hiddenKillSign = new()
{
Text = hiddenKillSignName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Misc/killsign.rsi"), "icon-hidden"),
Act = () =>
{
EnsureComp<KillSignComponent>(args.Target, out var comp);
comp.HideFromOwner = true;
Dirty(args.Target, comp);
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", hiddenKillSignName, Loc.GetString("admin-smite-kill-sign-hidden-description"))
};
args.Verbs.Add(hiddenKillSign);
var cluwneName = Loc.GetString("admin-smite-cluwne-name").ToLowerInvariant();
Verb cluwne = new()
{

View File

@@ -145,8 +145,8 @@ namespace Content.Server.Atmos.EntitySystems
if (!TryComp<PhysicsComponent>(uid, out var body))
return;
_fixture.TryCreateFixture(uid, component.FlammableCollisionShape, component.FlammableFixtureID, hard: false,
collisionMask: (int) CollisionGroup.FullTileLayer, body: body);
_fixture.TryCreateFixture(uid, component.FlammableCollisionShape, component.FlammableFixtureID, density: 0,
hard: false, collisionMask: (int) CollisionGroup.FullTileLayer, body: body);
}
private void OnInteractUsing(EntityUid uid, FlammableComponent flammable, InteractUsingEvent args)
@@ -228,7 +228,7 @@ namespace Content.Server.Atmos.EntitySystems
// Get the average of both entity's firestacks * mass
// Then for each entity, we divide the average by their mass and set their firestacks to that value
// An entity with a higher mass will lose some fire and transfer it to the one with lower mass.
var avg = (flammable.FireStacks * mass1 + otherFlammable.FireStacks * mass2) / 2f;
var avg = (flammable.FireStacks * mass1 + otherFlammable.FireStacks * mass2) / 2f;
// bring each entity to the same firestack mass, firestack amount is scaled by the inverse of the entity's mass
SetFireStacks(uid, avg / mass1, flammable, ignite: true);

View File

@@ -114,15 +114,18 @@ namespace Content.Server.Bible
return;
}
var userEnt = Identity.Entity(args.User, EntityManager);
var targetEnt = Identity.Entity(args.Target.Value, EntityManager);
// This only has a chance to fail if the target is not wearing anything on their head and is not a familiar.
if (!_invSystem.TryGetSlotEntity(args.Target.Value, "head", out var _) && !HasComp<FamiliarComponent>(args.Target.Value))
if (!_invSystem.TryGetSlotEntity(args.Target.Value, "head", out _) && !HasComp<FamiliarComponent>(args.Target.Value))
{
if (_random.Prob(component.FailChance))
{
var othersFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
var othersFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-others", ("user", userEnt), ("target", targetEnt), ("bible", uid));
_popupSystem.PopupEntity(othersFailMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.SmallCaution);
var selfFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
var selfFailMessage = Loc.GetString(component.LocPrefix + "-heal-fail-self", ("target", targetEnt), ("bible", uid));
_popupSystem.PopupEntity(selfFailMessage, args.User, args.User, PopupType.MediumCaution);
_audio.PlayPvs(component.BibleHitSound, args.User);
@@ -132,24 +135,25 @@ namespace Content.Server.Bible
}
}
string othersMessage;
string selfMessage;
if (_damageableSystem.TryChangeDamage(args.Target.Value, component.Damage, true, origin: uid))
{
var othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
_popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium);
othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-others", ("user", userEnt), ("target", targetEnt), ("bible", uid));
selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-self", ("target", targetEnt), ("bible", uid));
var selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
_popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.Large);
}
else
{
var othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-others", ("user", Identity.Entity(args.User, EntityManager)), ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
_popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium);
var selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-self", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("bible", uid));
_popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.Large);
_audio.PlayPvs(component.HealSoundPath, args.User);
_delay.TryResetDelay((uid, useDelay));
}
else
{
othersMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-others", ("user", userEnt), ("target", targetEnt), ("bible", uid));
selfMessage = Loc.GetString(component.LocPrefix + "-heal-success-none-self", ("target", targetEnt), ("bible", uid));
}
_popupSystem.PopupEntity(othersMessage, args.User, Filter.PvsExcept(args.User), true, PopupType.Medium);
_popupSystem.PopupEntity(selfMessage, args.User, args.User, PopupType.Large);
}
private void AddSummonVerb(EntityUid uid, SummonableComponent component, GetVerbsEvent<AlternativeVerb> args)

View File

@@ -42,7 +42,7 @@ namespace Content.Server.Body.Components
/// From which solution will this metabolizer attempt to metabolize chemicals
/// </summary>
[DataField("solution")]
public string SolutionName = BloodstreamComponent.DefaultChemicalsSolutionName;
public string SolutionName = BloodstreamComponent.DefaultBloodSolutionName;
/// <summary>
/// Does this component use a solution on it's parent entity (the body) or itself

View File

@@ -21,9 +21,6 @@ public sealed class BloodstreamSystem : SharedBloodstreamSystem
private void OnComponentInit(Entity<BloodstreamComponent> entity, ref ComponentInit args)
{
if (!SolutionContainer.EnsureSolution(entity.Owner,
entity.Comp.ChemicalSolutionName,
out var chemicalSolution) ||
!SolutionContainer.EnsureSolution(entity.Owner,
entity.Comp.BloodSolutionName,
out var bloodSolution) ||
!SolutionContainer.EnsureSolution(entity.Owner,
@@ -31,15 +28,14 @@ public sealed class BloodstreamSystem : SharedBloodstreamSystem
out var tempSolution))
return;
chemicalSolution.MaxVolume = entity.Comp.ChemicalMaxVolume;
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
bloodSolution.MaxVolume = entity.Comp.BloodReferenceSolution.Volume * entity.Comp.MaxVolumeModifier;
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
entity.Comp.BloodReferenceSolution.SetReagentData(GetEntityBloodData((entity, entity.Comp)));
// Fill blood solution with BLOOD
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
var solution = entity.Comp.BloodReagents.Clone();
solution.ScaleTo(entity.Comp.BloodMaxVolume - bloodSolution.Volume);
solution.SetReagentData(GetEntityBloodData(entity.Owner));
var solution = entity.Comp.BloodReferenceSolution.Clone();
solution.ScaleTo(entity.Comp.BloodReferenceSolution.Volume - bloodSolution.Volume);
bloodSolution.AddSolution(solution, PrototypeManager);
}
@@ -48,11 +44,14 @@ public sealed class BloodstreamSystem : SharedBloodstreamSystem
{
if (SolutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution))
{
var data = NewEntityBloodData(entity);
entity.Comp.BloodReferenceSolution.SetReagentData(data);
foreach (var reagent in bloodSolution.Contents)
{
List<ReagentData> reagentData = reagent.Reagent.EnsureReagentData();
reagentData.RemoveAll(x => x is DnaData);
reagentData.AddRange(GetEntityBloodData(entity.Owner));
reagentData.AddRange(data);
}
}
else

View File

@@ -1,3 +1,4 @@
using System.Linq;
using Content.Server.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ;
@@ -14,9 +15,7 @@ using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.Effects.Body;
using Content.Shared.EntityEffects.Effects.Solution;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Random.Helpers;
using Robust.Shared.Collections;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -134,17 +133,29 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
return;
}
// Copy the solution do not edit the original solution list
var list = solution.Contents.ToList();
// Collecting blood reagent for filtering
var ev = new MetabolismExclusionEvent();
RaiseLocalEvent(solutionEntityUid.Value, ref ev);
// randomize the reagent list so we don't have any weird quirks
// like alphabetical order or insertion order mattering for processing
var list = solution.Contents.ToArray();
_random.Shuffle(list);
bool isDead = _mobStateSystem.IsDead(solutionEntityUid.Value);
int reagents = 0;
foreach (var (reagent, quantity) in list)
{
if (!_prototypeManager.TryIndex<ReagentPrototype>(reagent.Prototype, out var proto))
continue;
// Skip blood reagents
if (ev.Reagents.Contains(reagent))
continue;
var mostToRemove = FixedPoint2.Zero;
if (proto.Metabolisms is null)
{
@@ -186,11 +197,8 @@ public sealed class MetabolizerSystem : SharedMetabolizerSystem
// if it's possible for them to be dead, and they are,
// then we shouldn't process any effects, but should probably
// still remove reagents
if (TryComp<MobStateComponent>(solutionEntityUid.Value, out var state))
{
if (!proto.WorksOnTheDead && _mobStateSystem.IsDead(solutionEntityUid.Value, state))
continue;
}
if (isDead && !proto.WorksOnTheDead)
continue;
var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value;

View File

@@ -17,8 +17,8 @@ using Content.Shared.Database;
using Content.Shared.EntityConditions;
using Content.Shared.EntityConditions.Conditions.Body;
using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.EntityEffects.Effects.Body;
using Content.Shared.EntityEffects.Effects.Damage;
using Content.Shared.Mobs.Systems;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;

View File

@@ -1,9 +1,6 @@
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Server.EntityEffects.Effects.Botany;
using Content.Shared.Atmos;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
using Content.Shared.Random;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -61,18 +58,18 @@ public partial struct SeedChemQuantity
/// <summary>
/// Minimum amount of chemical that is added to produce, regardless of the potency
/// </summary>
[DataField("Min")] public int Min;
[DataField("Min")] public FixedPoint2 Min = FixedPoint2.Epsilon;
/// <summary>
/// Maximum amount of chemical that can be produced after taking plant potency into account.
/// </summary>
[DataField("Max")] public int Max;
[DataField("Max")] public FixedPoint2 Max;
/// <summary>
/// When chemicals are added to produce, the potency of the seed is divided with this value. Final chemical amount is the result plus the `Min` value.
/// Example: PotencyDivisor of 20 with seed potency of 55 results in 2.75, 55/20 = 2.75. If minimum is 1 then final result will be 3.75 of that chemical, 55/20+1 = 3.75.
/// </summary>
[DataField("PotencyDivisor")] public int PotencyDivisor;
[DataField("PotencyDivisor")] public float PotencyDivisor;
/// <summary>
/// Inherent chemical is one that is NOT result of mutation or crossbreeding. These chemicals are removed if species mutation is executed.

View File

@@ -29,10 +29,10 @@ public sealed partial class BotanySystem
solutionContainer.RemoveAllSolution();
foreach (var (chem, quantity) in seed.Chemicals)
{
var amount = FixedPoint2.New(quantity.Min);
var amount = quantity.Min;
if (quantity.PotencyDivisor > 0 && seed.Potency > 0)
amount += FixedPoint2.New(seed.Potency / quantity.PotencyDivisor);
amount = FixedPoint2.New(MathHelper.Clamp(amount.Float(), quantity.Min, quantity.Max));
amount += seed.Potency / quantity.PotencyDivisor;
amount = FixedPoint2.Clamp(amount, quantity.Min, quantity.Max);
solutionContainer.MaxVolume += amount;
solutionContainer.AddReagent(chem, amount);
}

View File

@@ -53,6 +53,7 @@ public sealed class PlantHolderSystem : EntitySystem
public const float HydroponicsSpeedMultiplier = 1f;
public const float HydroponicsConsumptionMultiplier = 2f;
public readonly FixedPoint2 PlantMetabolismRate = FixedPoint2.New(1);
private static readonly ProtoId<TagPrototype> HoeTag = "Hoe";
private static readonly ProtoId<TagPrototype> PlantSampleTakerTag = "PlantSampleTaker";
@@ -885,13 +886,18 @@ public sealed class PlantHolderSystem : EntitySystem
if (solution.Volume > 0 && component.MutationLevel < 25)
{
foreach (var entry in component.SoilSolution.Value.Comp.Solution.Contents)
// Don't apply any effects to a non-unique seed ever! Remove this when botany code is sane...
EnsureUniqueSeed(uid, component);
foreach (var entry in solution.Contents)
{
if (entry.Quantity < PlantMetabolismRate)
continue;
var reagentProto = _prototype.Index<ReagentPrototype>(entry.Reagent.Prototype);
_entityEffects.ApplyEffects(uid, reagentProto.PlantMetabolisms.ToArray(), entry.Quantity.Float());
_entityEffects.ApplyEffects(uid, reagentProto.PlantMetabolisms.ToArray(), entry.Quantity);
}
_solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, FixedPoint2.New(1));
_solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, PlantMetabolismRate);
}
CheckLevelSanity(uid, component);

View File

@@ -1,81 +0,0 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Server.Popups;
namespace Content.Server.Chemistry.EntitySystems;
public sealed partial class ReactionMixerSystem : EntitySystem
{
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ReactionMixerComponent, AfterInteractEvent>(OnAfterInteract, before: [typeof(IngestionSystem)]);
SubscribeLocalEvent<ReactionMixerComponent, ShakeEvent>(OnShake);
SubscribeLocalEvent<ReactionMixerComponent, ReactionMixDoAfterEvent>(OnDoAfter);
}
private void OnAfterInteract(Entity<ReactionMixerComponent> entity, ref AfterInteractEvent args)
{
if (!args.Target.HasValue || !args.CanReach || !entity.Comp.MixOnInteract)
return;
if (!MixAttempt(entity, args.Target.Value, out _))
return;
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, entity.Comp.TimeToMix, new ReactionMixDoAfterEvent(), entity, args.Target.Value, entity);
_doAfterSystem.TryStartDoAfter(doAfterArgs);
args.Handled = true;
}
private void OnDoAfter(Entity<ReactionMixerComponent> entity, ref ReactionMixDoAfterEvent args)
{
//Do again to get the solution again
if (!MixAttempt(entity, args.Target!.Value, out var solution))
return;
_popup.PopupEntity(Loc.GetString(entity.Comp.MixMessage, ("mixed", Identity.Entity(args.Target!.Value, EntityManager)), ("mixer", Identity.Entity(entity.Owner, EntityManager))), args.User, args.User);
_solutionContainers.UpdateChemicals(solution!.Value, true, entity.Comp);
var afterMixingEvent = new AfterMixingEvent(entity, args.Target!.Value);
RaiseLocalEvent(entity, afterMixingEvent);
}
private void OnShake(Entity<ReactionMixerComponent> entity, ref ShakeEvent args)
{
if (!MixAttempt(entity, entity, out var solution))
return;
_solutionContainers.UpdateChemicals(solution!.Value, true, entity.Comp);
var afterMixingEvent = new AfterMixingEvent(entity, entity);
RaiseLocalEvent(entity, afterMixingEvent);
}
private bool MixAttempt(EntityUid ent, EntityUid target, out Entity<SolutionComponent>? solution)
{
solution = null;
var mixAttemptEvent = new MixingAttemptEvent(ent);
RaiseLocalEvent(ent, ref mixAttemptEvent);
if (mixAttemptEvent.Cancelled)
{
return false;
}
if (!_solutionContainers.TryGetMixableSolution(target, out solution, out _))
return false;
return true;
}
}

View File

@@ -148,7 +148,7 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
// Take our portion of the adjusted solution for this target
var individualInjection = solutionToInject.SplitSolution(volumePerBloodstream);
// Inject our portion into the target's bloodstream
if (_bloodstream.TryAddToChemicals(targetBloodstream.AsNullable(), individualInjection))
if (_bloodstream.TryAddToBloodstream(targetBloodstream.AsNullable(), individualInjection))
anySuccess = true;
}

View File

@@ -3,7 +3,7 @@ using Robust.Shared.Prototypes;
using System.IO;
using System.Linq;
using System.Text.Json;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.EntityEffects.Effects.Damage;
namespace Content.Server.Corvax.GuideGenerator;
public sealed class HealthChangeReagentsJsonGenerator

View File

@@ -0,0 +1,32 @@
using Content.Shared.DeviceLinking.Components;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.DeviceLinking.Systems;
using Robust.Shared.Random;
namespace Content.Server.DeviceLinking.Systems;
public sealed class RandomGateSystem : SharedRandomGateSystem
{
[Dependency] private readonly DeviceLinkSystem _deviceLink = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RandomGateComponent, SignalReceivedEvent>(OnSignalReceived);
}
private void OnSignalReceived(Entity<RandomGateComponent> ent, ref SignalReceivedEvent args)
{
if (args.Port != ent.Comp.InputPort)
return;
var output = _random.Prob(ent.Comp.SuccessProbability);
if (output != ent.Comp.LastOutput)
{
ent.Comp.LastOutput = output;
Dirty(ent);
_deviceLink.SendSignal(ent.Owner, ent.Comp.OutputPort, output);
}
}
}

View File

@@ -7,13 +7,11 @@ namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
public sealed partial class PlantAdjustPotencyEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantAdjustPotency>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustPotency> args)
{
if (entity.Comp.Seed == null || entity.Comp.Dead)
return;
_plantHolder.EnsureUniqueSeed(entity, entity.Comp);
entity.Comp.Seed.Potency = Math.Max(entity.Comp.Seed.Potency + args.Effect.Amount, 1);
}
}

View File

@@ -9,7 +9,6 @@ namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
public sealed partial class PlantDestroySeedsEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantDestroySeeds>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
[Dependency] private readonly PopupSystem _popup = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantDestroySeeds> args)
@@ -20,7 +19,6 @@ public sealed partial class PlantDestroySeedsEntityEffectSystem : EntityEffectSy
if (entity.Comp.Seed.Seedless)
return;
_plantHolder.EnsureUniqueSeed(entity, entity.Comp);
_popup.PopupEntity(
Loc.GetString("botany-plant-seedsdestroyed"),
entity,

View File

@@ -9,7 +9,6 @@ namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
public sealed partial class PlantDiethylamineEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantDiethylamine>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantDiethylamine> args)
{
@@ -18,13 +17,11 @@ public sealed partial class PlantDiethylamineEntityEffectSystem : EntityEffectSy
if (_random.Prob(0.1f))
{
_plantHolder.EnsureUniqueSeed(entity, entity);
entity.Comp.Seed!.Lifespan++;
}
if (_random.Prob(0.1f))
{
_plantHolder.EnsureUniqueSeed(entity, entity);
entity.Comp.Seed!.Endurance++;
}
}

View File

@@ -8,7 +8,6 @@ namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
public sealed partial class PlantRestoreSeedsEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantRestoreSeeds>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
[Dependency] private readonly PopupSystem _popup = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantRestoreSeeds> args)
@@ -19,7 +18,6 @@ public sealed partial class PlantRestoreSeedsEntityEffectSystem : EntityEffectSy
if (!entity.Comp.Seed.Seedless)
return;
_plantHolder.EnsureUniqueSeed(entity, entity.Comp);
_popup.PopupEntity(Loc.GetString("botany-plant-seedsrestored"), entity);
entity.Comp.Seed.Seedless = false;
}

View File

@@ -14,7 +14,6 @@ namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
public sealed partial class RobustHarvestEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, RobustHarvest>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<RobustHarvest> args)
{
@@ -23,7 +22,6 @@ public sealed partial class RobustHarvestEntityEffectSystem : EntityEffectSystem
if (entity.Comp.Seed.Potency < args.Effect.PotencyLimit)
{
_plantHolder.EnsureUniqueSeed(entity, entity.Comp);
entity.Comp.Seed.Potency = Math.Min(entity.Comp.Seed.Potency + args.Effect.PotencyIncrease, args.Effect.PotencyLimit);
if (entity.Comp.Seed.Potency > args.Effect.PotencySeedlessThreshold)
@@ -34,7 +32,6 @@ public sealed partial class RobustHarvestEntityEffectSystem : EntityEffectSystem
else if (entity.Comp.Seed.Yield > 1 && _random.Prob(0.1f))
{
// Too much of a good thing reduces yield
_plantHolder.EnsureUniqueSeed(entity, entity.Comp);
entity.Comp.Seed.Yield--;
}
}

View File

@@ -2,6 +2,7 @@ using Content.Server.Botany;
using Content.Server.Botany.Components;
using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.Effects.Botany;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -23,7 +24,7 @@ public sealed partial class PlantMutateChemicalsEntityEffectSystem : EntityEffec
// Add a random amount of a random chemical to this set of chemicals
var pick = _random.Pick(randomChems);
var chemicalId = _random.Pick(pick.Reagents);
var amount = _random.Next(1, (int)pick.Quantity);
var amount = _random.NextFloat(0.1f, (float)pick.Quantity);
var seedChemQuantity = new SeedChemQuantity();
if (chemicals.ContainsKey(chemicalId))
{
@@ -32,12 +33,12 @@ public sealed partial class PlantMutateChemicalsEntityEffectSystem : EntityEffec
}
else
{
seedChemQuantity.Min = 1;
seedChemQuantity.Max = 1 + amount;
seedChemQuantity.Min = FixedPoint2.Epsilon;
seedChemQuantity.Max = FixedPoint2.Zero + amount;
seedChemQuantity.Inherent = false;
}
var potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max);
seedChemQuantity.PotencyDivisor = potencyDivisor;
var potencyDivisor = 100f / seedChemQuantity.Max;
seedChemQuantity.PotencyDivisor = (float) potencyDivisor;
chemicals[chemicalId] = seedChemQuantity;
}
}

View File

@@ -1,4 +1,3 @@
using System.Numerics;
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
@@ -17,6 +16,7 @@ using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Numerics;
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
namespace Content.Server.Explosion.EntitySystems;
@@ -202,6 +202,8 @@ public sealed partial class ExplosionSystem
HashSet<EntityUid> processed,
string id,
float? fireStacks,
float? temperature,
float currentIntensity,
EntityUid? cause)
{
var size = grid.Comp.TileSize;
@@ -234,6 +236,12 @@ public sealed partial class ExplosionSystem
ProcessEntity(entity, epicenter, damage, throwForce, id, null, fireStacks, cause);
}
// heat the atmosphere
if (temperature != null)
{
_atmosphere.HotspotExpose(grid.Owner, tile, temperature.Value, currentIntensity, cause, true);
}
// Walls and reinforced walls will break into girders. These girders will also be considered turf-blocking for
// the purposes of destroying floors. Again, ideally the process of damaging an entity should somehow return
// information about the entities that were spawned as a result, but without that information we just have to
@@ -457,7 +465,7 @@ public sealed partial class ExplosionSystem
}
}
// ignite
// ignite entities with the flammable component
if (fireStacksOnIgnite != null)
{
if (_flammableQuery.TryGetComponent(uid, out var flammable))
@@ -855,6 +863,8 @@ sealed class Explosion
ProcessedEntities,
ExplosionType.ID,
ExplosionType.FireStacks,
ExplosionType.Temperature,
_currentIntensity,
Cause);
// If the floor is not blocked by some dense object, damage the floor tiles.

View File

@@ -54,6 +54,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly FlammableSystem _flammableSystem = default!;
[Dependency] private readonly DestructibleSystem _destructibleSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
private EntityQuery<FlammableComponent> _flammableQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;

View File

@@ -264,14 +264,14 @@ public sealed class SmokeSystem : EntitySystem
if (!TryComp<BloodstreamComponent>(entity, out var bloodstream))
return;
if (!_solutionContainerSystem.ResolveSolution(entity, bloodstream.ChemicalSolutionName, ref bloodstream.ChemicalSolution, out var chemSolution) || chemSolution.AvailableVolume <= 0)
if (!_solutionContainerSystem.ResolveSolution(entity, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution) || bloodSolution.AvailableVolume <= 0)
return;
var blockIngestion = _internals.AreInternalsWorking(entity);
var cloneSolution = solution.Clone();
var availableTransfer = FixedPoint2.Min(cloneSolution.Volume, component.TransferRate);
var transferAmount = FixedPoint2.Min(availableTransfer, chemSolution.AvailableVolume);
var transferAmount = FixedPoint2.Min(availableTransfer, bloodSolution.AvailableVolume);
var transferSolution = cloneSolution.SplitSolution(transferAmount);
foreach (var reagentQuantity in transferSolution.Contents.ToArray())
@@ -287,7 +287,7 @@ public sealed class SmokeSystem : EntitySystem
if (blockIngestion)
return;
if (_blood.TryAddToChemicals((entity, bloodstream), transferSolution))
if (_blood.TryAddToBloodstream((entity, bloodstream), transferSolution))
{
// Log solution addition by smoke
_logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} ingested smoke {SharedSolutionContainerSystem.ToPrettyString(transferSolution)}");

View File

@@ -3,6 +3,7 @@ using Content.Server.Administration;
using Content.Server.Maps;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Maps;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;

View File

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using Content.Server.GameTicking.Presets;
using Content.Server.Maps;
using Content.Shared.CCVar;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Shared.Player;

View File

@@ -8,6 +8,7 @@ using Content.Server.Roles;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.GameTicking;
using Content.Shared.Maps;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Preferences;
@@ -393,7 +394,9 @@ namespace Content.Server.GameTicking
}
else
{
profile = HumanoidCharacterProfile.Random();
var speciesToBlacklist =
new HashSet<string>(_cfg.GetCVar(CCVars.ICNewAccountSpeciesBlacklist).Split(","));
profile = HumanoidCharacterProfile.Random(speciesToBlacklist);
}
readyPlayerProfiles.Add(userId, profile);
}

View File

@@ -1,6 +1,5 @@
using Content.Server.GameTicking.Rules;
using Content.Server.Maps;
using Content.Shared.GridPreloader.Prototypes;
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.Holiday;
using Content.Shared.Maps;
namespace Content.Server.Maps.Conditions;

View File

@@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.GameTicking;
using Content.Shared.CCVar;
using Content.Shared.Maps;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;

View File

@@ -1,3 +1,4 @@
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;

View File

@@ -1,3 +1,5 @@
using Content.Shared.Maps;
namespace Content.Server.Maps;
/// <summary>

View File

@@ -18,6 +18,7 @@ using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using Content.Server.Body.Systems;
namespace Content.Server.Medical;
@@ -32,6 +33,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
public override void Initialize()
{
@@ -204,7 +206,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
_solutionContainerSystem.ResolveSolution(target, bloodstream.BloodSolutionName,
ref bloodstream.BloodSolution, out var bloodSolution))
{
bloodAmount = bloodSolution.FillFraction;
bloodAmount = _bloodstreamSystem.GetBloodLevel(target);
bleeding = bloodstream.BleedAmount > 0;
}

View File

@@ -27,12 +27,11 @@ public sealed class SuitSensorSystem : SharedSuitSensorSystem
// check if sensor is ready to update
if (curTime < sensor.NextUpdate)
continue;
sensor.NextUpdate += sensor.UpdateRate;
if (!CheckSensorAssignedStation((uid, sensor)))
continue;
sensor.NextUpdate += sensor.UpdateRate;
// get sensor status
var status = GetSensorState((uid, sensor));
if (status == null)

View File

@@ -5,6 +5,7 @@ using Content.Shared.Explosion;
using Content.Shared.Nuke;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Nuke
@@ -164,6 +165,14 @@ namespace Content.Server.Nuke
[DataField]
public string EnteredCode = "";
/// <summary>
/// Time at which the last nuke code was entered.
/// Used to apply a cooldown to prevent clients from attempting to brute force the nuke code by sending keypad messages every tick.
/// <seealso cref="SharedNukeComponent.EnterCodeCooldown"/>
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastCodeEnteredAt = TimeSpan.Zero;
/// <summary>
/// Current status of a nuclear bomb.
/// </summary>

View File

@@ -18,11 +18,10 @@ using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using Robust.Shared.Timing;
namespace Content.Server.Nuke;
@@ -45,6 +44,7 @@ public sealed class NukeSystem : EntitySystem
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[Dependency] private readonly IGameTiming _timing = default!;
/// <summary>
/// Used to calculate when the nuke song should start playing for maximum kino with the nuke sfx
@@ -232,6 +232,12 @@ public sealed class NukeSystem : EntitySystem
if (component.Status != NukeStatus.AWAIT_CODE)
return;
var curTime = _timing.CurTime;
if (curTime < component.LastCodeEnteredAt + SharedNukeComponent.EnterCodeCooldown)
return; // Validate that they are not entering codes faster than the cooldown.
component.LastCodeEnteredAt = curTime;
UpdateStatus(uid, component);
UpdateUserInterface(uid, component);
}

View File

@@ -157,7 +157,7 @@ namespace Content.Server.Nutrition.EntitySystems
}
_reactiveSystem.DoEntityReaction(containerManager.Owner, inhaledSolution, ReactionMethod.Ingestion);
_bloodstreamSystem.TryAddToChemicals((containerManager.Owner, bloodstream), inhaledSolution);
_bloodstreamSystem.TryAddToBloodstream((containerManager.Owner, bloodstream), inhaledSolution);
}
_timer -= UpdateTimer;

View File

@@ -5,7 +5,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Power.Components;
[RegisterComponent]
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class ApcComponent : BaseApcNetComponent
{
[DataField("onReceiveMessageSound")]
@@ -34,6 +34,32 @@ public sealed partial class ApcComponent : BaseApcNetComponent
public const float HighPowerThreshold = 0.9f;
public static TimeSpan VisualsChangeDelay = TimeSpan.FromSeconds(1);
/// <summary>
/// Maximum continuous load in Watts that this APC can supply to loads. Exceeding this starts a
/// timer, which after enough overloading causes the APC to "trip" off.
/// </summary>
[DataField]
public float MaxLoad = 20e3f;
/// <summary>
/// Time that the APC can be continuously overloaded before tripping off.
/// </summary>
[DataField]
public TimeSpan TripTime = TimeSpan.FromSeconds(3);
/// <summary>
/// Time that overloading began.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan? TripStartTime;
/// <summary>
/// Set to true if the APC tripped off. Used to indicate problems in the UI. Reset by switching
/// APC on.
/// </summary>
[DataField]
public bool TripFlag;
// TODO ECS power a little better!
// End the suffering
protected override void AddSelfToNet(IApcNet apcNet)

View File

@@ -2,7 +2,9 @@ using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.Power.Pow3r;
using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
using Content.Shared.APC;
using Content.Shared.Database;
using Content.Shared.Emag.Systems;
using Content.Shared.Emp;
using Content.Shared.Popups;
@@ -18,6 +20,7 @@ namespace Content.Server.Power.EntitySystems;
public sealed class ApcSystem : EntitySystem
{
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly PopupSystem _popup = default!;
@@ -43,11 +46,12 @@ public sealed class ApcSystem : EntitySystem
public override void Update(float deltaTime)
{
var query = EntityQueryEnumerator<ApcComponent, PowerNetworkBatteryComponent, UserInterfaceComponent>();
var curTime = _gameTiming.CurTime;
while (query.MoveNext(out var uid, out var apc, out var battery, out var ui))
{
if (apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < _gameTiming.CurTime && _ui.IsUiOpen((uid, ui), ApcUiKey.Key))
if (apc.LastUiUpdate + ApcComponent.VisualsChangeDelay < curTime && _ui.IsUiOpen((uid, ui), ApcUiKey.Key))
{
apc.LastUiUpdate = _gameTiming.CurTime;
apc.LastUiUpdate = curTime;
UpdateUIState(uid, apc, battery);
}
@@ -55,6 +59,28 @@ public sealed class ApcSystem : EntitySystem
{
UpdateApcState(uid, apc, battery);
}
// Overload
if (apc.MainBreakerEnabled && battery.CurrentSupply > apc.MaxLoad)
{
// Not already overloaded, start timer
if (apc.TripStartTime == null)
{
apc.TripStartTime = curTime;
}
else
{
if (curTime - apc.TripStartTime > apc.TripTime)
{
apc.TripFlag = true;
ApcToggleBreaker(uid, apc, battery); // off, we already checked MainBreakerEnabled above
}
}
}
else
{
apc.TripStartTime = null;
}
}
}
@@ -89,7 +115,7 @@ public sealed class ApcSystem : EntitySystem
if (_accessReader.IsAllowed(args.Actor, uid))
{
ApcToggleBreaker(uid, component);
ApcToggleBreaker(uid, component, user: args.Actor);
}
else
{
@@ -98,7 +124,12 @@ public sealed class ApcSystem : EntitySystem
}
}
public void ApcToggleBreaker(EntityUid uid, ApcComponent? apc = null, PowerNetworkBatteryComponent? battery = null)
/// <summary>Toggles the enabled state of the APC's main breaker.</summary>
public void ApcToggleBreaker(
EntityUid uid,
ApcComponent? apc = null,
PowerNetworkBatteryComponent? battery = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref apc, ref battery))
return;
@@ -106,8 +137,18 @@ public sealed class ApcSystem : EntitySystem
apc.MainBreakerEnabled = !apc.MainBreakerEnabled;
battery.CanDischarge = apc.MainBreakerEnabled;
if (apc.MainBreakerEnabled)
apc.TripFlag = false;
UpdateUIState(uid, apc);
_audio.PlayPvs(apc.OnReceiveMessageSound, uid, AudioParams.Default.WithVolume(-2f));
if (user != null)
{
var humanReadableState = apc.MainBreakerEnabled ? "Enabled" : "Disabled";
_adminLogger.Add(LogType.ItemConfigure, LogImpact.Medium,
$"{ToPrettyString(user):user} set the main breaker state of {ToPrettyString(uid):entity} to {humanReadableState:state}.");
}
}
private void OnEmagged(EntityUid uid, ApcComponent comp, ref GotEmaggedEvent args)
@@ -169,7 +210,9 @@ public sealed class ApcSystem : EntitySystem
var state = new ApcBoundInterfaceState(apc.MainBreakerEnabled,
(int) MathF.Ceiling(battery.CurrentSupply), apc.LastExternalState,
charge);
charge,
apc.MaxLoad,
apc.TripFlag);
_ui.SetUiState((uid, ui), ApcUiKey.Key, state);
}

View File

@@ -26,7 +26,7 @@ public sealed partial class BatterySystem
TrySetChargeCooldown(ent.Owner);
var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, ent.Comp.MaxCharge);
var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, delta, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref ev);
return delta;
}
@@ -61,21 +61,23 @@ public sealed partial class BatterySystem
return;
}
var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, ent.Comp.MaxCharge);
var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, ent.Comp.CurrentCharge - oldCharge, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref ev);
}
public override void SetMaxCharge(Entity<BatteryComponent?> ent, float value)
{
if (!Resolve(ent, ref ent.Comp))
return;
var old = ent.Comp.MaxCharge;
var oldCharge = ent.Comp.CurrentCharge;
ent.Comp.MaxCharge = Math.Max(value, 0);
ent.Comp.CurrentCharge = Math.Min(ent.Comp.CurrentCharge, ent.Comp.MaxCharge);
if (MathHelper.CloseTo(ent.Comp.MaxCharge, old))
return;
var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, ent.Comp.MaxCharge);
var ev = new ChargeChangedEvent(ent.Comp.CurrentCharge, ent.Comp.CurrentCharge - oldCharge, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref ev);
}

View File

@@ -86,13 +86,10 @@ public sealed class RiggableSystem : EntitySystem
if (!ent.Comp.IsRigged)
return;
if (TryComp<BatteryComponent>(ent, out var batteryComponent))
{
if (batteryComponent.CurrentCharge == 0f)
return;
if (args.Charge == 0f)
return; // No charge to cause an explosion.
Explode(ent, batteryComponent.CurrentCharge);
}
Explode(ent, args.Charge);
}
// predicted batteries
@@ -101,13 +98,13 @@ public sealed class RiggableSystem : EntitySystem
if (!ent.Comp.IsRigged)
return;
if (TryComp<PredictedBatteryComponent>(ent, out var predictedBatteryComponent))
{
var charge = _predictedBattery.GetCharge((ent.Owner, predictedBatteryComponent));
if (charge == 0f)
return;
if (args.CurrentCharge == 0f)
return; // No charge to cause an explosion.
Explode(ent, charge);
}
// Don't explode if we are not using any charge.
if (args.CurrentChargeRate == 0f && args.Delta == 0f)
return;
Explode(ent, args.CurrentCharge);
}
}

View File

@@ -361,7 +361,9 @@ namespace Content.Server.Preferences.Managers
var prefs = await _db.GetPlayerPreferencesAsync(userId, cancel);
if (prefs is null)
{
return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random(), cancel);
var speciesToBlacklist =
new HashSet<string>(_cfg.GetCVar(CCVars.ICNewAccountSpeciesBlacklist).Split(","));
return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random(speciesToBlacklist), cancel);
}
return prefs;

View File

@@ -1,78 +0,0 @@
using Content.Server.Body.Systems;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids.Components;
using Content.Shared.Rootable;
using Robust.Shared.Timing;
namespace Content.Server.Rootable;
// TODO: Move all of this to shared
/// <summary>
/// Adds an action to toggle rooting to the ground, primarily for the Diona species.
/// </summary>
public sealed class RootableSystem : SharedRootableSystem
{
[Dependency] private readonly ISharedAdminLogManager _logger = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly ReactiveSystem _reactive = default!;
[Dependency] private readonly BloodstreamSystem _blood = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<RootableComponent, BloodstreamComponent>();
var curTime = _timing.CurTime;
while (query.MoveNext(out var uid, out var rooted, out var bloodstream))
{
if (!rooted.Rooted || rooted.PuddleEntity == null || curTime < rooted.NextUpdate || !PuddleQuery.TryComp(rooted.PuddleEntity, out var puddleComp))
continue;
rooted.NextUpdate += rooted.TransferFrequency;
PuddleReact((uid, rooted, bloodstream), (rooted.PuddleEntity.Value, puddleComp!));
}
}
/// <summary>
/// Determines if the puddle is set up properly and if so, moves on to reacting.
/// </summary>
private void PuddleReact(Entity<RootableComponent, BloodstreamComponent> entity, Entity<PuddleComponent> puddleEntity)
{
if (!_solutionContainer.ResolveSolution(puddleEntity.Owner, puddleEntity.Comp.SolutionName, ref puddleEntity.Comp.Solution, out var solution) ||
solution.Contents.Count == 0)
{
return;
}
ReactWithEntity(entity, puddleEntity, solution);
}
/// <summary>
/// Attempt to transfer an amount of the solution to the entity's bloodstream.
/// </summary>
private void ReactWithEntity(Entity<RootableComponent, BloodstreamComponent> entity, Entity<PuddleComponent> puddleEntity, Solution solution)
{
if (!_solutionContainer.ResolveSolution(entity.Owner, entity.Comp2.ChemicalSolutionName, ref entity.Comp2.ChemicalSolution, out var chemSolution) || chemSolution.AvailableVolume <= 0)
return;
var availableTransfer = FixedPoint2.Min(solution.Volume, entity.Comp1.TransferRate);
var transferAmount = FixedPoint2.Min(availableTransfer, chemSolution.AvailableVolume);
var transferSolution = _solutionContainer.SplitSolution(puddleEntity.Comp.Solution!.Value, transferAmount);
_reactive.DoEntityReaction(entity, transferSolution, ReactionMethod.Ingestion);
if (_blood.TryAddToChemicals((entity, entity.Comp2), transferSolution))
{
// Log solution addition by puddle
_logger.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(entity):target} absorbed puddle {SharedSolutionContainerSystem.ToPrettyString(transferSolution)}");
}
}
}

View File

@@ -68,7 +68,7 @@ public sealed class StationAiSystem : SharedStationAiSystem
private readonly ProtoId<JobPrototype> _stationAiJob = "StationAi";
private readonly EntProtoId _stationAiBrain = "StationAiBrain";
private readonly ProtoId<AlertPrototype> _batteryAlert = "BorgBattery";
private readonly ProtoId<AlertPrototype> _batteryAlert = "AiBattery";
private readonly ProtoId<AlertPrototype> _damageAlert = "BorgHealth";
public override void Initialize()

View File

@@ -1,4 +1,5 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos.Components;
using Content.Shared.Trigger;
using Content.Shared.Trigger.Components.Effects;
@@ -31,7 +32,10 @@ public sealed class FireStackOnTriggerSystem : EntitySystem
if (target == null)
return;
_flame.AdjustFireStacks(target.Value, ent.Comp.FireStacks, ignite: ent.Comp.DoIgnite);
if (!TryComp<FlammableComponent>(target.Value, out var flammable))
return;
_flame.AdjustFireStacks(target.Value, ent.Comp.FireStacks, ignite: ent.Comp.DoIgnite, flammable: flammable);
args.Handled = true;
}
@@ -46,7 +50,10 @@ public sealed class FireStackOnTriggerSystem : EntitySystem
if (target == null)
return;
_flame.Extinguish(target.Value);
if (!TryComp<FlammableComponent>(target.Value, out var flammable))
return;
_flame.Extinguish(target.Value, flammable: flammable);
args.Handled = true;
}

View File

@@ -27,6 +27,12 @@ public sealed partial class VoiceMaskComponent : Component
[DataField]
public ProtoId<SpeechVerbPrototype>? VoiceMaskSpeechVerb;
/// <summary>
/// If true will override the users identity with whatever <see cref="VoiceMaskName"/> is.
/// </summary>
[DataField]
public bool OverrideIdentity;
/// <summary>
/// The action that gets displayed when the voice mask is equipped.
/// </summary>
@@ -45,3 +51,4 @@ public sealed partial class VoiceMaskComponent : Component
[DataField]
public EntityUid? ActionEntity;
}

View File

@@ -4,6 +4,9 @@ using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Clothing;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.IdentityManagement.Components;
using Content.Shared.Implants;
using Content.Shared.Inventory;
using Content.Shared.Lock;
using Content.Shared.Popups;
@@ -26,6 +29,7 @@ public sealed partial class VoiceMaskSystem : EntitySystem
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
// CCVar.
private int _maxNameLength;
@@ -33,7 +37,11 @@ public sealed partial class VoiceMaskSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerName);
SubscribeLocalEvent<VoiceMaskComponent, InventoryRelayedEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameInventory);
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<TransformSpeakerNameEvent>>(OnTransformSpeakerNameImplant);
SubscribeLocalEvent<VoiceMaskComponent, ImplantRelayEvent<SeeIdentityAttemptEvent>>(OnSeeIdentityAttemptEvent);
SubscribeLocalEvent<VoiceMaskComponent, ImplantImplantedEvent>(OnImplantImplantedEvent);
SubscribeLocalEvent<VoiceMaskComponent, ImplantRemovedEvent>(OnImplantRemovedEventEvent);
SubscribeLocalEvent<VoiceMaskComponent, LockToggledEvent>(OnLockToggled);
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeNameMessage>(OnChangeName);
SubscribeLocalEvent<VoiceMaskComponent, VoiceMaskChangeVerbMessage>(OnChangeVerb);
@@ -43,10 +51,30 @@ public sealed partial class VoiceMaskSystem : EntitySystem
InitializeTTS(); // Corvax-TTS
}
private void OnTransformSpeakerName(Entity<VoiceMaskComponent> entity, ref InventoryRelayedEvent<TransformSpeakerNameEvent> args)
private void OnTransformSpeakerNameInventory(Entity<VoiceMaskComponent> entity, ref InventoryRelayedEvent<TransformSpeakerNameEvent> args)
{
args.Args.VoiceName = GetCurrentVoiceName(entity);
args.Args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.Args.SpeechVerb;
TransformVoice(entity, args.Args);
}
private void OnTransformSpeakerNameImplant(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<TransformSpeakerNameEvent> args)
{
TransformVoice(entity, args.Event);
}
private void OnSeeIdentityAttemptEvent(Entity<VoiceMaskComponent> entity, ref ImplantRelayEvent<SeeIdentityAttemptEvent> args)
{
if (entity.Comp.OverrideIdentity)
args.Event.NameOverride = GetCurrentVoiceName(entity);
}
private void OnImplantImplantedEvent(Entity<VoiceMaskComponent> entity, ref ImplantImplantedEvent ev)
{
_identity.QueueIdentityUpdate(ev.Implanted);
}
private void OnImplantRemovedEventEvent(Entity<VoiceMaskComponent> entity, ref ImplantRemovedEvent ev)
{
_identity.QueueIdentityUpdate(ev.Implanted);
}
private void OnLockToggled(Entity<VoiceMaskComponent> ent, ref LockToggledEvent args)
@@ -79,6 +107,9 @@ public sealed partial class VoiceMaskSystem : EntitySystem
return;
}
var nameUpdatedEvent = new VoiceMaskNameUpdatedEvent(entity, entity.Comp.VoiceMaskName, message.Name);
RaiseLocalEvent(message.Actor, ref nameUpdatedEvent);
entity.Comp.VoiceMaskName = message.Name;
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(message.Actor):player} set voice of {ToPrettyString(entity):mask}: {entity.Comp.VoiceMaskName}");
@@ -123,5 +154,11 @@ public sealed partial class VoiceMaskSystem : EntitySystem
{
return entity.Comp.VoiceMaskName ?? Loc.GetString("voice-mask-default-name-override");
}
private void TransformVoice(Entity<VoiceMaskComponent> entity, TransformSpeakerNameEvent args)
{
args.VoiceName = GetCurrentVoiceName(entity);
args.SpeechVerb = entity.Comp.VoiceMaskSpeechVerb ?? args.SpeechVerb;
}
#endregion
}

View File

@@ -6,12 +6,12 @@ using Content.Server.Administration.Managers;
using Content.Server.Discord.WebhookMessages;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Presets;
using Content.Server.Maps;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Maps;
using Content.Shared.Players;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Voting;

View File

@@ -13,7 +13,7 @@ namespace Content.Server.Worldgen.Prototypes;
public sealed partial class BiomePrototype : IPrototype, IInheritingPrototype
{
/// <inheritdoc />
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<EntityPrototype>))]
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<BiomePrototype>))]
public string[]? Parents { get; private set; }
/// <inheritdoc />

View File

@@ -83,7 +83,7 @@ public class NoiseChannelConfig
public sealed partial class NoiseChannelPrototype : NoiseChannelConfig, IPrototype, IInheritingPrototype
{
/// <inheritdoc />
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<EntityPrototype>))]
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<NoiseChannelPrototype>))]
public string[]? Parents { get; private set; }
/// <inheritdoc />

View File

@@ -194,7 +194,7 @@ public sealed partial class ZombieSystem
zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
if (TryComp<BloodstreamComponent>(target, out var stream) && stream.BloodReagents is { } reagents)
if (TryComp<BloodstreamComponent>(target, out var stream) && stream.BloodReferenceSolution is { } reagents)
zombiecomp.BeforeZombifiedBloodReagents = reagents.Clone();
_humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);

View File

@@ -181,13 +181,17 @@ namespace Content.Shared.APC
public readonly int Power;
public readonly ApcExternalPowerState ApcExternalPower;
public readonly float Charge;
public readonly float MaxLoad;
public readonly bool Tripped;
public ApcBoundInterfaceState(bool mainBreaker, int power, ApcExternalPowerState apcExternalPower, float charge)
public ApcBoundInterfaceState(bool mainBreaker, int power, ApcExternalPowerState apcExternalPower, float charge, float maxLoad, bool tripped)
{
MainBreaker = mainBreaker;
Power = power;
ApcExternalPower = apcExternalPower;
Charge = charge;
MaxLoad = maxLoad;
Tripped = tripped;
}
public bool Equals(ApcBoundInterfaceState? other)
@@ -197,7 +201,9 @@ namespace Content.Shared.APC
return MainBreaker == other.MainBreaker &&
Power == other.Power &&
ApcExternalPower == other.ApcExternalPower &&
MathHelper.CloseTo(Charge, other.Charge);
MathHelper.CloseTo(Charge, other.Charge) &&
MathHelper.CloseTo(MaxLoad, other.MaxLoad) &&
Tripped == other.Tripped;
}
public override bool Equals(object? obj)
@@ -207,7 +213,7 @@ namespace Content.Shared.APC
public override int GetHashCode()
{
return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge);
return HashCode.Combine(MainBreaker, Power, (int) ApcExternalPower, Charge, MaxLoad, Tripped);
}
}

View File

@@ -13,6 +13,12 @@ public sealed partial class AccessOverriderComponent : Component
{
public static string PrivilegedIdCardSlotId = "AccessOverrider-privilegedId";
/// <summary>
/// If the Access Overrider UI will show info about the privileged ID
/// </summary>
[DataField]
public bool ShowPrivilegedId = true;
[DataField]
public ItemSlot PrivilegedIdSlot = new();
@@ -48,6 +54,7 @@ public sealed partial class AccessOverriderComponent : Component
public readonly string PrivilegedIdName;
public readonly bool IsPrivilegedIdPresent;
public readonly bool IsPrivilegedIdAuthorized;
public readonly bool ShowPrivilegedIdGrid;
public readonly ProtoId<AccessLevelPrototype>[]? TargetAccessReaderIdAccessList;
public readonly ProtoId<AccessLevelPrototype>[]? AllowedModifyAccessList;
public readonly ProtoId<AccessLevelPrototype>[]? MissingPrivilegesList;
@@ -59,7 +66,8 @@ public sealed partial class AccessOverriderComponent : Component
ProtoId<AccessLevelPrototype>[]? missingPrivilegesList,
string privilegedIdName,
string targetLabel,
Color targetLabelColor)
Color targetLabelColor,
bool showPrivilegedIdGrid)
{
IsPrivilegedIdPresent = isPrivilegedIdPresent;
IsPrivilegedIdAuthorized = isPrivilegedIdAuthorized;
@@ -69,6 +77,7 @@ public sealed partial class AccessOverriderComponent : Component
PrivilegedIdName = privilegedIdName;
TargetLabel = targetLabel;
TargetLabelColor = targetLabelColor;
ShowPrivilegedIdGrid = showPrivilegedIdGrid;
}
}

View File

@@ -57,6 +57,8 @@ public sealed partial class IdCardConsoleComponent : Component
"Cryogenics",
"Engineering",
"External",
"GenpopEnter",
"GenpopLeave",
"HeadOfPersonnel",
"HeadOfSecurity",
"Hydroponics",

View File

@@ -1,6 +1,43 @@
using Robust.Shared.GameStates;
using System.Numerics;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Shared.Administration.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class KillSignComponent : Component;
/// <summary>
/// Displays a sprite above an entity.
/// By default a huge sign saying "KILL".
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
public sealed partial class KillSignComponent : Component
{
/// <summary>
/// The sprite show above the entity.
/// </summary>
[DataField, AutoNetworkedField]
public SpriteSpecifier? Sprite = new SpriteSpecifier.Rsi(new ResPath("Objects/Misc/killsign.rsi"), "kill");
/// <summary>
/// Whether the granted layer should always be forced to be unshaded.
/// </summary>
[DataField, AutoNetworkedField]
public bool ForceUnshaded = true;
/// <summary>
/// Whether the granted layer should be offset to be above the entity.
/// </summary>
[DataField, AutoNetworkedField]
public bool DoOffset = true;
/// <summary>
/// Prevents the sign from displaying to the owner of the component, allowing everyone but them to see it.
/// </summary>
[DataField, AutoNetworkedField]
public bool HideFromOwner = false;
/// <summary>
/// The scale of the sprite.
/// </summary>
[DataField, AutoNetworkedField]
public Vector2 Scale = Vector2.One;
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.Utility;
namespace Content.Shared.Alert;
@@ -7,8 +8,17 @@ namespace Content.Shared.Alert;
/// An alert popup with associated icon, tooltip, and other data.
/// </summary>
[Prototype]
public sealed partial class AlertPrototype : IPrototype
public sealed partial class AlertPrototype : IPrototype, IInheritingPrototype
{
/// <inheritdoc />
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<AlertPrototype>))]
public string[]? Parents { get; private set; }
/// <inheritdoc />
[NeverPushInheritance]
[AbstractDataField]
public bool Abstract { get; private set; }
/// <summary>
/// Type of alert, no 2 alert prototypes should have the same one.
/// </summary>

View File

@@ -1,6 +1,7 @@
using Content.Shared.Alert;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
@@ -19,7 +20,6 @@ namespace Content.Shared.Body.Components;
[Access(typeof(SharedBloodstreamSystem))]
public sealed partial class BloodstreamComponent : Component
{
public const string DefaultChemicalsSolutionName = "chemicals";
public const string DefaultBloodSolutionName = "bloodstream";
public const string DefaultBloodTemporarySolutionName = "bloodstreamTemporary";
@@ -138,26 +138,26 @@ public sealed partial class BloodstreamComponent : Component
// TODO probably damage bleed thresholds.
/// <summary>
/// Max volume of internal chemical solution storage
/// Modifier applied to <see cref="BloodstreamComponent.BloodReferenceSolution.Volume"/> to determine maximum volume for bloodstream.
/// </summary>
[DataField]
public FixedPoint2 ChemicalMaxVolume = FixedPoint2.New(250);
[DataField, AutoNetworkedField]
public float MaxVolumeModifier = 2f;
/// <summary>
/// Max volume of internal blood storage,
/// and starting level of blood.
/// </summary>
[DataField]
public FixedPoint2 BloodMaxVolume = FixedPoint2.New(300);
/// <summary>
/// Which reagents are considered this entities 'blood'?
/// Defines which reagents are considered as 'blood' and how much of it is normal.
/// </summary>
/// <remarks>
/// Slime-people might use slime as their blood or something like that.
/// </remarks>
[DataField, AutoNetworkedField]
public Solution BloodReagents = new([new("Blood", 1)]);
public Solution BloodReferenceSolution = new([new("Blood", 300)]);
/// <summary>
/// Caches the blood data of an entity.
/// This is modified by DNA on init so it's not savable.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public List<ReagentData>? BloodData;
/// <summary>
/// Name/Key that <see cref="BloodSolution"/> is indexed by.
@@ -165,12 +165,6 @@ public sealed partial class BloodstreamComponent : Component
[DataField]
public string BloodSolutionName = DefaultBloodSolutionName;
/// <summary>
/// Name/Key that <see cref="ChemicalSolution"/> is indexed by.
/// </summary>
[DataField]
public string ChemicalSolutionName = DefaultChemicalsSolutionName;
/// <summary>
/// Name/Key that <see cref="TemporarySolution"/> is indexed by.
/// </summary>
@@ -183,12 +177,6 @@ public sealed partial class BloodstreamComponent : Component
[ViewVariables]
public Entity<SolutionComponent>? BloodSolution;
/// <summary>
/// Internal solution for reagent storage
/// </summary>
[ViewVariables]
public Entity<SolutionComponent>? ChemicalSolution;
/// <summary>
/// Temporary blood solution.
/// When blood is lost, it goes to this solution, and when this

View File

@@ -45,7 +45,7 @@ namespace Content.Shared.Body.Components
/// What solution should this stomach push reagents into, on the body?
/// </summary>
[DataField]
public string BodySolutionName = "chemicals";
public string BodySolutionName = BloodstreamComponent.DefaultBloodSolutionName;
/// <summary>
/// Time between reagents being ingested and them being

View File

@@ -0,0 +1,13 @@
using Content.Shared.Chemistry.Reagent;
namespace Content.Shared.Body.Events;
/// <summary>
/// Event called by <see cref="Content.Server.Body.Systems.MetabolizerSystem"/> to get a list of
/// blood like reagents for metabolism to skip.
/// </summary>
[ByRefEvent]
public readonly record struct MetabolismExclusionEvent()
{
public readonly List<ReagentId> Reagents = [];
}

View File

@@ -53,6 +53,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
SubscribeLocalEvent<BloodstreamComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<BloodstreamComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
SubscribeLocalEvent<BloodstreamComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<BloodstreamComponent, MetabolismExclusionEvent>(OnMetabolismExclusion);
}
public override void Update(float frameTime)
@@ -69,52 +70,41 @@ public abstract class SharedBloodstreamSystem : EntitySystem
bloodstream.NextUpdate += bloodstream.AdjustedUpdateInterval;
DirtyField(uid, bloodstream, nameof(BloodstreamComponent.NextUpdate)); // needs to be dirtied on the client so it can be rerolled during prediction
if (!SolutionContainer.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution, out var bloodSolution))
if (!SolutionContainer.ResolveSolution(uid, bloodstream.BloodSolutionName, ref bloodstream.BloodSolution))
continue;
// Adds blood to their blood level if it is below the maximum; Blood regeneration. Must be alive.
if (bloodSolution.Volume < bloodSolution.MaxVolume && !_mobStateSystem.IsDead(uid))
// Blood level regulation. Must be alive.
if (!_mobStateSystem.IsDead(uid))
{
TryModifyBloodLevel((uid, bloodstream), bloodstream.BloodRefreshAmount);
TryRegulateBloodLevel(uid, bloodstream.BloodRefreshAmount);
TickBleed((uid, bloodstream));
// deal bloodloss damage if their blood level is below a threshold.
var bloodPercentage = GetBloodLevel(uid);
if (bloodPercentage < bloodstream.BloodlossThreshold)
{
// bloodloss damage is based on the base value, and modified by how low your blood level is.
var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage);
_damageableSystem.TryChangeDamage(uid, amt, ignoreResistances: false, interruptsDoAfters: false);
// Apply dizziness as a symptom of bloodloss.
// The effect is applied in a way that it will never be cleared without being healthy.
// Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out
_status.TrySetStatusEffectDuration(uid, Bloodloss);
}
else
{
// If they're healthy, we'll try and heal some bloodloss instead.
_damageableSystem.TryChangeDamage(uid, bloodstream.BloodlossHealDamage * bloodPercentage, ignoreResistances: true, interruptsDoAfters: false);
_status.TryRemoveStatusEffect(uid, Bloodloss);
}
}
// Removes blood from the bloodstream based on bleed amount (bleed rate)
// as well as stop their bleeding to a certain extent.
if (bloodstream.BleedAmount > 0)
else
{
var ev = new BleedModifierEvent(bloodstream.BleedAmount, bloodstream.BleedReductionAmount);
RaiseLocalEvent(uid, ref ev);
// Blood is removed from the bloodstream at a 1-1 rate with the bleed amount
TryModifyBloodLevel((uid, bloodstream), -ev.BleedAmount);
// Bleed rate is reduced by the bleed reduction amount in the bloodstream component.
TryModifyBleedAmount((uid, bloodstream), -ev.BleedReductionAmount);
}
// deal bloodloss damage if their blood level is below a threshold.
var bloodPercentage = GetBloodLevelPercentage((uid, bloodstream));
if (bloodPercentage < bloodstream.BloodlossThreshold && !_mobStateSystem.IsDead(uid))
{
// bloodloss damage is based on the base value, and modified by how low your blood level is.
var amt = bloodstream.BloodlossDamage / (0.1f + bloodPercentage);
_damageableSystem.TryChangeDamage(uid, amt, ignoreResistances: false, interruptsDoAfters: false);
// Apply dizziness as a symptom of bloodloss.
// The effect is applied in a way that it will never be cleared without being healthy.
// Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out
_status.TrySetStatusEffectDuration(uid, Bloodloss);
}
else if (!_mobStateSystem.IsDead(uid))
{
// If they're healthy, we'll try and heal some bloodloss instead.
_damageableSystem.TryChangeDamage(
uid,
bloodstream.BloodlossHealDamage * bloodPercentage,
ignoreResistances: true, interruptsDoAfters: false);
_status.TryRemoveStatusEffect(uid, Bloodloss);
TickBleed((uid, bloodstream));
}
}
}
@@ -133,9 +123,6 @@ public abstract class SharedBloodstreamSystem : EntitySystem
if (args.Entity == entity.Comp.BloodSolution?.Owner)
entity.Comp.BloodSolution = null;
if (args.Entity == entity.Comp.ChemicalSolution?.Owner)
entity.Comp.ChemicalSolution = null;
if (args.Entity == entity.Comp.TemporarySolution?.Owner)
entity.Comp.TemporarySolution = null;
}
@@ -170,7 +157,6 @@ public abstract class SharedBloodstreamSystem : EntitySystem
private void OnReactionAttempt(Entity<BloodstreamComponent> ent, ref SolutionRelayEvent<ReactionAttemptEvent> args)
{
if (args.Name != ent.Comp.BloodSolutionName
&& args.Name != ent.Comp.ChemicalSolutionName
&& args.Name != ent.Comp.BloodTemporarySolutionName)
{
return;
@@ -221,7 +207,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
var prob = Math.Clamp(totalFloat / 25, 0, 1);
if (totalFloat > 0 && rand.Prob(prob))
{
TryModifyBloodLevel(ent.AsNullable(), -total / 5);
TryBleedOut(ent.AsNullable(), total / 5);
_audio.PlayPredicted(ent.Comp.InstantBloodSound, ent, args.Origin);
}
@@ -269,7 +255,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
}
// If the mob's blood level is below the damage threshhold, the pale message is added.
if (GetBloodLevelPercentage(ent.AsNullable()) < ent.Comp.BloodlossThreshold)
if (GetBloodLevel(ent.AsNullable()) < ent.Comp.BloodlossThreshold)
{
args.Message.PushNewline();
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner)));
@@ -291,25 +277,46 @@ public abstract class SharedBloodstreamSystem : EntitySystem
{
TryModifyBleedAmount(ent.AsNullable(), -ent.Comp.BleedAmount);
if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
TryModifyBloodLevel(ent.AsNullable(), bloodSolution.AvailableVolume);
if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution))
{
SolutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value);
TryModifyBloodLevel(ent.AsNullable(), ent.Comp.BloodReferenceSolution.Volume);
}
}
if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution))
SolutionContainer.RemoveAllSolution(ent.Comp.ChemicalSolution.Value);
private void OnMetabolismExclusion(Entity<BloodstreamComponent> ent, ref MetabolismExclusionEvent args)
{
// Adding all blood reagents for filtering blood in metabolizer
foreach (var (reagent, _) in ent.Comp.BloodReferenceSolution)
{
args.Reagents.Add(reagent);
}
}
/// <summary>
/// Returns the current blood level as a percentage (between 0 and 1).
/// This returns the minimum amount of *usable* blood.
/// For multi reagent bloodstreams, if you have 100 of Reagent Y need 100, and 50 of Reagent X and need 100,
/// this will return 0.5f
/// </summary>
public float GetBloodLevelPercentage(Entity<BloodstreamComponent?> ent)
/// <returns>Returns the current blood level as a value from 0 to <see cref="BloodstreamComponent.MaxVolumeModifier"/></returns>
public float GetBloodLevel(Entity<BloodstreamComponent?> entity)
{
if (!Resolve(ent, ref ent.Comp)
|| !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
if (!Resolve(entity, ref entity.Comp)
|| !SolutionContainer.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution)
|| entity.Comp.BloodReferenceSolution.Volume == 0)
{
return 0.0f;
}
return bloodSolution.FillFraction;
var totalBloodLevel = FixedPoint2.New(entity.Comp.MaxVolumeModifier); // Can't go above max volume factor...
foreach (var (reagentId, quantity) in entity.Comp.BloodReferenceSolution.Contents)
{
// Ideally we use a different calculation for blood pressure, this just defines how much *usable* blood you have!
totalBloodLevel = FixedPoint2.Min(totalBloodLevel, bloodSolution.GetTotalPrototypeQuantity(reagentId.Prototype) / quantity);
}
return (float)totalBloodLevel;
}
/// <summary>
@@ -327,77 +334,138 @@ public abstract class SharedBloodstreamSystem : EntitySystem
/// <summary>
/// Attempt to transfer a provided solution to internal solution.
/// </summary>
public bool TryAddToChemicals(Entity<BloodstreamComponent?> ent, Solution solution)
public bool TryAddToBloodstream(Entity<BloodstreamComponent?> ent, Solution solution)
{
if (!Resolve(ent, ref ent.Comp, logMissing: false)
|| !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution))
|| !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution))
return false;
if (SolutionContainer.TryAddSolution(ent.Comp.ChemicalSolution.Value, solution))
if (SolutionContainer.TryAddSolution(ent.Comp.BloodSolution.Value, solution))
return true;
return false;
}
/// <summary>
/// Removes a certain amount of all reagents except of a single excluded one from the bloodstream.
/// Removes a certain amount of all reagents except of a single excluded one from the bloodstream and blood itself.
/// </summary>
public bool FlushChemicals(Entity<BloodstreamComponent?> ent, ProtoId<ReagentPrototype>? excludedReagentID, FixedPoint2 quantity)
/// <returns>
/// Solution of removed chemicals or null if none were removed.
/// </returns>
public Solution? FlushChemicals(Entity<BloodstreamComponent?> ent, FixedPoint2 quantity, ProtoId<ReagentPrototype>? excludedReagent = null )
{
if (!Resolve(ent, ref ent.Comp, logMissing: false)
|| !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution, out var chemSolution))
|| !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
return null;
var flushedSolution = new Solution();
for (var i = bloodSolution.Contents.Count - 1; i >= 0; i--)
{
var (reagentId, _) = bloodSolution.Contents[i];
if (ent.Comp.BloodReferenceSolution.ContainsPrototype(reagentId.Prototype) || reagentId.Prototype == excludedReagent)
continue;
var reagentFlushAmount = SolutionContainer.RemoveReagent(ent.Comp.BloodSolution.Value, reagentId, quantity);
flushedSolution.AddReagent(reagentId, reagentFlushAmount);
}
return flushedSolution.Volume == 0 ? null : flushedSolution;
}
/// <summary>
/// A simple helper that tries to move blood volume up or down by a specified amount.
/// Blood will not go over normal volume for this entity's bloodstream.
/// </summary>
public bool TryModifyBloodLevel(Entity<BloodstreamComponent?> ent, FixedPoint2 amount)
{
var reference = 1f;
if (amount < 0)
{
reference = 0f;
amount *= -1;
}
return TryRegulateBloodLevel(ent, amount, reference);
}
/// <summary>
/// Attempts to bring an entity's blood level to a modified equilibrium volume.
/// </summary>
/// <param name="ent">Entity whose bloodstream we're modifying.</param>
/// <param name="amount">The absolute maximum amount of blood we can add or remove.</param>
/// <param name="referenceFactor">The modifier for an entity's blood equilibrium, try to hit an entity's default blood volume multiplied by this value.</param>
/// <remarks>This CANNOT go above maximum blood volume!</remarks>
/// <returns>False if we were unable to regulate blood level. This may return true even if blood level doesn't change!</returns>
public bool TryRegulateBloodLevel(Entity<BloodstreamComponent?> ent, FixedPoint2 amount, float referenceFactor = 1f)
{
if (!Resolve(ent, ref ent.Comp, logMissing: false)
|| !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution)
|| amount == 0)
return false;
for (var i = chemSolution.Contents.Count - 1; i >= 0; i--)
referenceFactor = Math.Clamp(referenceFactor, 0f, ent.Comp.MaxVolumeModifier);
foreach (var (referenceReagent, referenceQuantity) in ent.Comp.BloodReferenceSolution)
{
var (reagentId, _) = chemSolution.Contents[i];
if (reagentId.Prototype != excludedReagentID)
var error = referenceQuantity * referenceFactor - bloodSolution.GetTotalPrototypeQuantity(referenceReagent.Prototype);
var adjustedAmount = amount * referenceQuantity / ent.Comp.BloodReferenceSolution.Volume;
if (error > 0)
{
SolutionContainer.RemoveReagent(ent.Comp.ChemicalSolution.Value, reagentId, quantity);
error = FixedPoint2.Min(error, adjustedAmount);
bloodSolution.AddReagent(referenceReagent, error);
}
else if (error < 0)
{
// invert the error since we're removing reagents...
error = FixedPoint2.Min( -error, adjustedAmount);
bloodSolution.RemoveReagent(referenceReagent, error);
}
}
return true;
}
public void TickBleed(Entity<BloodstreamComponent> entity)
{
// Removes blood from the bloodstream based on bleed amount (bleed rate)
// as well as stop their bleeding to a certain extent.
if (entity.Comp.BleedAmount <= 0)
return;
var ev = new BleedModifierEvent(entity.Comp.BleedAmount, entity.Comp.BleedReductionAmount);
RaiseLocalEvent(entity, ref ev);
// Blood is removed from the bloodstream at a 1-1 rate with the bleed amount
TryBleedOut(entity.AsNullable(), ev.BleedAmount);
// Bleed rate is reduced by the bleed reduction amount in the bloodstream component.
TryModifyBleedAmount(entity.AsNullable(), -ev.BleedReductionAmount);
}
/// <summary>
/// Attempts to modify the blood level of this entity directly.
/// Removes blood by spilling out the bloodstream.
/// </summary>
public bool TryModifyBloodLevel(Entity<BloodstreamComponent?> ent, FixedPoint2 amount)
public bool TryBleedOut(Entity<BloodstreamComponent?> ent, FixedPoint2 amount)
{
if (!Resolve(ent, ref ent.Comp, logMissing: false)
|| !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
return false;
if (amount >= 0)
|| !SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution)
|| amount <= 0)
{
var min = FixedPoint2.Min(bloodSolution.AvailableVolume, amount);
var solution = ent.Comp.BloodReagents.Clone();
solution.ScaleTo(min);
solution.SetReagentData(GetEntityBloodData(ent));
SolutionContainer.AddSolution(ent.Comp.BloodSolution.Value, solution);
return min == amount;
return false;
}
// Removal is more involved,
// since we also wanna handle moving it to the temporary solution
// and then spilling it if necessary.
var newSol = SolutionContainer.SplitSolution(ent.Comp.BloodSolution.Value, -amount);
var leakedBlood = SolutionContainer.SplitSolution(ent.Comp.BloodSolution.Value, amount);
if (!SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution))
return true;
tempSolution.AddSolution(newSol, PrototypeManager);
tempSolution.AddSolution(leakedBlood, PrototypeManager);
if (tempSolution.Volume > ent.Comp.BleedPuddleThreshold)
{
// Pass some of the chemstream into the spilled blood.
if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution))
{
var temp = SolutionContainer.SplitSolution(ent.Comp.ChemicalSolution.Value, tempSolution.Volume / 10);
tempSolution.AddSolution(temp, PrototypeManager);
}
_puddle.TrySpillAt(ent.Owner, tempSolution, out _, sound: false);
tempSolution.RemoveAllSolution();
@@ -450,13 +518,6 @@ public abstract class SharedBloodstreamSystem : EntitySystem
SolutionContainer.RemoveAllSolution(ent.Comp.BloodSolution.Value);
}
if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.ChemicalSolutionName, ref ent.Comp.ChemicalSolution, out var chemSolution))
{
tempSol.MaxVolume += chemSolution.MaxVolume;
tempSol.AddSolution(chemSolution, PrototypeManager);
SolutionContainer.RemoveAllSolution(ent.Comp.ChemicalSolution.Value);
}
if (SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodTemporarySolutionName, ref ent.Comp.TemporarySolution, out var tempSolution))
{
tempSol.MaxVolume += tempSolution.MaxVolume;
@@ -488,33 +549,43 @@ public abstract class SharedBloodstreamSystem : EntitySystem
if (!SolutionContainer.ResolveSolution(ent.Owner, ent.Comp.BloodSolutionName, ref ent.Comp.BloodSolution, out var bloodSolution))
{
ent.Comp.BloodReagents = reagents.Clone();
DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReagents));
ent.Comp.BloodReferenceSolution = reagents.Clone();
DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReferenceSolution));
return;
}
var currentVolume = FixedPoint2.Zero;
foreach (var reagent in ent.Comp.BloodReagents)
foreach (var reagent in ent.Comp.BloodReferenceSolution)
{
currentVolume += bloodSolution.RemoveReagent(reagent.Reagent, quantity: bloodSolution.Volume, ignoreReagentData: true);
}
ent.Comp.BloodReagents = reagents.Clone();
DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReagents));
ent.Comp.BloodReferenceSolution = reagents.Clone();
DirtyField(ent, ent.Comp, nameof(BloodstreamComponent.BloodReferenceSolution));
if (currentVolume == FixedPoint2.Zero)
return;
var solution = ent.Comp.BloodReagents.Clone();
var solution = ent.Comp.BloodReferenceSolution.Clone();
solution.ScaleSolution(currentVolume / solution.Volume);
solution.SetReagentData(GetEntityBloodData(ent));
SolutionContainer.AddSolution(ent.Comp.BloodSolution.Value, solution);
}
/// <summary>
/// Get the reagent data for blood that a specific entity should have.
/// </summary>
public List<ReagentData> GetEntityBloodData(EntityUid uid)
public List<ReagentData> GetEntityBloodData(Entity<BloodstreamComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return NewEntityBloodData(entity);
return entity.Comp.BloodData ?? NewEntityBloodData(entity);
}
/// <summary>
/// Gets new blood data for this entity and caches it in <see cref="BloodstreamComponent.BloodData"/>
/// </summary>
protected List<ReagentData> NewEntityBloodData(EntityUid uid)
{
var bloodData = new List<ReagentData>();
var dnaData = new DnaData();
@@ -525,7 +596,6 @@ public abstract class SharedBloodstreamSystem : EntitySystem
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
bloodData.Add(dnaData);
return bloodData;
}
}

View File

@@ -7,6 +7,7 @@ using Content.Shared.Body.Part;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Movement.Components;
using Content.Shared.Standing;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -156,17 +157,23 @@ public partial class SharedBodySystem
if (!Resolve(bodyEnt, ref bodyEnt.Comp, logMissing: false))
return;
if (legEnt.Comp.PartType == BodyPartType.Leg)
{
bodyEnt.Comp.LegEntities.Remove(legEnt);
UpdateMovementSpeed(bodyEnt);
Dirty(bodyEnt, bodyEnt.Comp);
if (legEnt.Comp.PartType != BodyPartType.Leg)
return;
if (!bodyEnt.Comp.LegEntities.Any())
{
Standing.Down(bodyEnt);
}
}
bodyEnt.Comp.LegEntities.Remove(legEnt);
UpdateMovementSpeed(bodyEnt);
Dirty(bodyEnt, bodyEnt.Comp);
if (bodyEnt.Comp.LegEntities.Count != 0)
return;
if (!TryComp<StandingStateComponent>(bodyEnt, out var standingState)
|| !standingState.Standing
|| !Standing.Down(bodyEnt, standingState: standingState))
return;
var ev = new DropHandItemsEvent();
RaiseLocalEvent(bodyEnt, ref ev);
}
private void PartRemoveDamage(Entity<BodyComponent?> bodyEnt, Entity<BodyPartComponent> partEnt)

View File

@@ -65,6 +65,13 @@ public sealed partial class CCVars
public static readonly CVarDef<string> ICRandomSpeciesWeights =
CVarDef.Create("ic.random_species_weights", "SpeciesWeights", CVar.SERVER);
/// <summary>
/// The list of species that will NOT be given to new account joins when they are assigned a random character.
/// This only affects the first time a character is made for an account, nothing else.
/// </summary>
public static readonly CVarDef<string> ICNewAccountSpeciesBlacklist =
CVarDef.Create("ic.blacklist_species_new_account", "Diona,Vulpkanin,Vox,SlimePerson", CVar.SERVER);
/// <summary>
/// Control displaying SSD indicators near players
/// </summary>

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