merge remote stable wizden

This commit is contained in:
Dmitry
2026-01-08 10:32:52 +07:00
418 changed files with 8045 additions and 6427 deletions

View File

@@ -351,7 +351,7 @@ resharper_csharp_qualified_using_at_nested_scope = false
resharper_csharp_prefer_qualified_reference = false
resharper_csharp_allow_alias = false
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,slnx}]
indent_size = 2
[nuget.config]

View File

@@ -14,7 +14,7 @@
Небольшие исправления/рефакторинг освобождаются от этого требования. -->
## Требования
<!-- Подтвердите следующее, поставив X в скобках [X]: -->
<!-- Подтвердите следующее, поставив X в скобках без пробелов [X]: -->
- [ ] Я прочитал(а) и следую [Рекомендациям по оформлению Pull Request и Changelog](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
- [ ] Я добавил(а) медиафайлы к этому PR или он не требует демонстрации в игре.
<!-- Вы должны понимать, что несоблюдение вышеуказанного может привести к закрытию вашего PR по усмотрению сопровождающего -->

View File

@@ -21,7 +21,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore

View File

@@ -36,7 +36,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore

View File

@@ -36,7 +36,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore

View File

@@ -20,7 +20,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v3.2.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Get Engine Tag
run: |

View File

@@ -47,7 +47,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Get Engine Tag
run: |

View File

@@ -65,7 +65,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore

View File

@@ -26,7 +26,7 @@ jobs:
- name: Setup .NET Core
uses: actions/setup-dotnet@v4.1.0
with:
dotnet-version: 9.0.x
dotnet-version: 10.0.x
- name: Install dependencies
run: dotnet restore
- name: Build

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# MSbuild binlog files
*.binlog
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.

View File

@@ -11,7 +11,7 @@ import time
from pathlib import Path
from typing import List
SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
SOLUTION_PATH = Path("..") / "SpaceStation14.slnx"
# If this doesn't match the saved version we overwrite them all.
CURRENT_HOOKS_VERSION = "4"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"

View File

@@ -1,17 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<OutputPath>..\bin\Content.Benchmarks\</OutputPath>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputType>Exe</OutputType>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>12</LangVersion>
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
<Nullable>disable</Nullable>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
<!-- pin transitive deps -->
<PackageReference Include="System.Management" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
@@ -19,10 +22,12 @@
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Content.Tests\Content.Tests.csproj" />
<ProjectReference Include="..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Benchmarks\Robust.Benchmarks.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Server.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Benchmarks.props" />
<Import Project="..\RobustToolbox\Imports\Testing.props" />
</Project>

View File

@@ -134,7 +134,7 @@ internal sealed class AdminNameOverlay : Overlay
? null
: _prototypeManager.Index(playerInfo.RoleProto.Value);
var roleName = Loc.GetString(rolePrototype?.Name ?? RoleTypePrototype.FallbackName);
var roleName = rolePrototype?.Name ?? RoleTypePrototype.FallbackName;
var roleColor = rolePrototype?.Color ?? RoleTypePrototype.FallbackColor;
var roleSymbol = rolePrototype?.Symbol ?? RoleTypePrototype.FallbackSymbol;
@@ -213,7 +213,7 @@ internal sealed class AdminNameOverlay : Overlay
{
color = Color.GreenYellow;
color.A = alpha;
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? color : colorDisconnected);
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.StartingJob, uiScale, playerInfo.Connected ? color : colorDisconnected);
currentOffset += lineoffset;
}
@@ -241,7 +241,7 @@ internal sealed class AdminNameOverlay : Overlay
color = roleColor;
symbol = IsFiltered(playerInfo.RoleProto) ? symbol : string.Empty;
text = IsFiltered(playerInfo.RoleProto)
? roleName.ToUpper()
? Loc.GetString(roleName).ToUpper()
: string.Empty;
break;
case AdminOverlayAntagFormat.Subtype:

View File

@@ -7,7 +7,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Serilog;
namespace Content.Client.Cargo.UI;

View File

@@ -1,16 +0,0 @@
using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class HyposprayStatusControlSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<HyposprayComponent>(ent => new HyposprayStatusControl(ent, _solutionContainers));
}
}

View File

@@ -0,0 +1,20 @@
using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Prototypes;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class InjectorStatusControlSystem : EntitySystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainers = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<InjectorComponent>(injector => new InjectorStatusControl(injector, _solutionContainers, _prototypeManager));
}
}

View File

@@ -1,16 +0,0 @@
using Content.Client.Chemistry.UI;
using Content.Client.Items;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Client.Chemistry.EntitySystems;
public sealed class InjectorSystem : SharedInjectorSystem
{
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<InjectorComponent>(ent => new InjectorStatusControl(ent, SolutionContainer));
}
}

View File

@@ -48,6 +48,10 @@ namespace Content.Client.Chemistry.UI
(uint) _window.BottleDosage.Value, _window.LabelLine));
_window.BufferSortButton.OnPressed += _ => SendMessage(
new ChemMasterSortingTypeCycleMessage());
_window.OutputBufferDraw.OnPressed += _ => SendMessage(
new ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource.Internal));
_window.OutputBeakerDraw.OnPressed += _ => SendMessage(
new ChemMasterOutputDrawSourceMessage(ChemMasterDrawSource.External));
for (uint i = 0; i < _window.PillTypeButtons.Length; i++)
{

View File

@@ -79,10 +79,13 @@
<!-- Packaging -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'chem-master-window-packaging-text'}" />
<Label Text="{Loc 'chem-master-output-source'}" StyleClasses="LabelSecondaryColor" Margin="0 0 5 0"/>
<Button MinSize="80 0" Name="OutputBufferDraw" Access="Public" Text="{Loc 'chem-master-output-buffer-draw'}" ToggleMode="True" StyleClasses="OpenRight" />
<Button MinSize="80 0" Name="OutputBeakerDraw" Access="Public" Text="{Loc 'chem-master-output-beaker-draw'}" ToggleMode="True" StyleClasses="OpenLeft" />
<Control HorizontalExpand="True"/>
<Label Text="{Loc 'chem-master-window-buffer-label'}" />
<Label Name="BufferCurrentVolume" StyleClasses="LabelWeak" />
<!-- Output Draw Source -->
<Label Name="DrawSource"/>
<Label Name="BufferCurrentVolume" StyleClasses="LabelSecondaryColor" />
</BoxContainer>
<!-- Wrap the packaging info-->

View File

@@ -150,7 +150,17 @@ namespace Content.Client.Chemistry.UI
// Ensure the Panel Info is updated, including UI elements for Buffer Volume, Output Container and so on
UpdatePanelInfo(castState);
BufferCurrentVolume.Text = $" {castState.BufferCurrentVolume?.Int() ?? 0}u";
switch (castState.DrawSource)
{
case ChemMasterDrawSource.Internal:
SetBufferText(castState.BufferCurrentVolume, "chem-master-output-buffer-draw");
break;
case ChemMasterDrawSource.External:
SetBufferText(castState.InputContainerInfo?.CurrentVolume, "chem-master-output-beaker-draw");
break;
default:
throw new($"Chemmaster {castState.OutputContainerInfo} draw source is not set");
}
InputEjectButton.Disabled = castState.InputContainerInfo is null;
OutputEjectButton.Disabled = castState.OutputContainerInfo is null;
@@ -168,9 +178,14 @@ namespace Content.Client.Chemistry.UI
var holdsReagents = output?.Reagents != null;
var pillNumberMax = holdsReagents ? 0 : remainingCapacity;
var bottleAmountMax = holdsReagents ? remainingCapacity : 0;
var bufferVolume = castState.BufferCurrentVolume?.Int() ?? 0;
var outputVolume = castState.DrawSource switch
{
ChemMasterDrawSource.Internal => castState.BufferCurrentVolume?.Int() ?? 0,
ChemMasterDrawSource.External => castState.InputContainerInfo?.CurrentVolume.Int() ?? 0,
_ => 0,
};
PillDosage.Value = (int)Math.Min(bufferVolume, castState.PillDosageLimit);
PillDosage.Value = (int)Math.Min(outputVolume, castState.PillDosageLimit);
PillTypeButtons[castState.SelectedPillType].Pressed = true;
@@ -186,25 +201,35 @@ namespace Content.Client.Chemistry.UI
// Avoid division by zero
if (PillDosage.Value > 0)
{
PillNumber.Value = Math.Min(bufferVolume / PillDosage.Value, pillNumberMax);
PillNumber.Value = Math.Min(outputVolume / PillDosage.Value, pillNumberMax);
}
else
{
PillNumber.Value = 0;
}
BottleDosage.Value = Math.Min(bottleAmountMax, bufferVolume);
BottleDosage.Value = Math.Min(bottleAmountMax, outputVolume);
}
/// <summary>
/// Generate a product label based on reagents in the buffer.
/// Generate a product label based on reagents in the buffer or beaker.
/// </summary>
/// <param name="state">State data sent by the server.</param>
private string GenerateLabel(ChemMasterBoundUserInterfaceState state)
{
if (state.BufferCurrentVolume == 0)
if (
state.BufferCurrentVolume == 0 && state.DrawSource == ChemMasterDrawSource.Internal ||
state.InputContainerInfo?.CurrentVolume == 0 && state.DrawSource == ChemMasterDrawSource.External ||
state.InputContainerInfo?.Reagents == null
)
return "";
var reagent = state.BufferReagents.OrderBy(r => r.Quantity).First().Reagent;
var reagent = (state.DrawSource switch
{
ChemMasterDrawSource.Internal => state.BufferReagents,
ChemMasterDrawSource.External => state.InputContainerInfo.Reagents ?? [],
_ => throw new($"Chemmaster {state.OutputContainerInfo} draw source is not set"),
}).MinBy(r => r.Quantity)
.Reagent;
_prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto);
return proto?.LocalizedName ?? "";
}
@@ -233,6 +258,8 @@ namespace Content.Client.Chemistry.UI
_ => Loc.GetString("chem-master-window-sort-type-none")
};
OutputBufferDraw.Pressed = state.DrawSource == ChemMasterDrawSource.Internal;
OutputBeakerDraw.Pressed = state.DrawSource == ChemMasterDrawSource.External;
if (!state.BufferReagents.Any())
{
@@ -414,6 +441,12 @@ namespace Content.Client.Chemistry.UI
get => LabelLineEdit.Text;
set => LabelLineEdit.Text = value;
}
private void SetBufferText(FixedPoint2? volume, string text)
{
BufferCurrentVolume.Text = $" {volume ?? FixedPoint2.Zero}u";
DrawSource.Text = Loc.GetString(text);
}
}
public sealed class ReagentButton : Button

View File

@@ -1,58 +0,0 @@
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.Chemistry.UI;
public sealed class HyposprayStatusControl : Control
{
private readonly Entity<HyposprayComponent> _parent;
private readonly RichTextLabel _label;
private readonly SharedSolutionContainerSystem _solutionContainers;
private FixedPoint2 PrevVolume;
private FixedPoint2 PrevMaxVolume;
private bool PrevOnlyAffectsMobs;
public HyposprayStatusControl(Entity<HyposprayComponent> parent, SharedSolutionContainerSystem solutionContainers)
{
_parent = parent;
_solutionContainers = solutionContainers;
_label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
AddChild(_label);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
return;
// only updates the UI if any of the details are different than they previously were
if (PrevVolume == solution.Volume
&& PrevMaxVolume == solution.MaxVolume
&& PrevOnlyAffectsMobs == _parent.Comp.OnlyAffectsMobs)
return;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
PrevOnlyAffectsMobs = _parent.Comp.OnlyAffectsMobs;
var modeStringLocalized = Loc.GetString((_parent.Comp.OnlyAffectsMobs && _parent.Comp.CanContainerDraw) switch
{
false => "hypospray-all-mode-text",
true => "hypospray-mobs-only-mode-text",
});
_label.SetMarkup(Loc.GetString("hypospray-volume-label",
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", modeStringLocalized)));
}
}

View File

@@ -2,26 +2,32 @@ using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Chemistry.UI;
public sealed class InjectorStatusControl : Control
{
private readonly IPrototypeManager _prototypeManager;
private readonly Entity<InjectorComponent> _parent;
private readonly SharedSolutionContainerSystem _solutionContainers;
private readonly RichTextLabel _label;
private FixedPoint2 PrevVolume;
private FixedPoint2 PrevMaxVolume;
private FixedPoint2 PrevTransferAmount;
private InjectorToggleMode PrevToggleState;
private FixedPoint2 _prevVolume;
private FixedPoint2 _prevMaxVolume;
private FixedPoint2? _prevTransferAmount;
private InjectorBehavior _prevBehavior;
public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers)
public InjectorStatusControl(Entity<InjectorComponent> parent, SharedSolutionContainerSystem solutionContainers, IPrototypeManager prototypeManager)
{
_prototypeManager = prototypeManager;
_parent = parent;
_solutionContainers = solutionContainers;
_label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
@@ -32,33 +38,38 @@ public sealed class InjectorStatusControl : Control
{
base.FrameUpdate(args);
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution))
if (!_solutionContainers.TryGetSolution(_parent.Owner, _parent.Comp.SolutionName, out _, out var solution)
|| !_prototypeManager.Resolve(_parent.Comp.ActiveModeProtoId, out var activeMode))
return;
// only updates the UI if any of the details are different than they previously were
if (PrevVolume == solution.Volume
&& PrevMaxVolume == solution.MaxVolume
&& PrevTransferAmount == _parent.Comp.CurrentTransferAmount
&& PrevToggleState == _parent.Comp.ToggleState)
if (_prevVolume == solution.Volume
&& _prevMaxVolume == solution.MaxVolume
&& _prevTransferAmount == _parent.Comp.CurrentTransferAmount
&& _prevBehavior == activeMode.Behavior)
return;
PrevVolume = solution.Volume;
PrevMaxVolume = solution.MaxVolume;
PrevTransferAmount = _parent.Comp.CurrentTransferAmount;
PrevToggleState = _parent.Comp.ToggleState;
_prevVolume = solution.Volume;
_prevMaxVolume = solution.MaxVolume;
_prevTransferAmount = _parent.Comp.CurrentTransferAmount;
_prevBehavior = activeMode.Behavior;
// Update current volume and injector state
var modeStringLocalized = Loc.GetString(_parent.Comp.ToggleState switch
// Seeing transfer volume is only important for injectors that can change it.
if (activeMode.TransferAmounts.Count > 1 && _parent.Comp.CurrentTransferAmount.HasValue)
{
_label.SetMarkup(Loc.GetString("injector-volume-transfer-label",
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", Loc.GetString(activeMode.Name)),
("transferVolume", _parent.Comp.CurrentTransferAmount.Value)));
}
else
{
InjectorToggleMode.Draw => "injector-draw-text",
InjectorToggleMode.Inject => "injector-inject-text",
_ => "injector-invalid-injector-toggle-mode"
});
_label.SetMarkup(Loc.GetString("injector-volume-label",
("currentVolume", solution.Volume),
("totalVolume", solution.MaxVolume),
("modeString", modeStringLocalized),
("transferVolume", _parent.Comp.CurrentTransferAmount)));
("modeString", Loc.GetString(activeMode.Name))));
}
}
}

