forked from space-syndicate/space-station-14
Merge pull request #3474 from DIMMoon1/upstream12.2
Some checks failed
CRLF Check / CRLF Check (pull_request) Successful in 19s
RSI Validator / Validate RSIs (pull_request) Failing after 57s
RGA schema validator / YAML RGA schema validator (pull_request) Successful in 1m23s
Close PRs on master / run (pull_request_target) Failing after 12s
Check Merge Conflicts / check-conflicts (pull_request_target) Successful in 7s
Labels: Review / add_label (pull_request_target) Failing after 9s
Map file schema validator / YAML map schema validator (pull_request) Successful in 2m34s
Labels: Size / size-label (pull_request_target) Failing after 4s
Labels: PR / labeler (pull_request_target) Failing after 1m18s
YAML Linter / YAML Linter (pull_request) Successful in 9m32s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
Update Wiki / Build and Publish JSON blobs to wiki (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
RSI Validator / Validate RSIs (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
Some checks failed
CRLF Check / CRLF Check (pull_request) Successful in 19s
RSI Validator / Validate RSIs (pull_request) Failing after 57s
RGA schema validator / YAML RGA schema validator (pull_request) Successful in 1m23s
Close PRs on master / run (pull_request_target) Failing after 12s
Check Merge Conflicts / check-conflicts (pull_request_target) Successful in 7s
Labels: Review / add_label (pull_request_target) Failing after 9s
Map file schema validator / YAML map schema validator (pull_request) Successful in 2m34s
Labels: Size / size-label (pull_request_target) Failing after 4s
Labels: PR / labeler (pull_request_target) Failing after 1m18s
YAML Linter / YAML Linter (pull_request) Successful in 9m32s
Build & Test Map Renderer / Build & Test Debug (push) Has been cancelled
Build & Test Map Renderer / build (ubuntu-latest) (push) Has been cancelled
Build & Test Debug / Build & Test Debug (push) Has been cancelled
Build & Test Debug / build (ubuntu-latest) (push) Has been cancelled
Test Packaging / Test Packaging (push) Has been cancelled
Update Wiki / Build and Publish JSON blobs to wiki (push) Has been cancelled
RGA schema validator / YAML RGA schema validator (push) Has been cancelled
RSI Validator / Validate RSIs (push) Has been cancelled
Map file schema validator / YAML map schema validator (push) Has been cancelled
YAML Linter / YAML Linter (push) Has been cancelled
Upstream12.2
This commit is contained in:
11
CONTRIBUTING.md
Normal file
11
CONTRIBUTING.md
Normal 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.
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<GridContainer xmlns="https://spacestation14.io"
|
||||
Columns="5"
|
||||
Columns="4"
|
||||
HorizontalAlignment="Center">
|
||||
</GridContainer>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed class AdminLogLabel : RichTextLabel
|
||||
OnVisibilityChanged += VisibilityChanged;
|
||||
}
|
||||
|
||||
public SharedAdminLog Log { get; }
|
||||
public new SharedAdminLog Log { get; }
|
||||
|
||||
public HSeparator Separator { get; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
24
Content.Client/Commands/ShowWallmountsCommand.cs
Normal file
24
Content.Client/Commands/ShowWallmountsCommand.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
5
Content.Client/DeviceLinking/Systems/RandomGateSystem.cs
Normal file
5
Content.Client/DeviceLinking/Systems/RandomGateSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.DeviceLinking.Systems;
|
||||
|
||||
namespace Content.Client.DeviceLinking.Systems;
|
||||
|
||||
public sealed class RandomGateSystem : SharedRandomGateSystem;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
19
Content.Client/DeviceLinking/UI/RandomGateSetupWindow.xaml
Normal file
19
Content.Client/DeviceLinking/UI/RandomGateSetupWindow.xaml
Normal 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>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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-->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
});
|
||||
|
||||
|
||||
25
Content.Client/RichText/UserFormattableTags.cs
Normal file
25
Content.Client/RichText/UserFormattableTags.cs
Normal 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),
|
||||
];
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
using Content.Shared.Rootable;
|
||||
|
||||
namespace Content.Client.Rootable;
|
||||
|
||||
public sealed class RootableSystem : SharedRootableSystem;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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!;
|
||||
|
||||
|
||||
58
Content.Client/Wall/WallmountDebugOverlay.cs
Normal file
58
Content.Client/Wall/WallmountDebugOverlay.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
102
Content.IntegrationTests/Tests/Atmos/AtmosTest.cs
Normal file
102
Content.IntegrationTests/Tests/Atmos/AtmosTest.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
127
Content.IntegrationTests/Tests/Atmos/RoomSpacingTest.cs
Normal file
127
Content.IntegrationTests/Tests/Atmos/RoomSpacingTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
159
Content.IntegrationTests/Tests/Atmos/TileAtmosphereTest.cs
Normal file
159
Content.IntegrationTests/Tests/Atmos/TileAtmosphereTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.IO;
|
||||
using Content.Server.Maps;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -148,7 +148,8 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
missingAccess,
|
||||
privilegedIdName,
|
||||
targetLabel,
|
||||
targetLabelColor);
|
||||
targetLabelColor,
|
||||
component.ShowPrivilegedId);
|
||||
|
||||
_userInterface.SetUiState(uid, AccessOverriderUiKey.Key, newState);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
32
Content.Server/DeviceLinking/Systems/RandomGateSystem.cs
Normal file
32
Content.Server/DeviceLinking/Systems/RandomGateSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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--;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Holiday;
|
||||
using Content.Shared.Maps;
|
||||
|
||||
namespace Content.Server.Maps.Conditions;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Maps;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Content.Shared.Maps;
|
||||
|
||||
namespace Content.Server.Maps;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,8 @@ public sealed partial class IdCardConsoleComponent : Component
|
||||
"Cryogenics",
|
||||
"Engineering",
|
||||
"External",
|
||||
"GenpopEnter",
|
||||
"GenpopLeave",
|
||||
"HeadOfPersonnel",
|
||||
"HeadOfSecurity",
|
||||
"Hydroponics",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
13
Content.Shared/Body/Events/MetabolismExclusionEvent.cs
Normal file
13
Content.Shared/Body/Events/MetabolismExclusionEvent.cs
Normal 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 = [];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user