View File

@@ -30,7 +30,10 @@ namespace Content.Client.Construction.UI
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly SpriteSystem _spriteSystem;
private readonly ISawmill _sawmill;
private readonly IConstructionMenuView _constructionView;
private readonly EntityWhitelistSystem _whitelistSystem;
@@ -90,6 +93,7 @@ namespace Content.Client.Construction.UI
_constructionView = new ConstructionMenu();
_whitelistSystem = _entManager.System<EntityWhitelistSystem>();
_spriteSystem = _entManager.System<SpriteSystem>();
_sawmill = _logManager.GetSawmill("construction.ui");
// This is required so that if we load after the system is initialized, we can bind to it immediately
if (_systemManager.TryGetEntitySystem<ConstructionSystem>(out var constructionSystem))
@@ -284,7 +288,7 @@ namespace Content.Client.Construction.UI
if (!_constructionSystem!.TryGetRecipePrototype(recipe.ID, out var targetProtoId))
{
Logger.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
_sawmill.Error("Cannot find the target prototype in the recipe cache with the id \"{0}\" of {1}.",
recipe.ID,
nameof(ConstructionPrototype));
continue;

View File

@@ -1,26 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>12</LangVersion>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Client\</OutputPath>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Tools;DebugOpt</Configurations>
<Platforms>AnyCPU</Platforms>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="Nett" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="Pidgin" />
<PackageReference Include="Robust.Shared.AuthLib" />
</ItemGroup>
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<ItemGroup>
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Shared\Content.Corvax.Interfaces.Shared.csproj" />
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Client\Content.Corvax.Interfaces.Client.csproj" />

View File

@@ -73,7 +73,19 @@ public sealed class PuddleSystem : SharedPuddleSystem
// Maybe someday we'll have clientside prediction for entity spawning, but not today.
// Until then, these methods do nothing on the client.
/// <inheritdoc/>
public override bool TrySplashSpillAt(EntityUid uid, EntityCoordinates coordinates, Solution solution, out EntityUid puddleUid, bool sound = true, EntityUid? user = null)
public override bool TrySplashSpillAt(Entity<SpillableComponent?> entity, EntityCoordinates coordinates, out EntityUid puddleUid, out Solution solution, bool sound = true, EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
solution = new Solution();
return false;
}
public override bool TrySplashSpillAt(EntityUid entity,
EntityCoordinates coordinates,
Solution spilled,
out EntityUid puddleUid,
bool sound = true,
EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
return false;

View File

@@ -42,7 +42,7 @@ public sealed class GuidebookRichPrototypeLink : Control, IPrototypeLinkControl
public void SetMessage(FormattedMessage message)
{
_message = message;
_richTextLabel.SetMessage(_message);
_richTextLabel.SetMessage(_message, tagsAllowed: null);
}
public IPrototype? LinkedPrototype { get; set; }

View File

@@ -82,7 +82,7 @@ public sealed partial class DocumentParsingManager
}
msg.Pop();
rt.SetMessage(msg);
rt.SetMessage(msg, tagsAllowed: null);
return rt;
},
TextParser)

View File

@@ -18,7 +18,7 @@ public sealed partial class InfoSection : BoxContainer
{
TitleLabel.Text = title;
if (markup)
Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
Content.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()), tagsAllowed: null);
else
Content.SetMessage(text);
}

View File

@@ -24,7 +24,7 @@ namespace Content.Client.Info
}
public void SetInfoBlob(string markup)
{
_richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup));
_richTextLabel.SetMessage(FormattedMessage.FromMarkupOrThrow(markup), tagsAllowed: null);
}
}
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.Kitchen.EntitySystems;
using JetBrains.Annotations;
namespace Content.Client.Kitchen.EntitySystems;
[UsedImplicitly]
public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem;

View File

@@ -20,8 +20,10 @@ namespace Content.Client.Launcher
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClipboardManager _clipboard = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private LauncherConnectingGui? _control;
private ISawmill _sawmill = default!;
private Page _currentPage;
private string? _connectFailReason;
@@ -61,6 +63,8 @@ namespace Content.Client.Launcher
{
_control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg, _clipboard);
_sawmill = _logManager.GetSawmill("launcher-ui");
_userInterfaceManager.StateRoot.AddChild(_control);
_clientNetManager.ConnectFailed += OnConnectFailed;
@@ -115,12 +119,12 @@ namespace Content.Client.Launcher
}
else
{
Logger.InfoS("launcher-ui", $"Redial not possible, no Ss14Address");
_sawmill.Info($"Redial not possible, no Ss14Address");
}
}
catch (Exception ex)
{
Logger.ErrorS("launcher-ui", $"Redial exception: {ex}");
_sawmill.Error($"Redial exception: {ex}");
}
return false;
}

View File

@@ -1,6 +1,5 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Prometheus;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;

View File

@@ -23,7 +23,6 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
_window = this.CreateWindowCenteredLeft<IFFConsoleWindow>();
_window.ShowIFF += SendIFFMessage;
_window.ShowVessel += SendVesselMessage;
}
protected override void UpdateState(BoundUserInterfaceState state)
@@ -44,14 +43,6 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
});
}
private void SendVesselMessage(bool obj)
{
SendMessage(new IFFShowVesselMessage()
{
Show = obj,
});
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);

View File

@@ -9,12 +9,6 @@
<Button Name="ShowIFFOnButton" Text="{Loc 'iff-console-on'}" StyleClasses="OpenRight" />
<Button Name="ShowIFFOffButton" Text="{Loc 'iff-console-off'}" StyleClasses="OpenLeft" />
</BoxContainer>
<Label Name="ShowVesselLabel" Text="{Loc 'iff-console-show-vessel-label'}" HorizontalExpand="True" StyleClasses="highlight" />
<BoxContainer Orientation="Horizontal" MinWidth="120">
<Button Name="ShowVesselOnButton" Text="{Loc 'iff-console-on'}" StyleClasses="OpenRight" />
<Button Name="ShowVesselOffButton" Text="{Loc 'iff-console-off'}" StyleClasses="OpenLeft" />
</BoxContainer>
</GridContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -13,9 +13,7 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
IComputerWindow<IFFConsoleBoundUserInterfaceState>
{
private readonly ButtonGroup _showIFFButtonGroup = new();
private readonly ButtonGroup _showVesselButtonGroup = new();
public event Action<bool>? ShowIFF;
public event Action<bool>? ShowVessel;
public IFFConsoleWindow()
{
@@ -24,11 +22,6 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFFOnButton.Group = _showIFFButtonGroup;
ShowIFFOnButton.OnPressed += args => ShowIFFPressed(true);
ShowIFFOffButton.OnPressed += args => ShowIFFPressed(false);
ShowVesselOffButton.Group = _showVesselButtonGroup;
ShowVesselOnButton.Group = _showVesselButtonGroup;
ShowVesselOnButton.OnPressed += args => ShowVesselPressed(true);
ShowVesselOffButton.OnPressed += args => ShowVesselPressed(false);
}
private void ShowIFFPressed(bool pressed)
@@ -36,19 +29,14 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFF?.Invoke(pressed);
}
private void ShowVesselPressed(bool pressed)
{
ShowVessel?.Invoke(pressed);
}
public void UpdateState(IFFConsoleBoundUserInterfaceState state)
{
if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0)
if ((state.AllowedFlags & IFFFlags.HideLabel) != 0x0 || (state.AllowedFlags & IFFFlags.Hide) != 0x0)
{
ShowIFFOffButton.Disabled = false;
ShowIFFOnButton.Disabled = false;
if ((state.Flags & IFFFlags.HideLabel) != 0x0)
if ((state.Flags & IFFFlags.HideLabel) != 0x0 || (state.Flags & IFFFlags.Hide) != 0x0)
{
ShowIFFOffButton.Pressed = true;
}
@@ -62,25 +50,5 @@ public sealed partial class IFFConsoleWindow : FancyWindow,
ShowIFFOffButton.Disabled = true;
ShowIFFOnButton.Disabled = true;
}
if ((state.AllowedFlags & IFFFlags.Hide) != 0x0)
{
ShowVesselOffButton.Disabled = false;
ShowVesselOnButton.Disabled = false;
if ((state.Flags & IFFFlags.Hide) != 0x0)
{
ShowVesselOffButton.Pressed = true;
}
else
{
ShowVesselOnButton.Pressed = true;
}
}
else
{
ShowVesselOffButton.Disabled = true;
ShowVesselOnButton.Disabled = true;
}
}
}

View File

@@ -21,7 +21,7 @@ public sealed partial class BorgMenu : FancyWindow
[Dependency] private readonly IEntityManager _entity = default!;
private readonly NameModifierSystem _nameModifier;
private readonly PowerCellSystem _powerCell;
private readonly PredictedBatterySystem _battery;
private readonly SharedBatterySystem _battery;
public Action? BrainButtonPressed;
public Action? EjectBatteryButtonPressed;
@@ -44,7 +44,7 @@ public sealed partial class BorgMenu : FancyWindow
_nameModifier = _entity.System<NameModifierSystem>();
_powerCell = _entity.System<PowerCellSystem>();
_battery = _entity.System<PredictedBatterySystem>();
_battery = _entity.System<SharedBatterySystem>();
_maxNameLength = _cfgManager.GetCVar(CCVars.MaxNameLength);

View File

@@ -3,10 +3,10 @@
StyleClasses="PanelLight"
Margin="5 5 5 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 5 0 5">
<TextureButton Name="RemoveButton" VerticalAlignment="Top" HorizontalAlignment="Left" Scale="0.5 0.5"/>
<SpriteView Name="ModuleView" Margin="0 0 5 0"/>
<BoxContainer RectClipContent="True" HorizontalExpand="True">
<Label Name="ModuleName" HorizontalExpand="True" HorizontalAlignment="Center"/>
</BoxContainer>
<TextureButton Name="RemoveButton" VerticalAlignment="Top" HorizontalAlignment="Right" Scale="0.5 0.5"/>
</BoxContainer>
</PanelContainer>

View File

@@ -18,7 +18,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _player = default!;

View File

@@ -28,7 +28,16 @@ public class ListContainer : Control
/// Called when creating a button on the UI.
/// The provided <see cref="ListContainerButton"/> is the generated button that Controls should be parented to.
/// </summary>
public Action<ListData, ListContainerButton>? GenerateItem;
public Action<ListData, ListContainerButton>? GenerateItem
{
get => _generateItem;
set {
_generateItem = value;
// Invalidate _itemHeight so we recalculate the size of children the next
// time PopulateList() is called
_itemHeight = 0;
}
}
/// <inheritdoc cref="BaseButton.OnPressed"/>
public Action<BaseButton.ButtonEventArgs, ListData>? ItemPressed;
@@ -59,6 +68,7 @@ public class ListContainer : Control
private bool _updateChildren = false;
private bool _suppressScrollValueChanged;
private ButtonGroup? _buttonGroup;
public Action<ListData, ListContainerButton>? _generateItem;
public int ScrollSpeedY { get; set; } = 50;

View File

@@ -117,7 +117,7 @@ public partial class ChatBox : UIWidget
formatted.PushColor(color);
formatted.AddMarkupOrThrow(message);
formatted.Pop();
Contents.AddMessage(formatted);
Contents.AddMessage(formatted, tagsAllowed: null);
}
public void Focus(ChatSelectChannel? channel = null)

View File

@@ -26,6 +26,8 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
_window.OnNameChange += OnNameSelected;
_window.OnVerbChange += verb => SendMessage(new VoiceMaskChangeVerbMessage(verb));
_window.OnToggle += OnToggle;
_window.OnAccentToggle += OnAccentToggle;
_window.OnVoiceChange += voice => SendMessage(new VoiceMaskChangeVoiceMessage(voice)); // Corvax-TTS
}
@@ -34,6 +36,16 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
SendMessage(new VoiceMaskChangeNameMessage(name));
}
private void OnToggle()
{
SendMessage(new VoiceMaskToggleMessage());
}
private void OnAccentToggle()
{
SendMessage(new VoiceMaskAccentToggleMessage());
}
protected override void UpdateState(BoundUserInterfaceState state)
{
if (state is not VoiceMaskBuiState cast || _window == null)
@@ -41,6 +53,7 @@ public sealed class VoiceMaskBoundUserInterface : BoundUserInterface
return;
}
_window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide);
_window.UpdateState(cast.Name, cast.Voice, cast.Verb); // Corvax-TTS
}

View File

@@ -12,6 +12,8 @@
<Label Text="{Loc 'voice-mask-name-change-speech-style'}" />
<OptionButton Name="SpeechVerbSelector" /> <!-- Populated in LoadVerbs -->
</BoxContainer>
<Button Name="ToggleAccentButton" Text="{Loc 'voice-mask-name-change-accent-toggle'}" HorizontalExpand="True" ToggleMode="True" Margin="5"/>
<Button Name="ToggleButton" Text="{Loc 'voice-mask-name-change-toggle'}" HorizontalExpand="True" ToggleMode="True" Margin="5"/>
<!-- Corvax-TTS-Start -->
<BoxContainer Orientation="Horizontal" Margin="5" Visible="False" Name="TTSContainer">
<Label Text="{Loc 'voice-mask-voice-change-info'}" />

View File

@@ -15,6 +15,8 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
{
public Action<string>? OnNameChange;
public Action<string?>? OnVerbChange;
public Action? OnToggle;
public Action? OnAccentToggle;
public Action<string>? OnVoiceChange; // Corvax-TTS
private List<(string, string)> _verbs = new();
@@ -37,6 +39,9 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
SpeechVerbSelector.SelectId(args.Id);
};
ToggleButton.OnPressed += args => OnToggle?.Invoke();
ToggleAccentButton.OnPressed += args => OnAccentToggle?.Invoke();
// Corvax-TTS-Start
if (IoCManager.Resolve<IConfigurationManager>().GetCVar(CCCVars.TTSEnabled))
{
@@ -104,6 +109,8 @@ public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
{
NameSelector.Text = name;
_verb = verb;
ToggleButton.Pressed = active;
ToggleAccentButton.Pressed = accentHide;
for (int id = 0; id < SpeechVerbSelector.ItemCount; id++)
{

View File

@@ -1,12 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<OutputPath>..\bin\Content.IntegrationTests\</OutputPath>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<LangVersion>12</LangVersion>
<Nullable>disable</Nullable>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit3TestAdapter" />
@@ -17,12 +15,12 @@
<ProjectReference Include="..\Content.Server\Content.Server.csproj" />
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Content.Tests\Content.Tests.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.UnitTesting\Robust.UnitTesting.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\MSBuild\Robust.CompNetworkGenerator.targets" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Server.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Testing.props" />
</Project>

View File

@@ -0,0 +1,130 @@
using System.Numerics;
using Content.Server.Atmos.Monitor.Components;
using Content.Shared.Atmos;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Atmos;
/// <summary>
/// Test for determining that an AtmosMonitoringComponent/System correctly references
/// the GasMixture of the tile it is on if the tile's GasMixture ever changes.
/// </summary>
[TestOf(typeof(Atmospherics))]
public sealed class AtmosMonitoringTest : AtmosTest
{
// We can just reuse the dP test, I just want a grid.
protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
private readonly EntProtoId _airSensorProto = new("AirSensor");
private readonly EntProtoId _wallProto = new("WallSolid");
/// <summary>
/// Tests if the monitor properly nulls out its reference to the tile mixture
/// when a wall is placed on top of it, and restores the reference when the wall is removed.
/// </summary>
[Test]
public async Task NullOutTileAtmosphereGasMixture()
{
// run an atmos update to initialize everything For Real surely
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
var gridNetEnt = SEntMan.GetNetEntity(RelevantAtmos.Owner);
TargetCoords = new NetCoordinates(gridNetEnt, Vector2.Zero);
var netEnt = await Spawn(_airSensorProto);
var airSensorUid = SEntMan.GetEntity(netEnt);
Transform.TryGetGridTilePosition(airSensorUid, out var vec);
// run another one to ensure that the ref to the GasMixture was picked up
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
// should be in the middle
Assert.That(vec,
Is.EqualTo(Vector2i.Zero),
"Air sensor not in expected position on grid (0, 0)");
var atmosMonitor = SEntMan.GetComponent<AtmosMonitorComponent>(airSensorUid);
var tileMixture = SAtmos.GetTileMixture(airSensorUid);
Assert.That(tileMixture,
Is.SameAs(atmosMonitor.TileGas),
"Atmos monitor's TileGas does not match actual tile mixture after spawn.");
// ok now spawn a wall or something on top of it
var wall = await Spawn(_wallProto);
var wallUid = SEntMan.GetEntity(wall);
// ensure that atmospherics registers the change - the gas mixture should no longer exist
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
// the monitor's ref to the gas should be null now
Assert.That(atmosMonitor.TileGas,
Is.Null,
"Atmos monitor's TileGas is not null after wall placed on top. Possible dead reference.");
// the actual mixture on the tile should be null now too
var nullTileMixture = SAtmos.GetTileMixture(airSensorUid);
Assert.That(nullTileMixture, Is.Null, "Tile mixture is not null after wall placed on top.");
// ok now delete the wall
await Delete(wallUid);
// ensure that atmospherics registers the change - the gas mixture should be back
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
// gas mixture should now exist again
var newTileMixture = SAtmos.GetTileMixture(airSensorUid);
Assert.That(newTileMixture, Is.Not.Null, "Tile mixture is null after wall removed.");
// monitor's ref to the gas should be back too
Assert.That(atmosMonitor.TileGas,
Is.SameAs(newTileMixture),
"Atmos monitor's TileGas does not match actual tile mixture after wall removed.");
}
/// <summary>
/// Tests if the monitor properly updates its reference to the tile mixture
/// when the FixGridAtmos command is called.
/// </summary>
[Test]
public async Task FixGridAtmosReplaceMixtureOnTileChange()
{
// run an atmos update to initialize everything For Real surely
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
var gridNetEnt = SEntMan.GetNetEntity(RelevantAtmos.Owner);
TargetCoords = new NetCoordinates(gridNetEnt, Vector2.Zero);
var netEnt = await Spawn(_airSensorProto);
var airSensorUid = SEntMan.GetEntity(netEnt);
Transform.TryGetGridTilePosition(airSensorUid, out var vec);
// run another one to ensure that the ref to the GasMixture was picked up
SAtmos.RunProcessingFull(ProcessEnt, MapData.Grid.Owner, SAtmos.AtmosTickRate);
// should be in the middle
Assert.That(vec,
Is.EqualTo(Vector2i.Zero),
"Air sensor not in expected position on grid (0, 0)");
var atmosMonitor = SEntMan.GetComponent<AtmosMonitorComponent>(airSensorUid);
var tileMixture = SAtmos.GetTileMixture(airSensorUid);
Assert.That(tileMixture,
Is.SameAs(atmosMonitor.TileGas),
"Atmos monitor's TileGas does not match actual tile mixture after spawn.");
SAtmos.RebuildGridAtmosphere((ProcessEnt.Owner, ProcessEnt.Comp1, ProcessEnt.Comp3));
// EXTREMELY IMPORTANT: The reference to the tile mixture on the tile should be completely different.
var newTileMixture = SAtmos.GetTileMixture(airSensorUid);
Assert.That(newTileMixture,
Is.Not.SameAs(tileMixture),
"Tile mixture is the same instance after fixgridatmos was ran. It should be a new instance.");
// The monitor's ref to the tile mixture should have updated too.
Assert.That(atmosMonitor.TileGas,
Is.SameAs(newTileMixture),
"Atmos monitor's TileGas does not match actual tile mixture after fixgridatmos was ran.");
}
}

View File

@@ -20,7 +20,7 @@ public abstract class AtmosTest : InteractionTest
protected AtmosphereSystem SAtmos = default!;
protected EntityLookupSystem LookupSystem = default!;
protected Entity<GridAtmosphereComponent> RelevantAtmos = default!;
protected Entity<GridAtmosphereComponent> RelevantAtmos;
/// <summary>
/// Used in <see cref="AtmosphereSystem.RunProcessingFull"/>. Resolved during test setup.
@@ -40,14 +40,35 @@ public abstract class AtmosTest : InteractionTest
SAtmos = SEntMan.System<AtmosphereSystem>();
LookupSystem = SEntMan.System<EntityLookupSystem>();
RelevantAtmos = (MapData.Grid, SEntMan.GetComponent<GridAtmosphereComponent>(MapData.Grid));
SEntMan.TryGetComponent<GridAtmosphereComponent>(MapData.Grid, out var gridAtmosComp);
SEntMan.TryGetComponent<GasTileOverlayComponent>(MapData.Grid, out var overlayComp);
SEntMan.TryGetComponent<MapGridComponent>(MapData.Grid, out var mapGridComp);
var xform = SEntMan.GetComponent<TransformComponent>(MapData.Grid);
using (Assert.EnterMultipleScope())
{
Assert.That(gridAtmosComp,
Is.Not.Null,
"Loaded map doesn't have a GridAtmosphereComponent on its grid. " +
"Did you forget to override TestMapPath with a proper atmospherics testing map?");
Assert.That(overlayComp,
Is.Not.Null,
"Loaded map doesn't have a GasTileOverlayComponent on its grid. " +
"Did you forget to override TestMapPath with a proper atmospherics testing map?");
Assert.That(mapGridComp,
Is.Not.Null,
"Loaded map doesn't have a MapGridComponent on its grid. " +
"Did you forget to override TestMapPath with a proper atmospherics testing map?");
}
RelevantAtmos = (MapData.Grid, gridAtmosComp);
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));
gridAtmosComp,
overlayComp,
mapGridComp,
xform);
}
/// <summary>

View File

@@ -1,15 +1,11 @@
using System.Linq;
using System.Numerics;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Tests.Atmos;
@@ -20,7 +16,7 @@ namespace Content.IntegrationTests.Tests.Atmos;
/// </summary>
[TestFixture]
[TestOf(typeof(DeltaPressureSystem))]
public sealed class DeltaPressureTest
public sealed class DeltaPressureTest : AtmosTest
{
#region Prototypes
@@ -92,7 +88,7 @@ public sealed class DeltaPressureTest
#endregion
private readonly ResPath _testMap = new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
protected override ResPath? TestMapPath => new("Maps/Test/Atmospherics/DeltaPressure/deltapressuretest.yml");
// TODO ATMOS TESTS
// - Check for directional windows (partial airtight ents) properly computing pressure differences
@@ -111,40 +107,15 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingListAutoJoinTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
await Server.WaitAssertion(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt, Vector2.Zero));
var dpEnt = new Entity<DeltaPressureComponent>(uid, SEntMan.GetComponent<DeltaPressureComponent>(uid));
grid = gridSet.First();
Assert.That(SAtmos.IsDeltaPressureEntityInList(RelevantAtmos, dpEnt), "Entity was not in processing list when it should have automatically joined!");
SEntMan.DeleteEntity(uid);
Assert.That(!SAtmos.IsDeltaPressureEntityInList(RelevantAtmos, dpEnt), "Entity was still in processing list after deletion!");
});
await server.WaitAssertion(() =>
{
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have automatically joined!");
entMan.DeleteEntity(uid);
Assert.That(!atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was still in processing list after deletion!");
});
await pair.CleanReturnAsync();
}
/// <summary>
@@ -154,45 +125,27 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingDeltaStandbyTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
await Server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, SEntMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
await Server.WaitPost(() =>
{
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
var indices = Transform.GetGridOrMapTilePosition(dpEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
tile = RelevantAtmos.Comp.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
@@ -202,19 +155,17 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
await Server.WaitRunTicks(30);
// Entity should exist, if it took one tick of damage then it should be instantly destroyed.
await server.WaitAssertion(() =>
await Server.WaitAssertion(() =>
{
Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
Assert.That(!SEntMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
await Server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
@@ -224,47 +175,43 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingDeltaDamageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
// Load our test map in and assert that it exists.
await server.WaitPost(() =>
await Server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
SAtmos.SetAtmosphereSimulation(ProcessEnt, false);
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
/*
RUNNING REGULAR TICKS USING WaitRunTicks AND GUESSING AS TO HOW MANY ATMOS SIMULATION TICKS ARE HAPPENING
WILL CAUSE A RACE CONDITION THAT IS A PAIN IN THE ASS TO DEBUG
AN ENTITY MAY BE REMOVED AND ADDED BETWEEN A SUBTICK. IF LINDA PROCESSING IS ENABLED IT MIGHT CAUSE
AN EQUALIZATION TO PUT AIR IN OTHER TILES IN THE SMALL WINDOW WHERE THE TILE IS NOT AIRTIGHT
WHICH WILL THROW OFF DELTAS
*/
await Server.WaitPost(() =>
{
SAtmos.RunProcessingFull(ProcessEnt,ProcessEnt.Owner, SAtmos.AtmosTickRate);
});
await Server.WaitPost(() =>
{
// Need to spawn an entity each run to ensure it works for all directions.
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTest", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, SEntMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
var indices = Transform.GetGridOrMapTilePosition(dpEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
tile = gridAtmosComp.Tiles[offsetIndices];
var tile = ProcessEnt.Comp1.Tiles[offsetIndices];
Assert.That(tile.Air, Is.Not.Null, $"Tile at {offsetIndices} should have air!");
@@ -274,19 +221,29 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
// Entity should exist, if it took one tick of damage then it should be instantly destroyed.
await server.WaitAssertion(() =>
// get jiggy with it! hit that dance white boy!
await Server.WaitPost(() =>
{
Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
tile.Air!.Clear();
SAtmos.RunProcessingFull(ProcessEnt,ProcessEnt.Owner, SAtmos.AtmosTickRate);
});
await server.WaitRunTicks(30);
}
// need to run some ticks as deleted entities are queued for removal
// and not removed instantly
await Server.WaitRunTicks(30);
await pair.CleanReturnAsync();
// Entity shouldn't exist, if it took one tick of damage then it should be instantly destroyed.
await Server.WaitAssertion(() =>
{
Assert.That(SEntMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold pressure from {direction} side!");
// Double whammy: in case any unintended gas leak occured due to a race condition,
// clear out all the tiles.
foreach (var mix in SAtmos.GetAllMixtures(ProcessEnt))
{
mix.Clear();
}
});
}
}
/// <summary>
@@ -296,39 +253,23 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingAbsoluteStandbyTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
await server.WaitPost(() =>
await Server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, SEntMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
await Server.WaitPost(() =>
{
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
var indices = Transform.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = SEntMan.GetComponent<GridAtmosphereComponent>(ProcessEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
@@ -340,18 +281,16 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
await Server.WaitRunTicks(30);
await server.WaitAssertion(() =>
await Server.WaitAssertion(() =>
{
Assert.That(!entMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
Assert.That(!SEntMan.Deleted(dpEnt), $"{dpEnt} should still exist after experiencing non-threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
await Server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
}
/// <summary>
@@ -361,41 +300,21 @@ public sealed class DeltaPressureTest
[Test]
public async Task ProcessingAbsoluteDamageTest()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var mapLoader = entMan.System<MapLoaderSystem>();
var atmosphereSystem = entMan.System<AtmosphereSystem>();
var transformSystem = entMan.System<SharedTransformSystem>();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = true };
Entity<MapGridComponent> grid = default;
Entity<DeltaPressureComponent> dpEnt = default;
TileAtmosphere tile = null!;
AtmosDirection direction = default;
await server.WaitPost(() =>
{
#pragma warning disable NUnit2045
Assert.That(mapLoader.TryLoadMap(_testMap, out _, out var gridSet, deserializationOptions),
$"Failed to load map {_testMap}.");
Assert.That(gridSet, Is.Not.Null, "There were no grids loaded from the map!");
#pragma warning restore NUnit2045
grid = gridSet.First();
});
for (var i = 0; i < Atmospherics.Directions; i++)
{
await server.WaitPost(() =>
await Server.WaitPost(() =>
{
// Spawn fresh entity each iteration to verify all directions work
var uid = entMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(grid.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, entMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(atmosphereSystem.IsDeltaPressureEntityInList(grid.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTestAbsolute", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, SEntMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var indices = transformSystem.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = entMan.GetComponent<GridAtmosphereComponent>(grid);
var indices = Transform.GetGridOrMapTilePosition(dpEnt);
var gridAtmosComp = SEntMan.GetComponent<GridAtmosphereComponent>(ProcessEnt);
direction = (AtmosDirection)(1 << i);
var offsetIndices = indices.Offset(direction);
@@ -408,17 +327,15 @@ public sealed class DeltaPressureTest
tile.Air!.AdjustMoles(Gas.Nitrogen, moles);
});
await server.WaitRunTicks(30);
await Server.WaitRunTicks(30);
await server.WaitAssertion(() =>
await Server.WaitAssertion(() =>
{
Assert.That(entMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
Assert.That(SEntMan.Deleted(dpEnt), $"{dpEnt} still exists after experiencing threshold absolute pressure from {direction} side!");
tile.Air!.Clear();
});
await server.WaitRunTicks(30);
}
await pair.CleanReturnAsync();
await Server.WaitRunTicks(30);
}
}
}

View File

@@ -31,6 +31,8 @@ namespace Content.IntegrationTests.Tests.Buckle
- type: Hands
- type: ComplexInteraction
- type: InputMover
- type: Physics
bodyType: KinematicController
- type: Body
prototype: Human
- type: StandingState

View File

@@ -54,7 +54,7 @@ public sealed class ObjectiveCommandsTest
});
Assert.That(mindEnt, Is.Not.Null);
var mindComp = mindEnt.Value.Comp;
var mindComp = mindEnt!.Value.Comp;
Assert.That(mindComp.Objectives, Is.Empty, "Dummy player started with objectives.");
await pair.WaitCommand($"addobjective {playerSession.Name} {ObjectiveProtoId}");

View File

@@ -164,7 +164,7 @@ namespace Content.IntegrationTests.Tests.Damageable
var damageToDeal = FixedPoint2.New(types.Count * 5);
DamageSpecifier damage = new(group3, damageToDeal);
sDamageableSystem.TryChangeDamage(uid, damage, true);
sDamageableSystem.ChangeDamage(uid, damage, true);
Assert.Multiple(() =>
{
@@ -178,7 +178,7 @@ namespace Content.IntegrationTests.Tests.Damageable
});
// Heal
sDamageableSystem.TryChangeDamage(uid, -damage);
sDamageableSystem.ChangeDamage(uid, -damage);
Assert.Multiple(() =>
{
@@ -197,7 +197,7 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(types, Has.Count.EqualTo(3));
damage = new DamageSpecifier(group3, 14);
sDamageableSystem.TryChangeDamage(uid, damage, true);
sDamageableSystem.ChangeDamage(uid, damage, true);
Assert.Multiple(() =>
{
@@ -209,7 +209,7 @@ namespace Content.IntegrationTests.Tests.Damageable
});
// Heal
sDamageableSystem.TryChangeDamage(uid, -damage);
sDamageableSystem.ChangeDamage(uid, -damage);
Assert.Multiple(() =>
{
@@ -225,7 +225,7 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
});
damage = new DamageSpecifier(group1, FixedPoint2.New(10)) + new DamageSpecifier(type2b, FixedPoint2.New(10));
sDamageableSystem.TryChangeDamage(uid, damage, true);
sDamageableSystem.ChangeDamage(uid, damage, true);
Assert.Multiple(() =>
{
@@ -245,9 +245,9 @@ namespace Content.IntegrationTests.Tests.Damageable
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
// Test 'wasted' healing
sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3a, 5));
sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(type3b, 7));
sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -11));
sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(type3a, 5));
sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(type3b, 7));
sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, -11));
Assert.Multiple(() =>
{
@@ -257,11 +257,11 @@ namespace Content.IntegrationTests.Tests.Damageable
});
// Test Over-Healing
sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, FixedPoint2.New(-100)));
sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, FixedPoint2.New(-100)));
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
// Test that if no health change occurred, returns false
sDamageableSystem.TryChangeDamage(uid, new DamageSpecifier(group3, -100));
sDamageableSystem.ChangeDamage(uid, new DamageSpecifier(group3, -100));
Assert.That(sDamageableComponent.TotalDamage, Is.EqualTo(FixedPoint2.Zero));
});
await pair.CleanReturnAsync();

View File

@@ -112,7 +112,7 @@ public sealed partial class MindTests
Assert.That(entMan.EntityExists(attachedEntity), Is.True);
Assert.That(attachedEntity, Is.Not.EqualTo(playerEnt));
Assert.That(entMan.HasComponent<GhostComponent>(attachedEntity));
var transform = entMan.GetComponent<TransformComponent>(attachedEntity.Value);
var transform = entMan.GetComponent<TransformComponent>(attachedEntity!.Value);
Assert.That(transform.MapID, Is.Not.EqualTo(MapId.Nullspace));
Assert.That(transform.MapID, Is.Not.EqualTo(testMap.MapId));
#pragma warning restore NUnit2045
@@ -175,7 +175,7 @@ public sealed partial class MindTests
Assert.That(player.AttachedEntity, Is.Not.Null);
Assert.That(entMan.EntityExists(player.AttachedEntity));
#pragma warning restore NUnit2045
var originalEntity = player.AttachedEntity.Value;
var originalEntity = player.AttachedEntity!.Value;
EntityUid ghost = default!;
await server.WaitAssertion(() =>
@@ -248,7 +248,7 @@ public sealed partial class MindTests
var mindId = player.ContentData()?.Mind;
Assert.That(mindId, Is.Not.Null);
var mind = entMan.GetComponent<MindComponent>(mindId.Value);
var mind = entMan.GetComponent<MindComponent>(mindId!.Value);
Assert.That(mind.VisitingEntity, Is.Null);
await pair.CleanReturnAsync();

View File

@@ -0,0 +1,186 @@
using Content.Shared.Coordinates;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
namespace Content.IntegrationTests.Tests.Power;
[TestFixture]
public sealed class PowerStateTest
{
[TestPrototypes]
private const string Prototypes = @"
- type: entity
id: PowerStateApcReceiverDummy
components:
- type: ApcPowerReceiver
- type: ExtensionCableReceiver
- type: Transform
anchored: true
- type: PowerState
isWorking: false
idlePowerDraw: 10
workingPowerDraw: 50
";
/// <summary>
/// Asserts that switching from idle to working updates the power receiver load to the working draw.
/// </summary>
[Test]
public async Task SetWorkingState_IdleToWorking_UpdatesLoad()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var mapManager = server.ResolveDependency<IMapManager>();
var entManager = server.ResolveDependency<IEntityManager>();
var mapSys = entManager.System<SharedMapSystem>();
await server.WaitAssertion(() =>
{
mapSys.CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
var receiver = entManager.GetComponent<Server.Power.Components.ApcPowerReceiverComponent>(ent);
var powerState = entManager.GetComponent<PowerStateComponent>(ent);
Assert.Multiple(() =>
{
Assert.That(powerState.IsWorking, Is.False);
Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
});
var system = entManager.System<PowerStateSystem>();
system.SetWorkingState((ent, powerState), true);
Assert.Multiple(() =>
{
Assert.That(powerState.IsWorking, Is.True);
Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
});
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that switching from working to idle updates the power receiver load to the idle draw.
/// </summary>
[Test]
public async Task SetWorkingState_WorkingToIdle_UpdatesLoad()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var mapManager = server.ResolveDependency<IMapManager>();
var entManager = server.ResolveDependency<IEntityManager>();
var mapSys = entManager.System<SharedMapSystem>();
await server.WaitAssertion(() =>
{
mapSys.CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
var receiver = entManager.GetComponent<Server.Power.Components.ApcPowerReceiverComponent>(ent);
var powerState = entManager.GetComponent<PowerStateComponent>(ent);
var system = entManager.System<PowerStateSystem>();
Entity<PowerStateComponent> newEnt = (ent, powerState);
Assert.Multiple(() =>
{
Assert.That(powerState.IsWorking, Is.False);
Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
});
system.SetWorkingState(newEnt, true);
Assert.Multiple(() =>
{
Assert.That(powerState.IsWorking, Is.True);
Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
});
system.SetWorkingState(newEnt, false);
Assert.Multiple(() =>
{
Assert.That(powerState.IsWorking, Is.False);
Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
});
});
await pair.CleanReturnAsync();
}
/// <summary>
/// Asserts that setting the working state to the current state does not change the power receiver load.
/// </summary>
[Test]
public async Task SetWorkingState_AlreadyInState_NoChange()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var mapManager = server.ResolveDependency<IMapManager>();
var entManager = server.ResolveDependency<IEntityManager>();
var mapSys = entManager.System<SharedMapSystem>();
await server.WaitAssertion(() =>
{
mapSys.CreateMap(out var mapId);
var grid = mapManager.CreateGridEntity(mapId);
mapSys.SetTile(grid, Vector2i.Zero, new Tile(1));
var ent = entManager.SpawnEntity("PowerStateApcReceiverDummy", grid.Owner.ToCoordinates());
var receiver = entManager.GetComponent<Server.Power.Components.ApcPowerReceiverComponent>(ent);
var powerState = entManager.GetComponent<PowerStateComponent>(ent);
var system = entManager.System<PowerStateSystem>();
Entity<PowerStateComponent> valueTuple = (ent, powerState);
Assert.Multiple(() =>
{
Assert.That(powerState.IsWorking, Is.False);
Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
});
system.SetWorkingState(valueTuple, false);
Assert.Multiple(() =>
{
Assert.That(powerState.IsWorking, Is.False);
Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
});
system.SetWorkingState(valueTuple, true);
Assert.Multiple(() =>
{
Assert.That(powerState.IsWorking, Is.True);
Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
});
system.SetWorkingState(valueTuple, true);
Assert.Multiple(() =>
{
Assert.That(powerState.IsWorking, Is.True);
Assert.That(receiver.Load, Is.EqualTo(powerState.WorkingPowerDraw).Within(0.01f));
});
});
await pair.CleanReturnAsync();
}
}

View File

@@ -54,6 +54,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
netsync: false
- type: BatteryCharger
- type: entity
@@ -68,6 +69,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
netsync: false
- type: BatteryDischarger
- type: entity
@@ -85,6 +87,7 @@ namespace Content.IntegrationTests.Tests.Power
nodeGroupID: HVPower
- type: PowerNetworkBattery
- type: Battery
netsync: false
- type: BatteryDischarger
node: output
- type: BatteryCharger
@@ -110,6 +113,7 @@ namespace Content.IntegrationTests.Tests.Power
maxSupply: 1000
supplyRampTolerance: 1000
- type: Battery
netsync: false
maxCharge: 1000
startingCharge: 1000
- type: Transform
@@ -119,6 +123,7 @@ namespace Content.IntegrationTests.Tests.Power
id: ApcDummy
components:
- type: Battery
netsync: false
maxCharge: 10000
startingCharge: 10000
- type: PowerNetworkBattery
@@ -380,6 +385,8 @@ namespace Content.IntegrationTests.Tests.Power
const float startingCharge = 100_000;
PowerNetworkBatteryComponent netBattery = default!;
EntityUid generatorEnt = default!;
EntityUid consumerEnt = default!;
BatteryComponent battery = default!;
PowerConsumerComponent consumer = default!;
@@ -395,8 +402,8 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
}
var generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(generatorEnt);
battery = entityManager.GetComponent<BatteryComponent>(generatorEnt);
@@ -441,7 +448,8 @@ namespace Content.IntegrationTests.Tests.Power
// Trivial integral to calculate expected power spent.
const double spentExpected = (200 + 100) / 2.0 * 0.25;
Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
var currentCharge = batterySys.GetCharge((generatorEnt, battery));
Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
});
});
@@ -460,7 +468,8 @@ namespace Content.IntegrationTests.Tests.Power
// Trivial integral to calculate expected power spent.
const double spentExpected = (400 + 100) / 2.0 * 0.75 + 400 * 0.25;
Assert.That(battery.CurrentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
var currentCharge = batterySys.GetCharge((generatorEnt, battery));
Assert.That(currentCharge, Is.EqualTo(startingCharge - spentExpected).Within(tickDev));
});
});
@@ -576,6 +585,8 @@ namespace Content.IntegrationTests.Tests.Power
var entityManager = server.ResolveDependency<IEntityManager>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid generatorEnt = default!;
EntityUid batteryEnt = default!;
PowerSupplierComponent supplier = default!;
BatteryComponent battery = default!;
@@ -591,8 +602,8 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
}
var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
var batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
supplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
var netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(batteryEnt);
@@ -615,7 +626,8 @@ namespace Content.IntegrationTests.Tests.Power
{
// half a second @ 500 W = 250
// 50% efficiency, so 125 J stored total.
Assert.That(battery.CurrentCharge, Is.EqualTo(125).Within(0.1));
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
Assert.That(currentCharge, Is.EqualTo(125).Within(0.1));
Assert.That(supplier.CurrentSupply, Is.EqualTo(500).Within(0.1));
});
});
@@ -633,6 +645,9 @@ namespace Content.IntegrationTests.Tests.Power
var gameTiming = server.ResolveDependency<IGameTiming>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid batteryEnt = default!;
EntityUid supplyEnt = default!;
EntityUid consumerEnt = default!;
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
@@ -653,9 +668,9 @@ namespace Content.IntegrationTests.Tests.Power
var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
@@ -694,7 +709,8 @@ namespace Content.IntegrationTests.Tests.Power
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(200).Within(0.1));
const int expectedSpent = 200;
Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
});
});
@@ -711,6 +727,9 @@ namespace Content.IntegrationTests.Tests.Power
var gameTiming = server.ResolveDependency<IGameTiming>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid batteryEnt = default!;
EntityUid supplyEnt = default!;
EntityUid consumerEnt = default!;
PowerConsumerComponent consumer = default!;
PowerSupplierComponent supplier = default!;
PowerNetworkBatteryComponent netBattery = default!;
@@ -731,9 +750,9 @@ namespace Content.IntegrationTests.Tests.Power
var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
@@ -772,7 +791,8 @@ namespace Content.IntegrationTests.Tests.Power
Assert.That(netBattery.SupplyRampPosition, Is.EqualTo(400).Within(0.1));
const int expectedSpent = 400;
Assert.That(battery.CurrentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
var currentCharge = batterySys.GetCharge((batteryEnt, battery));
Assert.That(currentCharge, Is.EqualTo(battery.MaxCharge - expectedSpent).Within(tickDev));
});
});
@@ -1223,6 +1243,9 @@ namespace Content.IntegrationTests.Tests.Power
var entityManager = server.ResolveDependency<IEntityManager>();
var batterySys = entityManager.System<BatterySystem>();
var mapSys = entityManager.System<SharedMapSystem>();
EntityUid generatorEnt = default!;
EntityUid substationEnt = default!;
EntityUid apcEnt = default!;
PowerNetworkBatteryComponent substationNetBattery = default!;
BatteryComponent apcBattery = default!;
@@ -1242,9 +1265,9 @@ namespace Content.IntegrationTests.Tests.Power
entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 1));
entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 2));
var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
var substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
var apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
var generatorSupplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
substationNetBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(substationEnt);
@@ -1262,8 +1285,9 @@ namespace Content.IntegrationTests.Tests.Power
{
Assert.Multiple(() =>
{
var currentCharge = batterySys.GetCharge((apcEnt, apcBattery));
Assert.That(substationNetBattery.CurrentSupply, Is.GreaterThan(0)); //substation should be providing power
Assert.That(apcBattery.CurrentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
Assert.That(currentCharge, Is.GreaterThan(0)); //apc battery should have gained charge
});
});

View File

@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.NodeGroups;
using Content.Server.Power.Pow3r;
using Content.Shared.Maps;
@@ -48,6 +49,7 @@ public sealed class StationPowerTests
var entMan = server.EntMan;
var protoMan = server.ProtoMan;
var ticker = entMan.System<GameTicker>();
var batterySys = entMan.System<BatterySystem>();
// Load the map
await server.WaitAssertion(() =>
@@ -71,7 +73,8 @@ public sealed class StationPowerTests
if (node.NodeGroup is not IBasePowerNet group)
continue;
networks.TryGetValue(group.NetworkNode, out var charge);
networks[group.NetworkNode] = charge + battery.CurrentCharge;
var currentCharge = batterySys.GetCharge((uid, battery));
networks[group.NetworkNode] = charge + currentCharge;
}
var totalStartingCharge = networks.MaxBy(n => n.Value).Value;

View File

@@ -14,7 +14,7 @@ public sealed class AdminTest : ToolshedTest
var toolMan = Server.ResolveDependency<ToolshedManager>();
var admin = Server.ResolveDependency<IAdminManager>();
var ignored = new HashSet<Assembly>()
{typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
{typeof(LocTest).Assembly};
await Server.WaitAssertion(() =>
{

View File

@@ -19,7 +19,7 @@ public sealed class LocTest : ToolshedTest
var locStrings = new HashSet<string>();
var ignored = new HashSet<Assembly>()
{typeof(LocTest).Assembly, typeof(Robust.UnitTesting.Shared.Toolshed.LocTest).Assembly};
{typeof(LocTest).Assembly};
await Server.WaitAssertion(() =>
{

View File

@@ -3,19 +3,27 @@
<OutputType>Exe</OutputType>
<OutputPath>..\bin\Content.MapRenderer\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<Nullable>enable</Nullable>
<ServerGarbageCollection>true</ServerGarbageCollection>
<IsTestingPlatformApplication>false</IsTestingPlatformApplication>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<ProjectReference Include="..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.UnitTesting\Robust.UnitTesting.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NUnit" />
<PackageReference Include="Moq" />
<PackageReference Include="SixLabors.ImageSharp" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Server.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Testing.props" />
</Project>

View File

@@ -2,13 +2,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ServerGarbageCollection>True</ServerGarbageCollection>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RobustToolbox\Robust.Packaging\Robust.Packaging.csproj" />
</ItemGroup>
<Import Project="../MSBuild/Content.props" />
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Packaging.props" />
</Project>

View File

@@ -0,0 +1,80 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Content.Packaging;
/// <summary>
/// Helper class for working with <c>.deps.json</c> files.
/// </summary>
public sealed class DepsHandler
{
public readonly Dictionary<string, LibraryInfo> Libraries = new();
public DepsHandler(DepsData data)
{
if (data.Targets.Count != 1)
throw new Exception("Expected exactly one target");
var target = data.Targets.Single().Value;
foreach (var (libNameAndVersion, libInfo) in target)
{
var split = libNameAndVersion.Split('/', 2);
Libraries.Add(split[0], libInfo);
}
}
public static DepsHandler Load(string depsFile)
{
using var f = File.OpenRead(depsFile);
var depsData = JsonSerializer.Deserialize<DepsData>(f) ?? throw new InvalidOperationException("Deps are null!");
return new DepsHandler(depsData);
}
public HashSet<string> RecursiveGetLibrariesFrom(string start)
{
var found = new HashSet<string>();
RecursiveAddLibraries(start, found);
return found;
}
private void RecursiveAddLibraries(string start, HashSet<string> set)
{
if (!set.Add(start))
return;
var lib = Libraries[start];
if (lib.Dependencies == null)
return;
foreach (var dep in lib.Dependencies.Keys)
{
RecursiveAddLibraries(dep, set);
}
}
public sealed class DepsData
{
[JsonInclude, JsonPropertyName("targets")]
public required Dictionary<string, Dictionary<string, LibraryInfo>> Targets;
}
public sealed class LibraryInfo
{
[JsonInclude, JsonPropertyName("dependencies")]
public Dictionary<string, string>? Dependencies;
[JsonInclude, JsonPropertyName("runtime")]
public Dictionary<string, object>? Runtime;
// Paths are like lib/netstandard2.0/JetBrains.Annotations.dll
public IEnumerable<string> GetDllNames()
{
return Runtime == null ? [] : Runtime.Keys.Select(p => p.Split('/')[^1]);
}
}
}

View File

@@ -37,29 +37,9 @@ public static class ServerPackaging
.Select(o => o.Rid)
.ToList();
private static readonly List<string> ServerContentAssemblies = new()
{
// Corvax-Secrets-Start
"Content.Corvax.Interfaces.Shared",
"Content.Corvax.Interfaces.Server",
// Corvax-Secrets-End
"Content.Server.Database",
"Content.Server",
"Content.Shared",
"Content.Shared.Database",
};
private static readonly List<string> ServerExtraAssemblies = new()
{
// Python script had Npgsql. though we want Npgsql.dll as well soooo
"Npgsql",
"Microsoft",
"NetCord",
};
private static readonly List<string> ServerNotExtraAssemblies = new()
{
"Microsoft.CodeAnalysis",
"JetBrains.Annotations",
};
private static readonly HashSet<string> BinSkipFolders = new()
@@ -208,27 +188,13 @@ public static class ServerPackaging
var inputPassCore = graph.InputCore;
var inputPassResources = graph.InputResources;
var contentAssemblies = new List<string>(ServerContentAssemblies);
// Corvax-Secrets-Start
if (UseSecrets)
contentAssemblies.AddRange(new[] { "Content.Corvax.Shared", "Content.Corvax.Server" });
// Corvax-Secrets-End
// Additional assemblies that need to be copied such as EFCore.
var sourcePath = Path.Combine(contentDir, "bin", "Content.Server");
// Should this be an asset pass?
// For future archaeologists I just want audio rework to work and need the audio pass so
// just porting this as is from python.
foreach (var fullPath in Directory.EnumerateFiles(sourcePath, "*.*", SearchOption.AllDirectories))
{
var fileName = Path.GetFileNameWithoutExtension(fullPath);
var deps = DepsHandler.Load(Path.Combine(sourcePath, "Content.Server.deps.json"));
if (!ServerNotExtraAssemblies.Any(o => fileName.StartsWith(o)) && ServerExtraAssemblies.Any(o => fileName.StartsWith(o)))
{
contentAssemblies.Add(fileName);
}
}
var contentAssemblies = GetContentAssemblyNamesToCopy(deps);
await RobustSharedPackaging.DoResourceCopy(
Path.Combine("RobustToolbox", "bin", "Server",
@@ -260,5 +226,21 @@ public static class ServerPackaging
inputPassResources.InjectFinished();
}
// This returns both content assemblies (e.g. Content.Server.dll) and dependencies (e.g. Npgsql)
private static IEnumerable<string> GetContentAssemblyNamesToCopy(DepsHandler deps)
{
var depsContent = deps.RecursiveGetLibrariesFrom("Content.Server").SelectMany(GetLibraryNames);
var depsRobust = deps.RecursiveGetLibrariesFrom("Robust.Server").SelectMany(GetLibraryNames);
var depsContentExclusive = depsContent.Except(depsRobust).ToHashSet();
// Remove .dll suffix and apply filtering.
var names = depsContentExclusive.Select(p => p[..^4]).Where(p => !ServerNotExtraAssemblies.Any(p.StartsWith));
return names;
IEnumerable<string> GetLibraryNames(string library) => deps.Libraries[library].GetDllNames();
}
private readonly record struct PlatformReg(string Rid, string TargetOs, bool BuildByDefault);
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>

View File

@@ -1,26 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>12</LangVersion>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Replay\</OutputPath>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="Nett" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\XamlIL.targets" />
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\Imports\Client.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
</Project>

View File

@@ -1,16 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>12</LangVersion>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Server.Database\</OutputPath>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<Nullable>enable</Nullable>
<NoWarn>RA0003</NoWarn>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -31,6 +31,7 @@ public sealed partial class AdminVerbSystem
private static readonly EntProtoId DefaultChangelingRule = "Changeling";
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
private static readonly EntProtoId DefaultWizardRule = "Wizard";
private static readonly EntProtoId DefaultNinjaRule = "NinjaSpawn";
private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
// All antag verbs have names so invokeverb works.
@@ -207,6 +208,21 @@ public sealed partial class AdminVerbSystem
};
args.Verbs.Add(wizard);
var ninjaName = Loc.GetString("admin-verb-text-make-space-ninja");
Verb ninja = new()
{
Text = ninjaName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Objects/Weapons/Melee/energykatana.rsi"), "icon"),
Act = () =>
{
_antag.ForceMakeAntag<NinjaRoleComponent>(targetPlayer, DefaultNinjaRule);
},
Impact = LogImpact.High,
Message = string.Join(": ", ninjaName, Loc.GetString("admin-verb-make-space-ninja")),
};
args.Verbs.Add(ninja);
if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
args.Verbs.Add(paradox);
}

View File

@@ -4,7 +4,6 @@ using System.Numerics;
using Content.Server.Cargo.Components;
using Content.Server.Doors.Systems;
using Content.Server.Hands.Systems;
using Content.Server.Power.EntitySystems;
using Content.Server.Stack;
using Content.Server.Station.Systems;
using Content.Server.Weapons.Ranged.Systems;
@@ -51,8 +50,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly AdminTestArenaSystem _adminTestArenaSystem = default!;
[Dependency] private readonly StationJobsSystem _stationJobsSystem = default!;
[Dependency] private readonly JointSystem _jointSystem = default!;
[Dependency] private readonly BatterySystem _batterySystem = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
[Dependency] private readonly SharedBatterySystem _batterySystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly GunSystem _gun = default!;
@@ -161,57 +159,6 @@ public sealed partial class AdminVerbSystem
args.Verbs.Add(makeVulnerable);
}
if (TryComp<PredictedBatteryComponent>(args.Target, out var pBattery))
{
Verb refillBattery = new()
{
Text = Loc.GetString("admin-verbs-refill-battery"),
Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill_battery.png")),
Act = () =>
{
_predictedBatterySystem.SetCharge((args.Target, pBattery), pBattery.MaxCharge);
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-refill-battery-description"),
Priority = (int)TricksVerbPriorities.RefillBattery,
};
args.Verbs.Add(refillBattery);
Verb drainBattery = new()
{
Text = Loc.GetString("admin-verbs-drain-battery"),
Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/drain_battery.png")),
Act = () =>
{
_predictedBatterySystem.SetCharge((args.Target, pBattery), 0);
},
Impact = LogImpact.Medium,
Priority = (int)TricksVerbPriorities.DrainBattery,
};
args.Verbs.Add(drainBattery);
Verb infiniteBattery = new()
{
Text = Loc.GetString("admin-verbs-infinite-battery"),
Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/infinite_battery.png")),
Act = () =>
{
var recharger = EnsureComp<PredictedBatterySelfRechargerComponent>(args.Target);
recharger.AutoRechargeRate = pBattery.MaxCharge; // Instant refill.
recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
Dirty(args.Target, recharger);
_predictedBatterySystem.RefreshChargeRate((args.Target, pBattery));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
Priority = (int)TricksVerbPriorities.InfiniteBattery,
};
args.Verbs.Add(infiniteBattery);
}
if (TryComp<BatteryComponent>(args.Target, out var battery))
{
Verb refillBattery = new()
@@ -254,6 +201,8 @@ public sealed partial class AdminVerbSystem
var recharger = EnsureComp<BatterySelfRechargerComponent>(args.Target);
recharger.AutoRechargeRate = battery.MaxCharge; // Instant refill.
recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
Dirty(args.Target, recharger);
_batterySystem.RefreshChargeRate((args.Target, battery));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),

View File

@@ -100,7 +100,7 @@ public sealed partial class AnomalySystem
// no air-blocked areas.
if (_atmosphere.IsTileSpace(grid, xform.MapUid, tile) ||
_atmosphere.IsTileAirBlocked(grid, tile, mapGridComp: gridComp))
_atmosphere.IsTileAirBlockedCached(grid, tile))
{
continue;
}

View File

@@ -306,6 +306,8 @@ public partial class AtmosphereSystem
/// <param name="directions">The directions to check for air-blockage.</param>
/// <param name="mapGridComp">Optional map grid component associated with the grid.</param>
/// <returns>True if the tile is air-blocked in the specified directions, false otherwise.</returns>
/// <remarks>This rebuilds airtight data on-the-fly. You should only use this if you've just
/// invalidated airtight data, and you cannot wait one atmostick to revalidate it.</remarks>
[PublicAPI]
public bool IsTileAirBlocked(EntityUid gridUid,
Vector2i tile,
@@ -315,11 +317,35 @@ public partial class AtmosphereSystem
if (!Resolve(gridUid, ref mapGridComp, false))
return false;
// TODO ATMOS: This reconstructs the data instead of getting the cached version. Might want to include a method to get the cached version later.
var data = GetAirtightData(gridUid, mapGridComp, tile);
return data.BlockedDirections.IsFlagSet(directions);
}
/// <summary>
/// Checks if a tile on a grid is air-blocked in the specified directions, using cached data.
/// </summary>
/// <param name="grid">The grid to check.</param>
/// <param name="tile">The tile on the grid to check.</param>
/// <param name="directions">The directions to check for air-blockage.</param>
/// <returns>True if the tile is air-blocked in the specified directions, false otherwise.</returns>
/// <remarks>Returns data that is currently cached by Atmospherics.
/// You should always use this method over <see cref="IsTileAirBlocked"/> as it's more performant.
/// If you need to get up-to-date data because you've just invalidated airtight data,
/// use <see cref="IsTileAirBlocked"/>.</remarks>
[PublicAPI]
public bool IsTileAirBlockedCached(Entity<GridAtmosphereComponent?> grid,
Vector2i tile,
AtmosDirection directions = AtmosDirection.All)
{
if (!_atmosQuery.Resolve(grid, ref grid.Comp, false))
return false;
if (!grid.Comp.Tiles.TryGetValue(tile, out var atmosTile))
return false;
return atmosTile.AirtightData.BlockedDirections.IsFlagSet(directions);
}
/// <summary>
/// Checks if a tile on a grid or map is space as defined by a tile's definition of space.
/// Some tiles can hold back space and others cannot - for example, plating can hold

View File

@@ -19,7 +19,9 @@ public sealed partial class AtmosphereSystem
// Fix Grid Atmos command.
_consoleHost.RegisterCommand("fixgridatmos",
"Makes every tile on a grid have a roundstart gas mix.",
"fixgridatmos <grid Ids>", FixGridAtmosCommand, FixGridAtmosCommandCompletions);
"fixgridatmos <grid Ids>",
FixGridAtmosCommand,
FixGridAtmosCommandCompletions);
}
private void ShutdownCommands()
@@ -36,9 +38,41 @@ public sealed partial class AtmosphereSystem
return;
}
foreach (var arg in args)
{
if (!NetEntity.TryParse(arg, out var netEntity) || !TryGetEntity(netEntity, out var euid))
{
shell.WriteError($"Failed to parse euid '{arg}'.");
return;
}
if (!TryComp(euid, out MapGridComponent? gridComp))
{
shell.WriteError($"Euid '{euid}' does not exist or is not a grid.");
return;
}
if (!TryComp(euid, out GridAtmosphereComponent? gridAtmosphere))
{
shell.WriteError($"Grid \"{euid}\" has no atmosphere component, try addatmos.");
continue;
}
RebuildGridAtmosphere((euid.Value, gridAtmosphere, gridComp));
}
}
/// <summary>
/// Rebuilds all <see cref="TileAtmosphere"/>s on a grid to have roundstart gas mixes.
/// </summary>
/// <remarks>Please be responsible with this method. Used only by tests and fixgridatmos.</remarks>
public void RebuildGridAtmosphere(Entity<GridAtmosphereComponent, MapGridComponent> ent)
{
var mixtures = new GasMixture[9];
for (var i = 0; i < mixtures.Length; i++)
{
mixtures[i] = new GasMixture(Atmospherics.CellVolume) { Temperature = Atmospherics.T20C };
}
// 0: Air
mixtures[0].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard);
@@ -72,34 +106,15 @@ public sealed partial class AtmosphereSystem
mixtures[8].AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesGasMiner);
mixtures[8].AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesGasMiner);
foreach (var arg in args)
{
if (!NetEntity.TryParse(arg, out var netEntity) || !TryGetEntity(netEntity, out var euid))
{
shell.WriteError($"Failed to parse euid '{arg}'.");
return;
}
if (!TryComp(euid, out MapGridComponent? gridComp))
{
shell.WriteError($"Euid '{euid}' does not exist or is not a grid.");
return;
}
if (!TryComp(euid, out GridAtmosphereComponent? gridAtmosphere))
{
shell.WriteError($"Grid \"{euid}\" has no atmosphere component, try addatmos.");
continue;
}
// Force Invalidate & update air on all tiles
Entity<GridAtmosphereComponent, GasTileOverlayComponent, MapGridComponent, TransformComponent> grid =
new(euid.Value, gridAtmosphere, Comp<GasTileOverlayComponent>(euid.Value), gridComp, Transform(euid.Value));
new(ent.Owner, ent.Comp1, Comp<GasTileOverlayComponent>(ent), ent.Comp2, Transform(ent));
RebuildGridTiles(grid);
var query = GetEntityQuery<AtmosFixMarkerComponent>();
foreach (var (indices, tile) in gridAtmosphere.Tiles.ToArray())
foreach (var (indices, tile) in ent.Comp1.Tiles.ToArray())
{
if (tile.Air is not {Immutable: false} air)
continue;
@@ -118,7 +133,6 @@ public sealed partial class AtmosphereSystem
air.Temperature = mixture.Temperature;
}
}
}
/// <summary>
/// Clears & re-creates all references to <see cref="TileAtmosphere"/>s stored on a grid.

View File

@@ -3,6 +3,7 @@ using System.Runtime.CompilerServices;
using Content.Server.Atmos.Reactions;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Reactions;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using DependencyAttribute = Robust.Shared.IoC.DependencyAttribute;
@@ -477,6 +478,26 @@ namespace Content.Server.Atmos.EntitySystems
return reaction;
}
/// <summary>
/// Adds an array of moles to a <see cref="GasMixture"/>.
/// Guards against negative moles by clamping to zero.
/// </summary>
/// <param name="mixture">The <see cref="GasMixture"/> to add moles to.</param>
/// <param name="molsToAdd">The <see cref="ReadOnlySpan{T}"/> of moles to add.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the length of the <see cref="ReadOnlySpan{T}"/>
/// is not the same as the length of the <see cref="GasMixture"/> gas array.</exception>
[PublicAPI]
public static void AddMolsToMixture(GasMixture mixture, ReadOnlySpan<float> molsToAdd)
{
// Span length should be as long as the length of the gas array.
// Technically this is a redundant check because NumericsHelpers will do the same thing,
// but eh.
ArgumentOutOfRangeException.ThrowIfNotEqual(mixture.Moles.Length, molsToAdd.Length, nameof(mixture.Moles.Length));
NumericsHelpers.Add(mixture.Moles, molsToAdd);
NumericsHelpers.Max(mixture.Moles, 0f);
}
public enum GasCompareResult
{
NoExchange = -2,

View File

@@ -265,6 +265,7 @@ namespace Content.Server.Atmos.EntitySystems
tile.ArchivedCycle = 0;
tile.LastShare = 0f;
tile.Hotspot = new Hotspot();
NotifyDeviceTileChanged((ent.Owner, ent.Comp1, ent.Comp3), tile.GridIndices);
return;
}
@@ -275,6 +276,10 @@ namespace Content.Server.Atmos.EntitySystems
if (data.FixVacuum)
GridFixTileVacuum(tile);
// Since we assigned the tile a new GasMixture we need to tell any devices
// on this tile that the reference has changed.
NotifyDeviceTileChanged((ent.Owner, ent.Comp1, ent.Comp3), tile.GridIndices);
}
private void QueueRunTiles(

View File

@@ -1,10 +1,8 @@
using System.Runtime.CompilerServices;
using Content.Server.Atmos.Components;
using Content.Server.Maps;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Maps;
using Robust.Shared.Map;
using Content.Shared.Atmos.Piping.Components;
using Robust.Shared.Map.Components;
namespace Content.Server.Atmos.EntitySystems;
@@ -176,4 +174,21 @@ public partial class AtmosphereSystem
_tile.PryTile(tileRef);
}
/// <summary>
/// Notifies all subscribing entities on a particular tile that the tile has changed.
/// Atmos devices may store references to tiles, so this is used to properly resync devices
/// after a significant atmos change on that tile, for example a tile getting a new <see cref="GasMixture"/>.
/// </summary>
/// <param name="ent">The grid atmosphere entity.</param>
/// <param name="tile">The tile to check for devices on.</param>
private void NotifyDeviceTileChanged(Entity<GridAtmosphereComponent, MapGridComponent> ent, Vector2i tile)
{
var inTile = _mapSystem.GetAnchoredEntities(ent.Owner, ent.Comp2, tile);
var ev = new AtmosDeviceTileChangedEvent();
foreach (var uid in inTile)
{
RaiseLocalEvent(uid, ref ev);
}
}
}

View File

@@ -64,8 +64,8 @@ public sealed partial class AtmosphereSystem : SharedAtmosphereSystem
InitializeGridAtmosphere();
InitializeMap();
_mapAtmosQuery = GetEntityQuery<MapAtmosphereComponent>();
_atmosQuery = GetEntityQuery<GridAtmosphereComponent>();
_mapAtmosQuery = GetEntityQuery<MapAtmosphereComponent>();
_airtightQuery = GetEntityQuery<AirtightComponent>();
_firelockQuery = GetEntityQuery<FirelockComponent>();

View File

@@ -43,7 +43,7 @@ public sealed class HeatExchangerSystem : EntitySystem
// make sure that the tile the device is on isn't blocked by a wall or something similar.
if (args.Grid is {} grid
&& _transform.TryGetGridTilePosition(uid, out var tile)
&& _atmosphereSystem.IsTileAirBlocked(grid, tile))
&& _atmosphereSystem.IsTileAirBlockedCached(grid, tile))
{
return;
}

View File

@@ -57,6 +57,13 @@ public sealed class AtmosMonitorSystem : EntitySystem
SubscribeLocalEvent<AtmosMonitorComponent, DeviceNetworkPacketEvent>(OnPacketRecv);
SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceDisabledEvent>(OnAtmosDeviceLeaveAtmosphere);
SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceEnabledEvent>(OnAtmosDeviceEnterAtmosphere);
SubscribeLocalEvent<AtmosMonitorComponent, AtmosDeviceTileChangedEvent>(OnAtmosDeviceTileChangedEvent);
}
private void OnAtmosDeviceTileChangedEvent(Entity<AtmosMonitorComponent> ent, ref AtmosDeviceTileChangedEvent args)
{
if (!ent.Comp.MonitorsPipeNet)
ent.Comp.TileGas = _atmosphereSystem.GetContainingMixture(ent.Owner, true);
}
private void OnAtmosDeviceLeaveAtmosphere(EntityUid uid, AtmosMonitorComponent atmosMonitor, ref AtmosDeviceDisabledEvent args)

View File

@@ -119,9 +119,9 @@ public sealed partial class CargoSystem
label.Id = bounty.Id;
label.AssociatedStationId = stationId;
var msg = new FormattedMessage();
msg.AddText(Loc.GetString("bounty-manifest-header", ("id", bounty.Id)));
msg.AddMarkupOrThrow(Loc.GetString("bounty-manifest-header", ("id", bounty.Id)));
msg.PushNewline();
msg.AddText(Loc.GetString("bounty-manifest-list-start"));
msg.AddMarkupOrThrow(Loc.GetString("bounty-manifest-list-start"));
msg.PushNewline();
foreach (var entry in prototype.Entries)
{

View File

@@ -77,11 +77,11 @@ public sealed class NanoTaskCartridgeSystem : SharedNanoTaskCartridgeSystem
printed.Task = item;
var msg = new FormattedMessage();
msg.AddText(Loc.GetString("nano-task-printed-description", ("description", item.Description)));
msg.AddMarkupOrThrow(Loc.GetString("nano-task-printed-description", ("description", FormattedMessage.EscapeText(item.Description))));
msg.PushNewline();
msg.AddText(Loc.GetString("nano-task-printed-requester", ("requester", item.TaskIsFor)));
msg.AddMarkupOrThrow(Loc.GetString("nano-task-printed-requester", ("requester", FormattedMessage.EscapeText(item.TaskIsFor))));
msg.PushNewline();
msg.AddText(item.Priority switch {
msg.AddMarkupOrThrow(item.Priority switch {
NanoTaskPriority.High => Loc.GetString("nano-task-printed-high-priority"),
NanoTaskPriority.Medium => Loc.GetString("nano-task-printed-medium-priority"),
NanoTaskPriority.Low => Loc.GetString("nano-task-printed-low-priority"),

View File

@@ -46,8 +46,11 @@ internal sealed partial class ChatManager : IChatManager
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly DiscordChatLink _discordLink = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISharedSponsorsManager? _sponsorsManager; // Corvax-Sponsors
private ISawmill _sawmill = default!;
/// <summary>
/// The maximum length a player-sent message can be sent
/// </summary>
@@ -67,6 +70,8 @@ internal sealed partial class ChatManager : IChatManager
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
_sawmill = _logManager.GetSawmill("SERVER");
RegisterRateLimits();
}
@@ -114,7 +119,7 @@ internal sealed partial class ChatManager : IChatManager
{
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride);
Logger.InfoS("SERVER", message);
_sawmill.Info(message);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}");
}

View File

@@ -745,7 +745,7 @@ public sealed partial class ChatSystem : SharedChatSystem
public string TransformSpeech(EntityUid sender, string message)
{
var ev = new TransformSpeechEvent(sender, message);
RaiseLocalEvent(ev);
RaiseLocalEvent(sender, ev, true);
return ev.Message;
}

View File

@@ -26,5 +26,11 @@ namespace Content.Server.Chemistry.Components
[DataField("clickSound"), ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
/// <summary>
/// Which source the chem master should draw from when making pills/bottles.
/// </summary>
[DataField]
public ChemMasterDrawSource DrawSource = ChemMasterDrawSource.Internal;
}
}

View File

@@ -57,6 +57,7 @@ namespace Content.Server.Chemistry.EntitySystems
SubscribeLocalEvent<ChemMasterComponent, ChemMasterReagentAmountButtonMessage>(OnReagentButtonMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterCreatePillsMessage>(OnCreatePillsMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterOutputToBottleMessage>(OnOutputToBottleMessage);
SubscribeLocalEvent<ChemMasterComponent, ChemMasterOutputDrawSourceMessage>(OnSetDrawSourceMessage);
}
private void SubscribeUpdateUiState<T>(Entity<ChemMasterComponent> ent, ref T ev)
@@ -77,7 +78,7 @@ namespace Content.Server.Chemistry.EntitySystems
var state = new ChemMasterBoundUserInterfaceState(
chemMaster.Mode, chemMaster.SortingType, BuildInputContainerInfo(inputContainer), BuildOutputContainerInfo(outputContainer),
bufferReagents, bufferCurrentVolume, chemMaster.PillType, chemMaster.PillDosageLimit, updateLabel);
bufferReagents, bufferCurrentVolume, chemMaster.PillType, chemMaster.PillDosageLimit, updateLabel, chemMaster.DrawSource);
_userInterfaceSystem.SetUiState(owner, ChemMasterUiKey.Key, state);
}
@@ -135,6 +136,17 @@ namespace Content.Server.Chemistry.EntitySystems
ClickSound(chemMaster);
}
private void OnSetDrawSourceMessage(Entity<ChemMasterComponent> chemMaster, ref ChemMasterOutputDrawSourceMessage message)
{
//Ensure draw source is valid, either from the internal buffer or the inserted beaker
if (!Enum.IsDefined(message.DrawSource))
return;
chemMaster.Comp.DrawSource = message.DrawSource;
UpdateUiState(chemMaster);
ClickSound(chemMaster);
}
private void TransferReagents(Entity<ChemMasterComponent> chemMaster, ReagentId id, FixedPoint2 amount, bool fromBuffer)
{
var container = _itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.InputSlotName);
@@ -208,9 +220,9 @@ namespace Content.Server.Chemistry.EntitySystems
return;
var needed = message.Dosage * message.Number;
if (!WithdrawFromBuffer(chemMaster, needed, user, out var withdrawal))
return;
if (!WithdrawFromSource(chemMaster, needed, user, out var withdrawal))
return;
_labelSystem.Label(container, message.Label);
for (var i = 0; i < message.Number; i++)
@@ -219,7 +231,10 @@ namespace Content.Server.Chemistry.EntitySystems
_storageSystem.Insert(container, item, out _, user: user, storage);
_labelSystem.Label(item, message.Label);
_solutionContainerSystem.EnsureSolutionEntity(item, SharedChemMaster.PillSolutionName,out var itemSolution ,message.Dosage);
_solutionContainerSystem.EnsureSolutionEntity(item,
SharedChemMaster.PillSolutionName,
out var itemSolution,
message.Dosage);
if (!itemSolution.HasValue)
return;
@@ -256,7 +271,7 @@ namespace Content.Server.Chemistry.EntitySystems
if (message.Label.Length > SharedChemMaster.LabelMaxLength)
return;
if (!WithdrawFromBuffer(chemMaster, message.Dosage, user, out var withdrawal))
if (!WithdrawFromSource(chemMaster, message.Dosage, user, out var withdrawal))
return;
_labelSystem.Label(container, message.Label);
@@ -270,34 +285,77 @@ namespace Content.Server.Chemistry.EntitySystems
ClickSound(chemMaster);
}
private bool WithdrawFromBuffer(
private bool WithdrawFromSource(
Entity<ChemMasterComponent> chemMaster,
FixedPoint2 neededVolume, EntityUid? user,
FixedPoint2 neededVolume,
EntityUid? user,
[NotNullWhen(returnValue: true)] out Solution? outputSolution)
{
outputSolution = null;
if (!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out var solution))
Solution? solution;
Entity<SolutionComponent>? soln = null;
switch (chemMaster.Comp.DrawSource)
{
case ChemMasterDrawSource.Internal:
if (!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out solution))
return false;
}
if (solution.Volume == 0)
{
if (user.HasValue)
_popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-empty-text"), user.Value);
if (user is { } uid)
_popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-empty-text"), uid);
return false;
}
if (neededVolume > solution.Volume)
{
if (user is { } uid)
_popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-low-text"), uid);
return false;
}
// ReSharper disable once InvertIf
if (neededVolume > solution.Volume)
break;
case ChemMasterDrawSource.External:
if (_itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.InputSlotName) is not {} container)
{
if (user.HasValue)
_popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-low-text"), user.Value);
_popupSystem.PopupCursor(Loc.GetString("chem-master-window-no-beaker-text"), user.Value);
return false;
}
if (!_solutionContainerSystem.TryGetFitsInDispenser(container, out soln, out solution))
return false;
if (solution.Volume == 0)
{
if (user is { } uid)
_popupSystem.PopupCursor(Loc.GetString("chem-master-window-beaker-empty-text"), uid);
return false;
}
if (neededVolume > solution.Volume)
{
if (user is { } uid)
_popupSystem.PopupCursor(Loc.GetString("chem-master-window-beaker-low-text"), uid);
return false;
}
break;
default:
return false;
}
outputSolution = solution.SplitSolution(neededVolume);
if (soln.HasValue)
_solutionContainerSystem.UpdateChemicals(soln.Value);
return true;
}

View File

@@ -1,6 +0,0 @@
using Content.Shared.Chemistry.EntitySystems;
namespace Content.Server.Chemistry.EntitySystems;
public sealed class InjectorSystem : SharedInjectorSystem;

View File

@@ -10,11 +10,11 @@ public sealed partial class CodewordFactionPrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; } = default!;
public string ID { get; private set; } = default!;
/// <summary>
/// The generator to use for this faction.
/// </summary>
[DataField(required:true)]
public ProtoId<CodewordGeneratorPrototype> Generator { get; } = default!;
public ProtoId<CodewordGeneratorPrototype> Generator { get; private set; } = default!;
}

View File

@@ -11,13 +11,13 @@ public sealed partial class CodewordGeneratorPrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; } = default!;
public string ID { get; private set; } = default!;
/// <summary>
/// List of datasets to use for word generation. All values will be concatenated into one list and then randomly chosen from
/// </summary>
[DataField]
public List<ProtoId<LocalizedDatasetPrototype>> Words { get; } =
public List<ProtoId<LocalizedDatasetPrototype>> Words { get; private set; } =
[
"Adjectives",
"Verbs",

View File

@@ -48,7 +48,7 @@ public sealed partial class BuildMech : IGraphAction
var cell = container.ContainedEntities[0];
if (!entityManager.TryGetComponent<PredictedBatteryComponent>(cell, out var batteryComponent))
if (!entityManager.TryGetComponent<BatteryComponent>(cell, out var batteryComponent))
{
Logger.Warning($"Mech construct entity {uid} had an invalid entity in container \"{Container}\"! Aborting build mech action.");
return;

View File

@@ -1,32 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
<TargetFramework>$(TargetFramework)</TargetFramework>
<LangVersion>12</LangVersion>
<IsPackable>false</IsPackable>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>..\bin\Content.Server\</OutputPath>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<OutputType Condition="'$(FullRelease)' != 'True'">Exe</OutputType>
<NoWarn>1998</NoWarn>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="NetCord" />
<PackageReference Include="prometheus-net" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Content.Packaging\Content.Packaging.csproj" />
<ProjectReference Include="..\Content.Server.Database\Content.Server.Database.csproj" />
<ProjectReference Include="..\Content.Shared.Database\Content.Shared.Database.csproj" />
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
<ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
<ProjectReference Include="..\Corvax\Content.Corvax.Interfaces.Server\Content.Corvax.Interfaces.Server.csproj" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
<Import Project="..\RobustToolbox\Imports\Server.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<Import Project="..\RobustToolbox\Imports\Packaging.props" />
</Project>

View File

@@ -1,42 +1,38 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Fluids.Components;
using Content.Shared.Chemistry.EntitySystems;
using JetBrains.Annotations;
namespace Content.Server.Destructible.Thresholds.Behaviors
{
namespace Content.Server.Destructible.Thresholds.Behaviors;
[UsedImplicitly]
[DataDefinition]
public sealed partial class SpillBehavior : IThresholdBehavior
{
/// <summary>
/// Optional fallback solution name if SpillableComponent is not present.
/// </summary>
[DataField]
public string? Solution;
/// <summary>
/// If there is a SpillableComponent on EntityUidowner use it to create a puddle/smear.
/// Or whatever solution is specified in the behavior itself.
/// If none are available do nothing.
/// When triggered, spills the entity's solution onto the ground.
/// Will first try to use the solution from a SpillableComponent if present,
/// otherwise falls back to the solution specified in the behavior's data fields.
/// The solution is properly drained/split before spilling to prevent double-spilling with other behaviors.
/// </summary>
/// <param name="owner">Entity on which behavior is executed</param>
/// <param name="system">system calling the behavior</param>
/// <param name="cause"></param>
/// <param name="owner">Entity whose solution will be spilled</param>
/// <param name="system">System calling this behavior</param>
/// <param name="cause">Optional entity that caused this behavior to trigger</param>
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
{
var solutionContainerSystem = system.EntityManager.System<SharedSolutionContainerSystem>();
var spillableSystem = system.EntityManager.System<PuddleSystem>();
var puddleSystem = system.EntityManager.System<PuddleSystem>();
var solutionContainer = system.EntityManager.System<SharedSolutionContainerSystem>();
var coordinates = system.EntityManager.GetComponent<TransformComponent>(owner).Coordinates;
if (system.EntityManager.TryGetComponent(owner, out SpillableComponent? spillableComponent) &&
solutionContainerSystem.TryGetSolution(owner, spillableComponent.SolutionName, out _, out var compSolution))
{
spillableSystem.TrySplashSpillAt(owner, coordinates, compSolution, out _, false, user: cause);
}
else if (Solution != null &&
solutionContainerSystem.TryGetSolution(owner, Solution, out _, out var behaviorSolution))
{
spillableSystem.TrySplashSpillAt(owner, coordinates, behaviorSolution, out _, user: cause);
}
}
// Spill the solution that was drained/split
if (solutionContainer.TryGetSolution(owner, Solution, out _, out var solution))
puddleSystem.TrySplashSpillAt(owner, coordinates, solution, out _, false, cause);
else
puddleSystem.TrySplashSpillAt(owner, coordinates, out _, out _, false, cause);
}
}

View File

@@ -33,8 +33,9 @@ public sealed partial class PlantMutateChemicalsEntityEffectSystem : EntityEffec
}
else
{
seedChemQuantity.Min = FixedPoint2.Epsilon;
seedChemQuantity.Max = FixedPoint2.Zero + amount;
//Set the minimum to a fifth of the quantity to give some level of bad luck protection
seedChemQuantity.Min = FixedPoint2.Clamp(pick.Quantity / 5f, FixedPoint2.Epsilon, 1f);
seedChemQuantity.Max = seedChemQuantity.Min + amount;
seedChemQuantity.Inherent = false;
}
var potencyDivisor = 100f / seedChemQuantity.Max;

View File

@@ -29,23 +29,14 @@ public sealed partial class PuddleSystem
private void SpillOnLand(Entity<SpillableComponent> entity, ref LandEvent args)
{
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
if (!entity.Comp.SpillWhenThrown || Openable.IsClosed(entity.Owner))
return;
if (Openable.IsClosed(entity.Owner))
return;
if (!entity.Comp.SpillWhenThrown)
return;
if (args.User != null)
if (TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, out _, out var solution) && args.User != null)
{
AdminLogger.Add(LogType.Landed,
$"{ToPrettyString(entity.Owner):entity} spilled a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} on landing");
}
var drainedSolution = _solutionContainerSystem.Drain(entity.Owner, soln.Value, solution.Volume);
TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, drainedSolution, out _);
}
private void OnDoAfter(Entity<SpillableComponent> entity, ref SpillDoAfterEvent args)

View File

@@ -369,7 +369,40 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// TODO: This can be predicted once https://github.com/space-wizards/RobustToolbox/pull/5849 is merged
/// <inheritdoc/>
public override bool TrySplashSpillAt(EntityUid uid,
public override bool TrySplashSpillAt(Entity<SpillableComponent?> entity,
EntityCoordinates coordinates,
out EntityUid puddleUid,
out Solution spilled,
bool sound = true,
EntityUid? user = null)
{
puddleUid = EntityUid.Invalid;
spilled = new Solution();
if (!Resolve(entity, ref entity.Comp))
return false;
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var solution))
return false;
spilled = solution.Value.Comp.Solution;
return TrySplashSpillAt(entity, coordinates, solution.Value, out puddleUid, sound, user);
}
private bool TrySplashSpillAt(EntityUid entity,
EntityCoordinates coordinates,
Entity<SolutionComponent> solution,
out EntityUid puddleUid,
bool sound = true,
EntityUid? user = null)
{
var result = TrySplashSpillAt(entity, coordinates, solution.Comp.Solution, out puddleUid, sound, user);
_solutionContainerSystem.UpdateChemicals(solution);
return result;
}
public override bool TrySplashSpillAt(EntityUid entity,
EntityCoordinates coordinates,
Solution solution,
out EntityUid puddleUid,
@@ -381,6 +414,7 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
if (solution.Volume == 0)
return false;
var spilled = solution.SplitSolution(solution.Volume);
var targets = new List<EntityUid>();
var reactive = new HashSet<Entity<ReactiveComponent>>();
_lookup.GetEntitiesInRange(coordinates, 1.0f, reactive);
@@ -392,26 +426,28 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
var owner = ent.Owner;
// between 5 and 30%
var splitAmount = solution.Volume * _random.NextFloat(0.05f, 0.30f);
var splitSolution = solution.SplitSolution(splitAmount);
var splitAmount = spilled.Volume * _random.NextFloat(0.05f, 0.30f);
var splitSolution = spilled.SplitSolution(splitAmount);
if (user != null)
{
AdminLogger.Add(LogType.Landed,
$"{ToPrettyString(user.Value):user} threw {ToPrettyString(uid):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} onto {ToPrettyString(owner):target}");
$"{ToPrettyString(user.Value):user} threw {ToPrettyString(entity):entity} which splashed a solution {SharedSolutionContainerSystem.ToPrettyString(spilled):solution} onto {ToPrettyString(owner):target}");
}
targets.Add(owner);
Reactive.DoEntityReaction(owner, splitSolution, ReactionMethod.Touch);
Popups.PopupEntity(
Loc.GetString("spill-land-spilled-on-other", ("spillable", uid),
("target", Identity.Entity(owner, EntityManager))), owner, PopupType.SmallCaution);
Popups.PopupEntity(Loc.GetString("spill-land-spilled-on-other",
("spillable", entity),
("target", Identity.Entity(owner, EntityManager))),
owner,
PopupType.SmallCaution);
}
_color.RaiseEffect(solution.GetColor(_prototypeManager), targets,
Filter.Pvs(uid, entityManager: EntityManager));
_color.RaiseEffect(spilled.GetColor(_prototypeManager), targets,
Filter.Pvs(entity, entityManager: EntityManager));
return TrySpillAt(coordinates, solution, out puddleUid, sound);
return TrySpillAt(coordinates, spilled, out puddleUid, sound);
}
/// <inheritdoc/>

View File

@@ -17,6 +17,9 @@ namespace Content.Server.GameTicking.Commands
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private readonly ISawmill _sawmill;
public string Command => "joingame";
public string Description => "";
@@ -25,7 +28,10 @@ namespace Content.Server.GameTicking.Commands
public JoinGameCommand()
{
IoCManager.InjectDependencies(this);
_sawmill = _logManager.GetSawmill("security");
}
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
@@ -46,7 +52,7 @@ namespace Content.Server.GameTicking.Commands
if (ticker.PlayerGameStatuses.TryGetValue(player.UserId, out var status) && status == PlayerGameStatus.JoinedGame)
{
Logger.InfoS("security", $"{player.Name} ({player.UserId}) attempted to latejoin while in-game.");
_sawmill.Info($"{player.Name} ({player.UserId}) attempted to latejoin while in-game.");
shell.WriteError($"{player.Name} is not in the lobby. This incident will be reported.");
return;
}

View File

@@ -2,7 +2,6 @@
using Content.Server.Administration;
using Content.Server.GameTicking.Presets;
using Content.Shared.Administration;
using Linguini.Shared.Util;
using Robust.Shared.Console;
namespace Content.Server.GameTicking.Commands
@@ -18,7 +17,7 @@ namespace Content.Server.GameTicking.Commands
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (!args.Length.InRange(1, 3))
if (args.Length is < 1 or > 3)
{
shell.WriteError(Loc.GetString("shell-need-between-arguments", ("lower", 1), ("upper", 3), ("currentAmount", args.Length)));
return;

View File

@@ -116,7 +116,7 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
tile = new Vector2i(randomX, randomY);
if (_atmosphere.IsTileSpace(targetGrid, Transform(targetGrid).MapUid, tile)
|| _atmosphere.IsTileAirBlocked(targetGrid, tile, mapGridComp: gridComp))
|| _atmosphere.IsTileAirBlockedCached(targetGrid, tile))
{
continue;
}

View File

@@ -49,15 +49,23 @@ public sealed class ToggleableGhostRoleSystem : EntitySystem
UpdateAppearance(uid, ToggleableGhostRoleStatus.Searching);
var ghostRole = EnsureComp<GhostRoleComponent>(uid);
EnsureComp<GhostTakeoverAvailableComponent>(uid);
ActivateGhostRole((uid, component));
}
public void ActivateGhostRole(Entity<ToggleableGhostRoleComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return;
var ghostRole = EnsureComp<GhostRoleComponent>(ent);
EnsureComp<GhostTakeoverAvailableComponent>(ent);
// GhostRoleComponent inherits custom settings from the ToggleableGhostRoleComponent
ghostRole.RoleName = Loc.GetString(component.RoleName);
ghostRole.RoleDescription = Loc.GetString(component.RoleDescription);
ghostRole.RoleRules = Loc.GetString(component.RoleRules);
ghostRole.JobProto = component.JobProto;
ghostRole.MindRoles = component.MindRoles;
ghostRole.RoleName = Loc.GetString(ent.Comp.RoleName);
ghostRole.RoleDescription = Loc.GetString(ent.Comp.RoleDescription);
ghostRole.RoleRules = Loc.GetString(ent.Comp.RoleRules);
ghostRole.JobProto = ent.Comp.JobProto;
ghostRole.MindRoles = ent.Comp.MindRoles;
}
private void OnExamined(EntityUid uid, ToggleableGhostRoleComponent component, ExaminedEvent args)

View File

@@ -88,7 +88,7 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
if (!_doAfter.TryStartDoAfter(args))
return;
_popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
_popup.PopupEntity(Loc.GetString("injector-component-needle-injecting-user"), target, user);
var userName = Identity.Entity(user, EntityManager);
_popup.PopupEntity(Loc.GetString("implanter-component-implanting-target", ("user", userName)), user, target, PopupType.LargeCaution);
@@ -112,7 +112,7 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
};
if (_doAfter.TryStartDoAfter(args))
_popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, user);
_popup.PopupEntity(Loc.GetString("injector-component-needle-injecting-user"), target, user);
}

View File

@@ -1,52 +0,0 @@
using Content.Shared.Kitchen;
using Content.Server.Kitchen.EntitySystems;
using Robust.Shared.Audio;
namespace Content.Server.Kitchen.Components
{
/// <summary>
/// The combo reagent grinder/juicer. The reason why grinding and juicing are seperate is simple,
/// think of grinding as a utility to break an object down into its reagents. Think of juicing as
/// converting something into its single juice form. E.g, grind an apple and get the nutriment and sugar
/// it contained, juice an apple and get "apple juice".
/// </summary>
[Access(typeof(ReagentGrinderSystem)), RegisterComponent]
public sealed partial class ReagentGrinderComponent : Component
{
[DataField]
public int StorageMaxEntities = 6;
[DataField]
public TimeSpan WorkTime = TimeSpan.FromSeconds(3.5); // Roughly matches the grind/juice sounds.
[DataField]
public float WorkTimeMultiplier = 1;
[DataField]
public SoundSpecifier ClickSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
[DataField]
public SoundSpecifier GrindSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/blender.ogg");
[DataField]
public SoundSpecifier JuiceSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/juicer.ogg");
[DataField]
public GrinderAutoMode AutoMode = GrinderAutoMode.Off;
public EntityUid? AudioStream;
}
[Access(typeof(ReagentGrinderSystem)), RegisterComponent]
public sealed partial class ActiveReagentGrinderComponent : Component
{
/// <summary>
/// Remaining time until the grinder finishes grinding/juicing.
/// </summary>
[ViewVariables]
public TimeSpan EndTime;
[ViewVariables]
public GrinderProgram Program;
}
}

View File

@@ -1,5 +1,3 @@
using Content.Server.Kitchen.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Stack;
using Content.Shared.Chemistry.EntitySystems;
@@ -20,15 +18,15 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using System.Linq;
using Content.Server.Construction.Completions;
using Content.Server.Jittering;
using Content.Shared.Jittering;
using Content.Shared.Kitchen.EntitySystems;
using Content.Shared.Power;
namespace Content.Server.Kitchen.EntitySystems
{
[UsedImplicitly]
internal sealed class ReagentGrinderSystem : EntitySystem
internal sealed class ReagentGrinderSystem : SharedReagentGrinderSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainersSystem = default!;
@@ -67,6 +65,8 @@ namespace Content.Server.Kitchen.EntitySystems
{
entity.Comp.AutoMode = (GrinderAutoMode) (((byte) entity.Comp.AutoMode + 1) % Enum.GetValues(typeof(GrinderAutoMode)).Length);
Dirty(entity);
UpdateUiState(entity);
}
@@ -90,12 +90,7 @@ namespace Content.Server.Kitchen.EntitySystems
foreach (var item in inputContainer.ContainedEntities.ToList())
{
var solution = active.Program switch
{
GrinderProgram.Grind => GetGrindSolution(item),
GrinderProgram.Juice => CompOrNull<ExtractableComponent>(item)?.JuiceSolution,
_ => null,
};
var solution = GetGrinderSolution(item, active.Program);
if (solution is null)
continue;
@@ -218,8 +213,8 @@ namespace Content.Server.Kitchen.EntitySystems
&& _solutionContainersSystem.TryGetFitsInDispenser(outputContainer.Value, out _, out containerSolution)
&& inputContainer.ContainedEntities.Count > 0)
{
canGrind = inputContainer.ContainedEntities.All(CanGrind);
canJuice = inputContainer.ContainedEntities.All(CanJuice);
canGrind = inputContainer.ContainedEntities.All(x => CanGrind(x));
canJuice = inputContainer.ContainedEntities.All(x => CanJuice(x));
}
var state = new ReagentGrinderInterfaceState(
@@ -293,10 +288,10 @@ namespace Content.Server.Kitchen.EntitySystems
SoundSpecifier? sound;
switch (program)
{
case GrinderProgram.Grind when inputContainer.ContainedEntities.All(CanGrind):
case GrinderProgram.Grind when inputContainer.ContainedEntities.All(x => CanGrind(x)):
sound = reagentGrinder.GrindSound;
break;
case GrinderProgram.Juice when inputContainer.ContainedEntities.All(CanJuice):
case GrinderProgram.Juice when inputContainer.ContainedEntities.All(x => CanJuice(x)):
sound = reagentGrinder.JuiceSound;
break;
default:
@@ -317,29 +312,5 @@ namespace Content.Server.Kitchen.EntitySystems
{
_audioSystem.PlayPvs(reagentGrinder.Comp.ClickSound, reagentGrinder.Owner, AudioParams.Default.WithVolume(-2f));
}
private Solution? GetGrindSolution(EntityUid uid)
{
if (TryComp<ExtractableComponent>(uid, out var extractable)
&& extractable.GrindableSolution is not null
&& _solutionContainersSystem.TryGetSolution(uid, extractable.GrindableSolution, out _, out var solution))
{
return solution;
}
else
return null;
}
private bool CanGrind(EntityUid uid)
{
var solutionName = CompOrNull<ExtractableComponent>(uid)?.GrindableSolution;
return solutionName is not null && _solutionContainersSystem.TryGetSolution(uid, solutionName, out _, out _);
}
private bool CanJuice(EntityUid uid)
{
return CompOrNull<ExtractableComponent>(uid)?.JuiceSolution is not null;
}
}
}

View File

@@ -153,7 +153,7 @@ public sealed class EmergencyLightSystem : SharedEmergencyLightSystem
}
else
{
_battery.SetCharge((entity.Owner, battery), battery.CurrentCharge + entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency);
_battery.ChangeCharge((entity.Owner, battery), entity.Comp.ChargingWattage * frameTime * entity.Comp.ChargingEfficiency);
if (_battery.IsFull((entity.Owner, battery)))
{
if (TryComp<ApcPowerReceiverComponent>(entity.Owner, out var receiver))

View File

@@ -24,7 +24,7 @@ namespace Content.Server.Light.EntitySystems
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPointLightSystem _lights = default!;
@@ -147,7 +147,7 @@ namespace Content.Server.Light.EntitySystems
}
// TODO: Very important: Make this charge rate based instead of instantly removing charge each update step.
// See PredictedBatteryComponent
// See BatteryComponent
public override void Update(float frameTime)
{
var toRemove = new RemQueue<Entity<HandheldLightComponent>>();

View File

@@ -1,4 +1,3 @@
using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.Mech.Components;
@@ -6,6 +5,7 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mech;
using Content.Shared.Mech.Components;
@@ -25,6 +25,7 @@ using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using System.Linq;
namespace Content.Server.Mech.Systems;
@@ -33,7 +34,7 @@ public sealed partial class MechSystem : SharedMechSystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
@@ -88,7 +89,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (TryComp<WiresPanelComponent>(uid, out var panel) && !panel.Open)
return;
if (component.BatterySlot.ContainedEntity == null && TryComp<PredictedBatteryComponent>(args.Used, out var battery))
if (component.BatterySlot.ContainedEntity == null && TryComp<BatteryComponent>(args.Used, out var battery))
{
InsertBattery(uid, args.Used, component, battery);
_actionBlocker.UpdateCanMove(uid);
@@ -109,7 +110,7 @@ public sealed partial class MechSystem : SharedMechSystem
private void OnInsertBattery(EntityUid uid, MechComponent component, EntInsertedIntoContainerMessage args)
{
if (args.Container != component.BatterySlot || !TryComp<PredictedBatteryComponent>(args.Entity, out var battery))
if (args.Container != component.BatterySlot || !TryComp<BatteryComponent>(args.Entity, out var battery))
return;
component.Energy = _battery.GetCharge((args.Entity, battery));
@@ -219,7 +220,7 @@ public sealed partial class MechSystem : SharedMechSystem
{
BreakOnMove = true,
};
_popup.PopupEntity(Loc.GetString("mech-eject-pilot-alert", ("item", uid), ("user", args.User)), uid, PopupType.Large);
_popup.PopupEntity(Loc.GetString("mech-eject-pilot-alert", ("item", uid), ("user", Identity.Entity(args.User, EntityManager))), uid, PopupType.Large);
_doAfter.TryStartDoAfter(doAfterEventArgs);
}
@@ -235,7 +236,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (_whitelistSystem.IsWhitelistFail(component.PilotWhitelist, args.User))
{
_popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), args.User);
_popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), Identity.Entity(args.User, EntityManager));
return;
}
@@ -337,7 +338,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (battery == null)
return false;
if (!TryComp<PredictedBatteryComponent>(battery, out var batteryComp))
if (!TryComp<BatteryComponent>(battery, out var batteryComp))
return false;
_battery.SetCharge((battery.Value, batteryComp), _battery.GetCharge((battery.Value, batteryComp)) + delta.Float());
@@ -353,7 +354,7 @@ public sealed partial class MechSystem : SharedMechSystem
return true;
}
public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, PredictedBatteryComponent? battery = null)
public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref component, false))
return;

View File

@@ -1,6 +1,5 @@
using Content.Server.Ninja.Events;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
@@ -17,8 +16,7 @@ namespace Content.Server.Ninja.Systems;
/// </summary>
public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
[Dependency] private readonly SharedBatterySystem _battery = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -80,20 +78,20 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
protected override bool TryDrainPower(Entity<BatteryDrainerComponent> ent, EntityUid target)
{
var (uid, comp) = ent;
if (comp.BatteryUid == null || !TryComp<PredictedBatteryComponent>(comp.BatteryUid.Value, out var battery))
if (comp.BatteryUid == null || !TryComp<BatteryComponent>(comp.BatteryUid.Value, out var battery))
return false;
if (!TryComp<BatteryComponent>(target, out var targetBattery) || !TryComp<PowerNetworkBatteryComponent>(target, out var pnb))
return false;
if (MathHelper.CloseToPercent(targetBattery.CurrentCharge, 0))
var available = _battery.GetCharge((target, targetBattery));
if (MathHelper.CloseToPercent(available, 0))
{
_popup.PopupEntity(Loc.GetString("battery-drainer-empty", ("battery", target)), uid, uid, PopupType.Medium);
return false;
}
var available = targetBattery.CurrentCharge;
var required = battery.MaxCharge - _predictedBattery.GetCharge((comp.BatteryUid.Value, battery));
var required = battery.MaxCharge - _battery.GetCharge((comp.BatteryUid.Value, battery));
// higher tier storages can charge more
var maxDrained = pnb.MaxSupply * comp.DrainTime;
var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained);
@@ -101,15 +99,13 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
return false;
var output = input * comp.DrainEfficiency;
// PowerCells use PredictedBatteryComponent
// SMES, substations and APCs use BatteryComponent
_predictedBattery.ChangeCharge((comp.BatteryUid.Value, battery), output);
_battery.ChangeCharge((comp.BatteryUid.Value, battery), output);
// TODO: create effect message or something
Spawn("EffectSparks", Transform(target).Coordinates);
_audio.PlayPvs(comp.SparkSound, target);
_popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
// repeat the doafter until battery is full
return !_predictedBattery.IsFull((comp.BatteryUid.Value, battery));
return !_battery.IsFull((comp.BatteryUid.Value, battery));
}
}

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