[DNM] v271.2.0 RT Update - .NET 10 (#3469)

Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Redrover1760 <39284090+Redrover1760@users.noreply.github.com>
Co-authored-by: Ilya246 <57039557+Ilya246@users.noreply.github.com>
This commit is contained in:
Maxim Bychkov
2026-03-22 14:04:46 +03:00
committed by GitHub
parent 7ceedb6014
commit ad37c2895a
367 changed files with 1211 additions and 2225 deletions
+1 -1
View File
@@ -348,7 +348,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,json}]
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props,json,slnx}]
indent_size = 2
[nuget.config]
+1 -1
View File
@@ -40,7 +40,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: Install dependencies
run: dotnet restore
+1 -1
View File
@@ -40,7 +40,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: Install dependencies
run: dotnet restore
+1 -1
View File
@@ -21,7 +21,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: |
+1 -1
View File
@@ -55,7 +55,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: Install dependencies
run: dotnet restore
+1 -1
View File
@@ -30,7 +30,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: Install dependencies
run: dotnet restore
- name: Build
+3
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.
+1 -1
View File
@@ -8,7 +8,7 @@ import shutil
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 = "2"
QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
+14 -9
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>
+3
View File
@@ -0,0 +1,3 @@
// Global usings for Content.Benchmarks
global using Robust.UnitTesting.Pool;
@@ -36,8 +36,6 @@ public sealed partial class ContentAudioSystem
[Dependency] private readonly IStateManager _state = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly CombatModeSystem _combatModeSystem = default!; //CLIENT ONE. WHY ARE THERE 3???
[Dependency] private readonly IPrototypeManager _protMan = default!;
[Dependency] private readonly SpaceBiomeSystem _spaceBiome = default!;
//options menu ---
private static float _volumeSliderAmbient;
@@ -8,7 +8,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;
+2 -2
View File
@@ -9,13 +9,13 @@
<!-- Main -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" Name="GeneralCategoryTexture"/>
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Emotes/vocal.png"/>
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" Name="HandsCategoryTexture"/>
</ui:RadialMenuTextureButtonWithSector>
</ui:RadialContainer>
@@ -5,10 +5,12 @@ using Content.Shared.Speech;
using Content.Shared.Whitelist;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Chat.UI;
@@ -86,6 +88,9 @@ public sealed partial class EmotesMenu : RadialMenu
continue;
AddEmoteClickAction(container);
}
GeneralCategoryTexture.Texture = spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Head/Soft/mimesoft.rsi"), "icon"));
HandsCategoryTexture.Texture = spriteSystem.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/latex.rsi"), "icon"));
}
private void AddEmoteClickAction(RadialContainer container)
+10 -12
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>nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Tools;DebugOpt</Configurations>
<Platforms>AnyCPU</Platforms>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
</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" />
</ItemGroup>
<ItemGroup>
+3 -2
View File
@@ -1,4 +1,5 @@
using Robust.Client.Graphics;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -34,7 +35,7 @@ namespace Content.Client.Cooldown
if (Progress >= 0f)
{
var hue = (5f / 18f) * lerp;
color = Color.FromHsv((hue, 0.75f, 0.75f, 0.50f));
color = Color.FromHsv(new Vector4(hue, 0.75f, 0.75f, 0.50f));
}
else
{
+2 -1
View File
@@ -1,4 +1,5 @@
using Content.Shared.Disposal;
using System.Numerics;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Unit;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
@@ -36,39 +36,37 @@ public sealed partial class DocumentParsingManager
.Assert(_tagControlParsers.ContainsKey, tag => $"unknown tag: {tag}")
.Bind(tag => _tagControlParsers[tag]);
// Frontier: comment parser
Parser<char, Unit> whitespaceAndCommentParser = SkipWhitespaces.Then(Try(String("<!--").Then(Parser<char>.Any.SkipUntil(Try(String("-->"))))).SkipMany());
var whitespaceAndCommentParser = SkipWhitespaces.Then(Try(String("<!--").Then(Parser<char>.Any.SkipUntil(Try(String("-->"))))).SkipMany());
_controlParser = OneOf(_tagParser, TryHeaderControl, TryListControl, TextControlParser)
.Before(whitespaceAndCommentParser);
// End Frontier
foreach (var typ in _reflectionManager.GetAllChildren<IDocumentTag>())
{
_tagControlParsers.Add(typ.Name, CreateTagControlParser(typ.Name, typ, _sandboxHelper));
}
ControlParser = whitespaceAndCommentParser.Then(_controlParser.Many()); // Frontier: SkipWhitespaces<whitespaceAndCommentParser
ControlParser = whitespaceAndCommentParser.Then(_controlParser.Many());
_sawmill = Logger.GetSawmill("Guidebook");
}
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId, bool log = true)
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId)
{
if (!_prototype.TryIndex(entryId, out var entry))
if (!_prototype.Resolve(entryId, out var entry))
return false;
using var file = _resourceManager.ContentFileReadText(entry.Text);
return TryAddMarkup(control, file.ReadToEnd(), log);
return TryAddMarkup(control, file.ReadToEnd());
}
public bool TryAddMarkup(Control control, GuideEntry entry, bool log = true)
public bool TryAddMarkup(Control control, GuideEntry entry)
{
using var file = _resourceManager.ContentFileReadText(entry.Text);
return TryAddMarkup(control, file.ReadToEnd(), log);
return TryAddMarkup(control, file.ReadToEnd());
}
public bool TryAddMarkup(Control control, string text, bool log = true)
public bool TryAddMarkup(Control control, string text)
{
try
{
@@ -83,7 +83,7 @@ public sealed partial class DocumentParsingManager
}
msg.Pop();
rt.SetMessage(msg);
rt.SetMessage(msg, tagsAllowed: null);
return rt;
},
TextParser)
+1
View File
@@ -5,6 +5,7 @@ using Robust.Client.Graphics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using System.Linq;
using System.Numerics;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Holopad;
+1 -1
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);
}
+1 -1
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);
}
}
}
@@ -1,4 +1,5 @@
using System.Linq;
using System.Numerics;
using Content.Client.Items.Systems;
using Content.Shared.Clothing;
using Content.Shared.Hands;
@@ -1,6 +1,4 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Prometheus;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Shared.Nyanotrasen.Kitchen.UI;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
@@ -98,10 +98,13 @@ public sealed class ParallaxManager : IParallaxManager
}
else
{
layers = await Task.WhenAll(
// Explicitly allocate params array to avoid sandbox violation since C# 14.
var tasks = new[]
{
LoadParallaxLayers(parallaxPrototype.Layers, loadedLayers, cancel),
LoadParallaxLayers(parallaxPrototype.LayersLQ, loadedLayers, cancel)
);
LoadParallaxLayers(parallaxPrototype.LayersLQ, loadedLayers, cancel),
};
layers = await Task.WhenAll(tasks);
}
cancel.ThrowIfCancellationRequested();
@@ -11,6 +11,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
using System.Numerics;
namespace Content.Client.Power.APC.UI
{
@@ -6,7 +6,6 @@ using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Content.Client.Power;
@@ -0,0 +1,38 @@
using System.Numerics;
using Content.Shared.Sprite;
using Robust.Client.GameObjects;
namespace Content.Client.Sprite;
public sealed class ScaleVisualsSystem : SharedScaleVisualsSystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ScaleVisualsComponent, AppearanceChangeEvent>(OnChangeData);
}
private void OnChangeData(Entity<ScaleVisualsComponent> ent, ref AppearanceChangeEvent args)
{
if (!args.AppearanceData.TryGetValue(ScaleVisuals.Scale, out var scale) ||
args.Sprite == null) return;
// save the original scale
ent.Comp.OriginalScale ??= args.Sprite.Scale;
var vecScale = (Vector2)scale;
_sprite.SetScale((ent.Owner, args.Sprite), vecScale);
}
// revert to the original scale
protected override void ResetScale(Entity<ScaleVisualsComponent> ent)
{
base.ResetScale(ent);
if (ent.Comp.OriginalScale != null)
_sprite.SetScale(ent.Owner, ent.Comp.OriginalScale.Value);
}
}
@@ -148,7 +148,7 @@ public partial class ChatBox : UIWidget
("size", 8+sizeIncrease)
));
} // WD EDIT END
Contents.AddMessage(formatted);
Contents.AddMessage(formatted, tagsAllowed: null);
}
public void Focus(ChatSelectChannel? channel = null)
@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Shared.Mobs;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
@@ -43,7 +44,7 @@ public sealed class ProgressColorSystem : EntitySystem
// lerp
var hue = 5f / 18f * progress;
return Color.FromHsv((hue, 1f, 0.75f, 1f));
return Color.FromHsv(new Vector4(hue, 1f, 0.75f, 1f));
}
return InterpolateColorGaussian(Plasma, progress);
@@ -614,7 +614,7 @@ public sealed class StorageWindow : BaseWindow
{
marked.Add(cell);
cell.ModulateSelfOverride = spotFree
? Color.FromHsv((0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
? Color.FromHsv(new Vector4(0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
: Color.FromHex("#2222CC");
}
}
@@ -126,6 +126,9 @@ public sealed partial class StationPickerControl : PickerControl
foreach (var (stationEntity, stationJobInformation) in stationList)
{
var icon = stationJobInformation.StationDisplayInfo?.StationIcon;
var iconTexture = icon != null ? _spriteSystem.Frame0(icon) : null;
var viewState = new StationListItem.ViewState(
stationEntity,
stationJobInformation.GetStationNameWithJobCount(),
@@ -136,7 +139,7 @@ public sealed partial class StationPickerControl : PickerControl
? _loc.GetString(stationJobInformation.StationDisplayInfo.StationDescription)
: "",
_lastSelectedStation?.StationEntity == stationEntity,
stationJobInformation.StationDisplayInfo?.StationIcon?.CanonPath
iconTexture
);
// Always select the first station in the list if none is selected yet.
@@ -1,4 +1,5 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -13,7 +14,7 @@ public sealed partial class StationListItem : PanelContainer
string stationSubtext,
string stationDescription,
bool selected,
string? iconPath)
Texture? icon)
{
public string StationName { get; } = stationName;
public string StationSubtext { get; } = stationSubtext;
@@ -22,7 +23,7 @@ public sealed partial class StationListItem : PanelContainer
public bool Selected { get; set; } = selected;
public string IconPath { get; } = iconPath ?? "";
public Texture? Icon { get; } = icon;
}
public StationListItem(ViewState state)
@@ -35,9 +36,9 @@ public sealed partial class StationListItem : PanelContainer
// Disallow repeated selection of same station that does UI reloads.
StationButton.Disabled = state.Selected;
if (state.IconPath.Length > 0)
if (state.Icon != null)
{
StationIcon.TexturePath = state.IconPath;
StationIcon.Texture = state.Icon;
}
}
}
@@ -22,7 +22,7 @@ public sealed class RMCCombatModeUISystem : EntitySystem
_hands.GetActiveHandEntity() is { } held &&
_rmcCombatMode.GetCrosshair(held) != null)
{
_crosshairCursor ??= _clyde.CreateCursor(new Image<Rgba32>(1, 1), Vector2i.One);
_crosshairCursor ??= _clyde.CreateCursor(new Image<Rgba32>(1, 1), Vector2i.Zero);
_clyde.SetCursor(_crosshairCursor);
}
else
@@ -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>
+1
View File
@@ -3,3 +3,4 @@
global using NUnit.Framework;
global using System;
global using System.Threading.Tasks;
global using Robust.UnitTesting.Pool;
@@ -1,23 +0,0 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.IntegrationTests.Pair;
/// <summary>
/// Simple data class that stored information about a map being used by a test.
/// </summary>
public sealed class TestMapData
{
public EntityUid MapUid { get; set; }
public Entity<MapGridComponent> Grid;
public MapId MapId;
public EntityCoordinates GridCoords { get; set; }
public MapCoordinates MapCoords { get; set; }
public TileRef Tile { get; set; }
// Client-side uids
public EntityUid CMapUid { get; set; }
public EntityUid CGridUid { get; set; }
public EntityCoordinates CGridCoords { get; set; }
}
@@ -1,69 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Pair;
public sealed partial class TestPair
{
private readonly Dictionary<string, object> _modifiedClientCvars = new();
private readonly Dictionary<string, object> _modifiedServerCvars = new();
private void OnServerCvarChanged(CVarChangeInfo args)
{
_modifiedServerCvars.TryAdd(args.Name, args.OldValue);
}
private void OnClientCvarChanged(CVarChangeInfo args)
{
_modifiedClientCvars.TryAdd(args.Name, args.OldValue);
}
internal void ClearModifiedCvars()
{
_modifiedClientCvars.Clear();
_modifiedServerCvars.Clear();
}
/// <summary>
/// Reverts any cvars that were modified during a test back to their original values.
/// </summary>
public async Task RevertModifiedCvars()
{
await Server.WaitPost(() =>
{
foreach (var (name, value) in _modifiedServerCvars)
{
if (Server.CfgMan.GetCVar(name).Equals(value))
continue;
Server.Log.Info($"Resetting cvar {name} to {value}");
Server.CfgMan.SetCVar(name, value);
}
// I just love order dependent cvars
if (_modifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik))
Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik);
});
await Client.WaitPost(() =>
{
foreach (var (name, value) in _modifiedClientCvars)
{
if (Client.CfgMan.GetCVar(name).Equals(value))
continue;
var flags = Client.CfgMan.GetCVarFlags(name);
if (flags.HasFlag(CVar.REPLICATED) && flags.HasFlag(CVar.SERVER))
continue;
Client.Log.Info($"Resetting cvar {name} to {value}");
Client.CfgMan.SetCVar(name, value);
}
});
ClearModifiedCvars();
}
}
+30 -142
View File
@@ -1,180 +1,68 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Preferences.Managers;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.UnitTesting;
using Robust.Shared.Utility;
namespace Content.IntegrationTests.Pair;
// Contains misc helper functions to make writing tests easier.
public sealed partial class TestPair
{
public Task<TestMapData> CreateTestMap(bool initialized = true)
=> CreateTestMap(initialized, "Plating");
/// <summary>
/// Creates a map, a grid, and a tile, and gives back references to them.
/// Loads a test map and returns a <see cref="TestMapData"/> representing it.
/// </summary>
[MemberNotNull(nameof(TestMap))]
public async Task<TestMapData> CreateTestMap(bool initialized = true, string tile = "Plating")
/// <param name="testMapPath">The <see cref="ResPath"/> to the test map to load.</param>
/// <param name="initialized">Whether to initialize the map on load.</param>
/// <returns>A <see cref="TestMapData"/> representing the loaded map.</returns>
public async Task<TestMapData> LoadTestMap(ResPath testMapPath, bool initialized = true)
{
var mapData = new TestMapData();
TestMap = mapData;
await Server.WaitIdleAsync();
var tileDefinitionManager = Server.ResolveDependency<ITileDefinitionManager>();
TestMapData mapData = new();
var deserializationOptions = DeserializationOptions.Default with { InitializeMaps = initialized };
var mapLoaderSys = Server.EntMan.System<MapLoaderSystem>();
var mapSys = Server.System<SharedMapSystem>();
TestMap = mapData;
await Server.WaitPost(() =>
// Load our test map in and assert that it exists.
await Server.WaitAssertion(() =>
{
mapData.MapUid = Server.System<SharedMapSystem>().CreateMap(out mapData.MapId, runMapInit: initialized);
mapData.Grid = Server.MapMan.CreateGridEntity(mapData.MapId);
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
var plating = tileDefinitionManager[tile];
var platingTile = new Tile(plating.TileId);
Server.System<SharedMapSystem>().SetTile(mapData.Grid.Owner, mapData.Grid.Comp, mapData.GridCoords, platingTile);
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
mapData.Tile = Server.System<SharedMapSystem>().GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
});
Assert.That(mapLoaderSys.TryLoadMap(testMapPath, out var map, out var gridSet, deserializationOptions),
$"Failed to load map {testMapPath}.");
Assert.That(gridSet, Is.Not.Empty, "There were no grids loaded from the map!");
TestMap = mapData;
if (!Settings.Connected)
return mapData;
mapData.MapUid = map!.Value.Owner;
mapData.MapId = map!.Value.Comp.MapId;
mapData.Grid = gridSet!.First();
mapData.GridCoords = new EntityCoordinates(mapData.Grid, 0, 0);
mapData.MapCoords = new MapCoordinates(0, 0, mapData.MapId);
mapData.Tile = mapSys.GetAllTiles(mapData.Grid.Owner, mapData.Grid.Comp).First();
});
await RunTicksSync(10);
mapData.CMapUid = ToClientUid(mapData.MapUid);
mapData.CGridUid = ToClientUid(mapData.Grid);
mapData.CGridCoords = new EntityCoordinates(mapData.CGridUid, 0, 0);
TestMap = mapData;
return mapData;
}
/// <summary>
/// Convert a client-side uid into a server-side uid
/// </summary>
public EntityUid ToServerUid(EntityUid uid) => ConvertUid(uid, Client, Server);
/// <summary>
/// Convert a server-side uid into a client-side uid
/// </summary>
public EntityUid ToClientUid(EntityUid uid) => ConvertUid(uid, Server, Client);
private static EntityUid ConvertUid(
EntityUid uid,
RobustIntegrationTest.IntegrationInstance source,
RobustIntegrationTest.IntegrationInstance destination)
{
if (!uid.IsValid())
return EntityUid.Invalid;
if (!source.EntMan.TryGetComponent<MetaDataComponent>(uid, out var meta))
{
Assert.Fail($"Failed to resolve MetaData while converting the EntityUid for entity {uid}");
return EntityUid.Invalid;
}
if (!destination.EntMan.TryGetEntity(meta.NetEntity, out var otherUid))
{
Assert.Fail($"Failed to resolve net ID while converting the EntityUid entity {source.EntMan.ToPrettyString(uid)}");
return EntityUid.Invalid;
}
return otherUid.Value;
}
/// <summary>
/// Execute a command on the server and wait some number of ticks.
/// </summary>
public async Task WaitCommand(string cmd, int numTicks = 10)
{
await Server.ExecuteCommand(cmd);
await RunTicksSync(numTicks);
}
/// <summary>
/// Execute a command on the client and wait some number of ticks.
/// </summary>
public async Task WaitClientCommand(string cmd, int numTicks = 10)
{
await Client.ExecuteCommand(cmd);
await RunTicksSync(numTicks);
}
/// <summary>
/// Retrieve all entity prototypes that have some component.
/// </summary>
public List<(EntityPrototype, T)> GetPrototypesWithComponent<T>(
HashSet<string>? ignored = null,
bool ignoreAbstract = true,
bool ignoreTestPrototypes = true)
where T : IComponent, new()
{
if (!Server.ResolveDependency<IComponentFactory>().TryGetRegistration<T>(out var reg)
&& !Client.ResolveDependency<IComponentFactory>().TryGetRegistration<T>(out reg))
{
Assert.Fail($"Unknown component: {typeof(T).Name}");
return new();
}
var id = reg.Name;
var list = new List<(EntityPrototype, T)>();
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
{
if (ignored != null && ignored.Contains(proto.ID))
continue;
if (ignoreAbstract && proto.Abstract)
continue;
if (ignoreTestPrototypes && IsTestPrototype(proto))
continue;
if (proto.Components.TryGetComponent(id, out var cmp))
list.Add((proto, (T)cmp));
}
return list;
}
/// <summary>
/// Retrieve all entity prototypes that have some component.
/// </summary>
public List<EntityPrototype> GetPrototypesWithComponent(Type type,
HashSet<string>? ignored = null,
bool ignoreAbstract = true,
bool ignoreTestPrototypes = true)
{
var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(type);
var list = new List<EntityPrototype>();
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
{
if (ignored != null && ignored.Contains(proto.ID))
continue;
if (ignoreAbstract && proto.Abstract)
continue;
if (ignoreTestPrototypes && IsTestPrototype(proto))
continue;
if (proto.Components.ContainsKey(id))
list.Add((proto));
}
return list;
}
/// <summary>
/// Set a user's antag preferences. Modified preferences are automatically reset at the end of the test.
/// </summary>
public async Task SetAntagPreference(ProtoId<AntagPrototype> id, bool value, NetUserId? user = null)
{
user ??= Client.User!.Value;
if (user is not {} userId)
if (user is not { } userId)
return;
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
@@ -183,7 +71,7 @@ public sealed partial class TestPair
// Automatic preference resetting only resets slot 0.
Assert.That(prefs.SelectedCharacterIndex, Is.EqualTo(0));
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var profile = (HumanoidCharacterProfile)prefs.Characters[0];
var newProfile = profile.WithAntagPreference(id, value);
_modifiedProfiles.Add(userId);
await Server.WaitPost(() => prefMan.SetProfile(userId, 0, newProfile).Wait());
@@ -211,7 +99,7 @@ public sealed partial class TestPair
var prefMan = Server.ResolveDependency<IServerPreferencesManager>();
var prefs = prefMan.GetPreferences(user);
var profile = (HumanoidCharacterProfile) prefs.Characters[0];
var profile = (HumanoidCharacterProfile)prefs.Characters[0];
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(profile.JobPriorities);
// Automatic preference resetting only resets slot 0.
@@ -1,64 +0,0 @@
#nullable enable
using System.Collections.Generic;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using Robust.UnitTesting;
namespace Content.IntegrationTests.Pair;
// This partial class contains helper methods to deal with yaml prototypes.
public sealed partial class TestPair
{
private Dictionary<Type, HashSet<string>> _loadedPrototypes = new();
private HashSet<string> _loadedEntityPrototypes = new();
public async Task LoadPrototypes(List<string> prototypes)
{
await LoadPrototypes(Server, prototypes);
await LoadPrototypes(Client, prototypes);
}
private async Task LoadPrototypes(RobustIntegrationTest.IntegrationInstance instance, List<string> prototypes)
{
var changed = new Dictionary<Type, HashSet<string>>();
foreach (var file in prototypes)
{
instance.ProtoMan.LoadString(file, changed: changed);
}
await instance.WaitPost(() => instance.ProtoMan.ReloadPrototypes(changed));
foreach (var (kind, ids) in changed)
{
_loadedPrototypes.GetOrNew(kind).UnionWith(ids);
}
if (_loadedPrototypes.TryGetValue(typeof(EntityPrototype), out var entIds))
_loadedEntityPrototypes.UnionWith(entIds);
}
public bool IsTestPrototype(EntityPrototype proto)
{
return _loadedEntityPrototypes.Contains(proto.ID);
}
public bool IsTestEntityPrototype(string id)
{
return _loadedEntityPrototypes.Contains(id);
}
public bool IsTestPrototype<TPrototype>(string id) where TPrototype : IPrototype
{
return IsTestPrototype(typeof(TPrototype), id);
}
public bool IsTestPrototype<TPrototype>(TPrototype proto) where TPrototype : IPrototype
{
return IsTestPrototype(typeof(TPrototype), proto.ID);
}
public bool IsTestPrototype(Type kind, string id)
{
return _loadedPrototypes.TryGetValue(kind, out var ids) && ids.Contains(id);
}
}
+21 -156
View File
@@ -8,82 +8,17 @@ using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Preferences;
using Robust.Client;
using Robust.Server.Player;
using Robust.Shared.Exceptions;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Player;
namespace Content.IntegrationTests.Pair;
// This partial class contains logic related to recycling & disposing test pairs.
public sealed partial class TestPair : IAsyncDisposable
public sealed partial class TestPair
{
public PairState State { get; private set; } = PairState.Ready;
private async Task OnDirtyDispose()
protected override async Task Cleanup()
{
var usageTime = Watch.Elapsed;
Watch.Restart();
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Test gave back pair {Id} in {usageTime.TotalMilliseconds} ms");
Kill();
var disposeTime = Watch.Elapsed;
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Disposed pair {Id} in {disposeTime.TotalMilliseconds} ms");
// Test pairs should only dirty dispose if they are failing. If they are not failing, this probably happened
// because someone forgot to clean-return the pair.
Assert.Warn("Test was dirty-disposed.");
}
private async Task OnCleanDispose()
{
await Server.WaitIdleAsync();
await Client.WaitIdleAsync();
await base.Cleanup();
await ResetModifiedPreferences();
await Server.RemoveAllDummySessions();
if (TestMap != null)
{
await Server.WaitPost(() => Server.EntMan.DeleteEntity(TestMap.MapUid));
TestMap = null;
}
await RevertModifiedCvars();
var usageTime = Watch.Elapsed;
Watch.Restart();
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Test borrowed pair {Id} for {usageTime.TotalMilliseconds} ms");
// Let any last minute failures the test cause happen.
await ReallyBeIdle();
if (!Settings.Destructive)
{
if (Client.IsAlive == false)
{
throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the client in pair {Id}:", Client.UnhandledException);
}
if (Server.IsAlive == false)
{
throw new Exception($"{nameof(CleanReturnAsync)}: Test killed the server in pair {Id}:", Server.UnhandledException);
}
}
if (Settings.MustNotBeReused)
{
Kill();
await ReallyBeIdle();
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Clean disposed in {Watch.Elapsed.TotalMilliseconds} ms");
return;
}
var sRuntimeLog = Server.ResolveDependency<IRuntimeLog>();
if (sRuntimeLog.ExceptionCount > 0)
throw new Exception($"{nameof(CleanReturnAsync)}: Server logged exceptions");
var cRuntimeLog = Client.ResolveDependency<IRuntimeLog>();
if (cRuntimeLog.ExceptionCount > 0)
throw new Exception($"{nameof(CleanReturnAsync)}: Client logged exceptions");
var returnTime = Watch.Elapsed;
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: PoolManager took {returnTime.TotalMilliseconds} ms to put pair {Id} back into the pool");
}
private async Task ResetModifiedPreferences()
@@ -93,61 +28,14 @@ public sealed partial class TestPair : IAsyncDisposable
{
await Server.WaitPost(() => prefMan.SetProfile(user, 0, new HumanoidCharacterProfile()).Wait());
}
_modifiedProfiles.Clear();
}
public async ValueTask CleanReturnAsync()
protected override async Task Recycle(PairSettings next, TextWriter testOut)
{
if (State != PairState.InUse)
throw new Exception($"{nameof(CleanReturnAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
await _testOut.WriteLineAsync($"{nameof(CleanReturnAsync)}: Return of pair {Id} started");
State = PairState.CleanDisposed;
await OnCleanDispose();
State = PairState.Ready;
PoolManager.NoCheckReturn(this);
ClearContext();
}
public async ValueTask DisposeAsync()
{
switch (State)
{
case PairState.Dead:
case PairState.Ready:
break;
case PairState.InUse:
await _testOut.WriteLineAsync($"{nameof(DisposeAsync)}: Dirty return of pair {Id} started");
await OnDirtyDispose();
PoolManager.NoCheckReturn(this);
ClearContext();
break;
default:
throw new Exception($"{nameof(DisposeAsync)}: Unexpected state. Pair: {Id}. State: {State}.");
}
}
public async Task CleanPooledPair(PoolSettings settings, TextWriter testOut)
{
Settings = default!;
Watch.Restart();
await testOut.WriteLineAsync($"Recycling...");
var gameTicker = Server.System<GameTicker>();
var cNetMgr = Client.ResolveDependency<IClientNetManager>();
await RunTicksSync(1);
// Disconnect the client if they are connected.
if (cNetMgr.IsConnected)
{
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Disconnecting client.");
await Client.WaitPost(() => cNetMgr.ClientDisconnect("Test pooling cleanup disconnect"));
await RunTicksSync(1);
}
Assert.That(cNetMgr.IsConnected, Is.False);
// Move to pre-round lobby. Required to toggle dummy ticker on and off
var gameTicker = Server.System<GameTicker>();
if (gameTicker.RunLevel != GameRunLevel.PreRoundLobby)
{
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Restarting round.");
@@ -160,8 +48,7 @@ public sealed partial class TestPair : IAsyncDisposable
//Apply Cvars
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Setting CVar ");
await PoolManager.SetupCVars(Client, settings);
await PoolManager.SetupCVars(Server, settings);
await ApplySettings(next);
await RunTicksSync(1);
// Restart server.
@@ -169,52 +56,30 @@ public sealed partial class TestPair : IAsyncDisposable
await Server.WaitPost(() => Server.EntMan.FlushEntities());
await Server.WaitPost(() => gameTicker.RestartRound());
await RunTicksSync(1);
// Connect client
if (settings.ShouldBeConnected)
{
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Connecting client");
Client.SetConnectTarget(Server);
await Client.WaitPost(() => cNetMgr.ClientConnect(null!, 0, null!));
}
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Idling");
await ReallyBeIdle();
await testOut.WriteLineAsync($"Recycling: {Watch.Elapsed.TotalMilliseconds} ms: Done recycling");
}
public void ValidateSettings(PoolSettings settings)
public override void ValidateSettings(PairSettings s)
{
base.ValidateSettings(s);
var settings = (PoolSettings) s;
var cfg = Server.CfgMan;
Assert.That(cfg.GetCVar(CCVars.AdminLogsEnabled), Is.EqualTo(settings.AdminLogsEnabled));
Assert.That(cfg.GetCVar(CCVars.GameLobbyEnabled), Is.EqualTo(settings.InLobby));
Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.UseDummyTicker));
Assert.That(cfg.GetCVar(CCVars.GameDummyTicker), Is.EqualTo(settings.DummyTicker));
var entMan = Server.ResolveDependency<EntityManager>();
var ticker = entMan.System<GameTicker>();
Assert.That(ticker.DummyTicker, Is.EqualTo(settings.UseDummyTicker));
var ticker = Server.System<GameTicker>();
Assert.That(ticker.DummyTicker, Is.EqualTo(settings.DummyTicker));
var expectPreRound = settings.InLobby | settings.DummyTicker;
var expectedLevel = expectPreRound ? GameRunLevel.PreRoundLobby : GameRunLevel.InRound;
Assert.That(ticker.RunLevel, Is.EqualTo(expectedLevel));
var baseClient = Client.ResolveDependency<IBaseClient>();
var netMan = Client.ResolveDependency<INetManager>();
Assert.That(netMan.IsConnected, Is.Not.EqualTo(!settings.ShouldBeConnected));
if (!settings.ShouldBeConnected)
if (ticker.DummyTicker || !settings.Connected)
return;
Assert.That(baseClient.RunLevel, Is.EqualTo(ClientRunLevel.InGame));
var cPlayer = Client.ResolveDependency<Robust.Client.Player.IPlayerManager>();
var sPlayer = Server.ResolveDependency<IPlayerManager>();
Assert.That(sPlayer.Sessions.Count(), Is.EqualTo(1));
var sPlayer = Server.ResolveDependency<ISharedPlayerManager>();
var session = sPlayer.Sessions.Single();
Assert.That(cPlayer.LocalSession?.UserId, Is.EqualTo(session.UserId));
if (ticker.DummyTicker)
return;
var status = ticker.PlayerGameStatuses[session.UserId];
var expected = settings.InLobby
? PlayerGameStatus.NotReadyToPlay
@@ -229,11 +94,11 @@ public sealed partial class TestPair : IAsyncDisposable
}
Assert.That(session.AttachedEntity, Is.Not.Null);
Assert.That(entMan.EntityExists(session.AttachedEntity));
Assert.That(entMan.HasComponent<MindContainerComponent>(session.AttachedEntity));
var mindCont = entMan.GetComponent<MindContainerComponent>(session.AttachedEntity!.Value);
Assert.That(Server.EntMan.EntityExists(session.AttachedEntity));
Assert.That(Server.EntMan.HasComponent<MindContainerComponent>(session.AttachedEntity));
var mindCont = Server.EntMan.GetComponent<MindContainerComponent>(session.AttachedEntity!.Value);
Assert.That(mindCont.Mind, Is.Not.Null);
Assert.That(entMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
Assert.That(Server.EntMan.TryGetComponent(mindCont.Mind, out MindComponent? mind));
Assert.That(mind!.VisitingEntity, Is.Null);
Assert.That(mind.OwnedEntity, Is.EqualTo(session.AttachedEntity!.Value));
Assert.That(mind.UserId, Is.EqualTo(session.UserId));
@@ -1,77 +0,0 @@
#nullable enable
namespace Content.IntegrationTests.Pair;
// This partial class contains methods for running the server/client pairs for some number of ticks
public sealed partial class TestPair
{
/// <summary>
/// Runs the server-client pair in sync
/// </summary>
/// <param name="ticks">How many ticks to run them for</param>
public async Task RunTicksSync(int ticks)
{
for (var i = 0; i < ticks; i++)
{
await Server.WaitRunTicks(1);
await Client.WaitRunTicks(1);
}
}
/// <summary>
/// Convert a time interval to some number of ticks.
/// </summary>
public int SecondsToTicks(float seconds)
{
return (int) Math.Ceiling(seconds / Server.Timing.TickPeriod.TotalSeconds);
}
/// <summary>
/// Run the server & client in sync for some amount of time
/// </summary>
public async Task RunSeconds(float seconds)
{
await RunTicksSync(SecondsToTicks(seconds));
}
/// <summary>
/// Runs the server-client pair in sync, but also ensures they are both idle each tick.
/// </summary>
/// <param name="runTicks">How many ticks to run</param>
public async Task ReallyBeIdle(int runTicks = 25)
{
for (var i = 0; i < runTicks; i++)
{
await Client.WaitRunTicks(1);
await Server.WaitRunTicks(1);
for (var idleCycles = 0; idleCycles < 4; idleCycles++)
{
await Client.WaitIdleAsync();
await Server.WaitIdleAsync();
}
}
}
/// <summary>
/// Run the server/clients until the ticks are synchronized.
/// By default the client will be one tick ahead of the server.
/// </summary>
public async Task SyncTicks(int targetDelta = 1)
{
var sTick = (int)Server.Timing.CurTick.Value;
var cTick = (int)Client.Timing.CurTick.Value;
var delta = cTick - sTick;
if (delta == targetDelta)
return;
if (delta > targetDelta)
await Server.WaitRunTicks(delta - targetDelta);
else
await Client.WaitRunTicks(targetDelta - delta);
sTick = (int)Server.Timing.CurTick.Value;
cTick = (int)Client.Timing.CurTick.Value;
delta = cTick - sTick;
Assert.That(delta, Is.EqualTo(targetDelta));
}
}
+88 -134
View File
@@ -1,16 +1,17 @@
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Content.Client.IoC;
using Content.Client.Parallax.Managers;
using Content.IntegrationTests.Tests.Destructible;
using Content.IntegrationTests.Tests.DeviceNetwork;
using Content.Server.GameTicking;
using Content.Shared.CCVar;
using Content.Shared.Players;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.UnitTesting;
namespace Content.IntegrationTests.Pair;
@@ -18,156 +19,109 @@ namespace Content.IntegrationTests.Pair;
/// <summary>
/// This object wraps a pooled server+client pair.
/// </summary>
public sealed partial class TestPair
public sealed partial class TestPair : RobustIntegrationTest.TestPair
{
public readonly int Id;
private bool _initialized;
private TextWriter _testOut = default!;
public readonly Stopwatch Watch = new();
public readonly List<string> TestHistory = new();
public PoolSettings Settings = default!;
public TestMapData? TestMap;
private List<NetUserId> _modifiedProfiles = new();
private int _nextServerSeed;
private int _nextClientSeed;
public int ServerSeed;
public int ClientSeed;
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
public void Deconstruct(
out RobustIntegrationTest.ServerIntegrationInstance server,
out RobustIntegrationTest.ClientIntegrationInstance client)
{
server = Server;
client = Client;
}
public ICommonSession? Player => Server.PlayerMan.SessionsDict.GetValueOrDefault(Client.User!.Value);
public ContentPlayerData? PlayerData => Player?.Data.ContentData();
public PoolTestLogHandler ServerLogHandler { get; private set; } = default!;
public PoolTestLogHandler ClientLogHandler { get; private set; } = default!;
public TestPair(int id)
protected override async Task Initialize()
{
Id = id;
}
await base.Initialize();
public async Task Initialize(PoolSettings settings, TextWriter testOut, List<string> testPrototypes)
{
if (_initialized)
throw new InvalidOperationException("Already initialized");
_initialized = true;
Settings = settings;
(Client, ClientLogHandler) = await PoolManager.GenerateClient(settings, testOut);
(Server, ServerLogHandler) = await PoolManager.GenerateServer(settings, testOut);
ActivateContext(testOut);
Client.CfgMan.OnCVarValueChanged += OnClientCvarChanged;
Server.CfgMan.OnCVarValueChanged += OnServerCvarChanged;
if (!settings.NoLoadTestPrototypes)
await LoadPrototypes(testPrototypes!);
if (!settings.UseDummyTicker)
// Prevent info log spam in some tests (particularly SpawnAndDeleteAllEntitiesOnDifferentMaps)
Server.System<SharedMapSystem>().Log.Level = LogLevel.Warning;
Client.EntMan.EntitySysManager.SystemLoaded += (_, e) =>
{
var gameTicker = Server.ResolveDependency<IEntityManager>().System<GameTicker>();
if (e.System is SharedMapSystem map)
map.Log.Level = LogLevel.Warning;
};
var settings = (PoolSettings)Settings;
if (!settings.DummyTicker)
{
var gameTicker = Server.System<GameTicker>();
await Server.WaitPost(() => gameTicker.RestartRound());
}
}
// Always initially connect clients to generate an initial random set of preferences/profiles.
// This is to try and prevent issues where if the first test that connects the client is consistently some test
// that uses a fixed seed, it would effectively prevent it from beingrandomized.
public override async Task RevertModifiedCvars()
{
// I just love order dependent cvars
// I.e., cvars that when changed automatically cause others to also change.
var modified = ModifiedServerCvars.TryGetValue(CCVars.PanicBunkerEnabled.Name, out var panik);
Client.SetConnectTarget(Server);
await Client.WaitIdleAsync();
var netMgr = Client.ResolveDependency<IClientNetManager>();
await Client.WaitPost(() => netMgr.ClientConnect(null!, 0, null!));
await ReallyBeIdle(10);
await Client.WaitRunTicks(1);
await base.RevertModifiedCvars();
if (!settings.ShouldBeConnected)
if (!modified)
return;
await Server.WaitPost(() => Server.CfgMan.SetCVar(CCVars.PanicBunkerEnabled.Name, panik!));
ClearModifiedCvars();
}
protected override async Task ApplySettings(IIntegrationInstance instance, PairSettings n)
{
var next = (PoolSettings)n;
await base.ApplySettings(instance, next);
var cfg = instance.CfgMan;
await instance.WaitPost(() =>
{
await Client.WaitPost(() => netMgr.ClientDisconnect("Initial disconnect"));
await ReallyBeIdle(10);
}
if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
cfg.SetCVar(CCVars.GameDummyTicker, next.DummyTicker);
var cRand = Client.ResolveDependency<IRobustRandom>();
var sRand = Server.ResolveDependency<IRobustRandom>();
_nextClientSeed = cRand.Next();
_nextServerSeed = sRand.Next();
if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
cfg.SetCVar(CCVars.GameLobbyEnabled, next.InLobby);
if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
cfg.SetCVar(CCVars.GameMap, next.Map);
if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
cfg.SetCVar(CCVars.AdminLogsEnabled, next.AdminLogsEnabled);
});
}
public void Kill()
protected override RobustIntegrationTest.ClientIntegrationOptions ClientOptions()
{
State = PairState.Dead;
ServerLogHandler.ShuttingDown = true;
ClientLogHandler.ShuttingDown = true;
Server.Dispose();
Client.Dispose();
}
var opts = base.ClientOptions();
private void ClearContext()
{
_testOut = default!;
ServerLogHandler.ClearContext();
ClientLogHandler.ClearContext();
}
public void ActivateContext(TextWriter testOut)
{
_testOut = testOut;
ServerLogHandler.ActivateContext(testOut);
ClientLogHandler.ActivateContext(testOut);
}
public void Use()
{
if (State != PairState.Ready)
throw new InvalidOperationException($"Pair is not ready to use. State: {State}");
State = PairState.InUse;
}
public enum PairState : byte
{
Ready = 0,
InUse = 1,
CleanDisposed = 2,
Dead = 3,
}
public void SetupSeed()
{
var sRand = Server.ResolveDependency<IRobustRandom>();
if (Settings.ServerSeed is { } severSeed)
opts.LoadTestAssembly = false;
opts.ContentStart = true;
opts.FailureLogLevel = LogLevel.Warning;
opts.Options = new()
{
ServerSeed = severSeed;
sRand.SetSeed(ServerSeed);
}
else
{
ServerSeed = _nextServerSeed;
sRand.SetSeed(ServerSeed);
_nextServerSeed = sRand.Next();
}
LoadConfigAndUserData = false,
};
var cRand = Client.ResolveDependency<IRobustRandom>();
if (Settings.ClientSeed is { } clientSeed)
opts.BeforeStart += () =>
{
ClientSeed = clientSeed;
cRand.SetSeed(ClientSeed);
}
else
IoCManager.Resolve<IModLoader>().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
{
ClientBeforeIoC = () => IoCManager.Register<IParallaxManager, DummyParallaxManager>(true)
});
};
return opts;
}
protected override RobustIntegrationTest.ServerIntegrationOptions ServerOptions()
{
var opts = base.ServerOptions();
opts.LoadTestAssembly = false;
opts.ContentStart = true;
opts.Options = new()
{
ClientSeed = _nextClientSeed;
cRand.SetSeed(ClientSeed);
_nextClientSeed = cRand.Next();
}
LoadConfigAndUserData = false,
};
opts.BeforeStart += () =>
{
// Server-only systems (i.e., systems that subscribe to events with server-only components)
// There's probably a better way to do this.
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
};
return opts;
}
}
+5 -47
View File
@@ -1,15 +1,14 @@
#nullable enable
using Content.Shared.CCVar;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.UnitTesting;
namespace Content.IntegrationTests;
// Partial class containing cvar logic
// Partial class containing test cvars
// This could probably be merged into the main file, but I'm keeping it separate to reduce
// conflicts for forks.
public static partial class PoolManager
{
private static readonly (string cvar, string value)[] TestCvars =
public static readonly (string cvar, string value)[] TestCvars =
{
// @formatter:off
(CCVars.DatabaseSynchronous.Name, "true"),
@@ -17,9 +16,7 @@ public static partial class PoolManager
(CCVars.HolidaysEnabled.Name, "false"),
(CCVars.GameMap.Name, TestMap),
(CCVars.AdminLogsQueueSendDelay.Name, "0"),
(CVars.NetPVS.Name, "false"),
(CCVars.NPCMaxUpdates.Name, "999999"),
(CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
(CCVars.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"),
@@ -29,55 +26,16 @@ public static partial class PoolManager
(CCVars.ProcgenPreload.Name, "false"),
(CCVars.WorldgenEnabled.Name, "false"),
(CCVars.GatewayGeneratorEnabled.Name, "false"),
(CVars.ReplayClientRecordingEnabled.Name, "false"),
(CVars.ReplayServerRecordingEnabled.Name, "false"),
(CCVars.GameDummyTicker.Name, "true"),
(CCVars.GameLobbyEnabled.Name, "false"),
(CCVars.ConfigPresetDevelopment.Name, "false"),
(CCVars.AdminLogsEnabled.Name, "false"),
(CCVars.AutosaveEnabled.Name, "false"),
(CVars.NetBufferSize.Name, "0"),
(CCVars.InteractionRateLimitCount.Name, "9999999"),
(CCVars.InteractionRateLimitPeriod.Name, "0.1"),
(CCVars.MovementMobPushing.Name, "false"),
(CCVars.GameLobbyDefaultPreset.Name, "secret"), // Frontier: Adventure takes ages, default to secret
(CCVars.StaticStorageUI.Name, "true"),// Frontier: causes storage test failures
(CCVars.StorageLimit.Name, "1")// Frontier: test failures with multiple storage enabled
(CCVars.StorageLimit.Name, "1")// Frontier: test failures with multiple storage
};
public static async Task SetupCVars(RobustIntegrationTest.IntegrationInstance instance, PoolSettings settings)
{
var cfg = instance.ResolveDependency<IConfigurationManager>();
await instance.WaitPost(() =>
{
if (cfg.IsCVarRegistered(CCVars.GameDummyTicker.Name))
cfg.SetCVar(CCVars.GameDummyTicker, settings.UseDummyTicker);
if (cfg.IsCVarRegistered(CCVars.GameLobbyEnabled.Name))
cfg.SetCVar(CCVars.GameLobbyEnabled, settings.InLobby);
if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
cfg.SetCVar(CVars.NetInterp, settings.DisableInterpolate);
if (cfg.IsCVarRegistered(CCVars.GameMap.Name))
cfg.SetCVar(CCVars.GameMap, settings.Map);
if (cfg.IsCVarRegistered(CCVars.AdminLogsEnabled.Name))
cfg.SetCVar(CCVars.AdminLogsEnabled, settings.AdminLogsEnabled);
if (cfg.IsCVarRegistered(CVars.NetInterp.Name))
cfg.SetCVar(CVars.NetInterp, !settings.DisableInterpolate);
if (cfg.IsCVarRegistered(CCVars.GameLobbyDefaultPreset.Name) && !string.IsNullOrEmpty(settings.GameLobbyDefaultPreset)) // Frontier
cfg.SetCVar(CCVars.GameLobbyDefaultPreset, settings.GameLobbyDefaultPreset); // Frontier
});
}
private static void SetDefaultCVars(RobustIntegrationTest.IntegrationOptions options)
{
foreach (var (cvar, value) in TestCvars)
{
options.CVarOverrides[cvar] = value;
}
}
}
+37 -378
View File
@@ -1,371 +1,17 @@
#nullable enable
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using Content.Client.IoC;
using Content.Client.Parallax.Managers;
using Content.IntegrationTests.Pair;
using Content.IntegrationTests.Tests;
using Content.IntegrationTests.Tests.Destructible;
using Content.IntegrationTests.Tests.DeviceNetwork;
using Content.IntegrationTests.Tests.Interaction.Click;
using Robust.Client;
using Robust.Server;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Content.Shared.CCVar;
using Robust.UnitTesting;
namespace Content.IntegrationTests;
/// <summary>
/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
/// </summary>
// The static class exist to avoid breaking changes
public static partial class PoolManager
{
public static readonly ContentPoolManager Instance = new();
public const string TestMap = "Empty";
private static int _pairId;
private static readonly object PairLock = new();
private static bool _initialized;
// Pair, IsBorrowed
private static readonly Dictionary<TestPair, bool> Pairs = new();
private static bool _dead;
private static Exception? _poolFailureReason;
private static HashSet<Assembly> _contentAssemblies = default!;
public static async Task<(RobustIntegrationTest.ServerIntegrationInstance, PoolTestLogHandler)> GenerateServer(
PoolSettings poolSettings,
TextWriter testOut)
{
var options = new RobustIntegrationTest.ServerIntegrationOptions
{
ContentStart = true,
Options = new ServerOptions()
{
LoadConfigAndUserData = false,
LoadContentResources = !poolSettings.NoLoadContent,
},
ContentAssemblies = _contentAssemblies.ToArray()
};
var logHandler = new PoolTestLogHandler("SERVER");
logHandler.ActivateContext(testOut);
options.OverrideLogHandler = () => logHandler;
options.BeforeStart += () =>
{
// Server-only systems (i.e., systems that subscribe to events with server-only components)
var entSysMan = IoCManager.Resolve<IEntitySystemManager>();
entSysMan.LoadExtraSystemType<DeviceNetworkTestSystem>();
entSysMan.LoadExtraSystemType<TestDestructibleListenerSystem>();
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
if (!poolSettings.NoValidate) // Mono
IoCManager.Resolve<IConfigurationManager>()
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
};
SetDefaultCVars(options);
var server = new RobustIntegrationTest.ServerIntegrationInstance(options);
await server.WaitIdleAsync();
await SetupCVars(server, poolSettings);
return (server, logHandler);
}
/// <summary>
/// This shuts down the pool, and disposes all the server/client pairs.
/// This is a one time operation to be used when the testing program is exiting.
/// </summary>
public static void Shutdown()
{
List<TestPair> localPairs;
lock (PairLock)
{
if (_dead)
return;
_dead = true;
localPairs = Pairs.Keys.ToList();
}
foreach (var pair in localPairs)
{
pair.Kill();
}
_initialized = false;
}
public static string DeathReport()
{
lock (PairLock)
{
var builder = new StringBuilder();
var pairs = Pairs.Keys.OrderBy(pair => pair.Id);
foreach (var pair in pairs)
{
var borrowed = Pairs[pair];
builder.AppendLine($"Pair {pair.Id}, Tests Run: {pair.TestHistory.Count}, Borrowed: {borrowed}");
for (var i = 0; i < pair.TestHistory.Count; i++)
{
builder.AppendLine($"#{i}: {pair.TestHistory[i]}");
}
}
return builder.ToString();
}
}
public static async Task<(RobustIntegrationTest.ClientIntegrationInstance, PoolTestLogHandler)> GenerateClient(
PoolSettings poolSettings,
TextWriter testOut)
{
var options = new RobustIntegrationTest.ClientIntegrationOptions
{
FailureLogLevel = LogLevel.Warning,
ContentStart = true,
ContentAssemblies = new[]
{
typeof(Shared.Entry.EntryPoint).Assembly,
typeof(Client.Entry.EntryPoint).Assembly,
typeof(PoolManager).Assembly,
}
};
if (poolSettings.NoLoadContent)
{
Assert.Warn("NoLoadContent does not work on the client, ignoring");
}
options.Options = new GameControllerOptions()
{
LoadConfigAndUserData = false,
// LoadContentResources = !poolSettings.NoLoadContent
};
var logHandler = new PoolTestLogHandler("CLIENT");
logHandler.ActivateContext(testOut);
options.OverrideLogHandler = () => logHandler;
options.BeforeStart += () =>
{
IoCManager.Resolve<IModLoader>().SetModuleBaseCallbacks(new ClientModuleTestingCallbacks
{
ClientBeforeIoC = () =>
{
// do not register extra systems or components here -- they will get cleared when the client is
// disconnected. just use reflection.
IoCManager.Register<IParallaxManager, DummyParallaxManager>(true);
IoCManager.Resolve<ILogManager>().GetSawmill("loc").Level = LogLevel.Error;
if (!poolSettings.NoValidate) // Mono
IoCManager.Resolve<IConfigurationManager>()
.OnValueChanged(RTCVars.FailureLogLevel, value => logHandler.FailureLevel = value, true);
}
});
};
SetDefaultCVars(options);
var client = new RobustIntegrationTest.ClientIntegrationInstance(options);
await client.WaitIdleAsync();
await SetupCVars(client, poolSettings);
return (client, logHandler);
}
/// <summary>
/// Gets a <see cref="Pair.TestPair"/>, which can be used to get access to a server, and client <see cref="Pair.TestPair"/>
/// </summary>
/// <param name="poolSettings">See <see cref="PoolSettings"/></param>
/// <returns></returns>
public static async Task<TestPair> GetServerClient(PoolSettings? poolSettings = null)
{
return await GetServerClientPair(poolSettings ?? new PoolSettings());
}
private static string GetDefaultTestName(TestContext testContext)
{
return testContext.Test.FullName.Replace("Content.IntegrationTests.Tests.", "");
}
private static async Task<TestPair> GetServerClientPair(PoolSettings poolSettings)
{
if (!_initialized)
throw new InvalidOperationException($"Pool manager has not been initialized");
// Trust issues with the AsyncLocal that backs this.
var testContext = TestContext.CurrentContext;
var testOut = TestContext.Out;
DieIfPoolFailure();
var currentTestName = poolSettings.TestName ?? GetDefaultTestName(testContext);
var poolRetrieveTimeWatch = new Stopwatch();
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Called by test {currentTestName}");
TestPair? pair = null;
try
{
poolRetrieveTimeWatch.Start();
if (poolSettings.MustBeNew)
{
await testOut.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Creating pair, because settings of pool settings");
pair = await CreateServerClientPair(poolSettings, testOut);
}
else
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Looking in pool for a suitable pair");
pair = GrabOptimalPair(poolSettings);
if (pair != null)
{
pair.ActivateContext(testOut);
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Suitable pair found");
var canSkip = pair.Settings.CanFastRecycle(poolSettings);
if (canSkip)
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleanup not needed, Skipping cleanup of pair");
await SetupCVars(pair.Client, poolSettings);
await SetupCVars(pair.Server, poolSettings);
await pair.RunTicksSync(1);
}
else
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Cleaning existing pair");
await pair.CleanPooledPair(poolSettings, testOut);
}
await pair.RunTicksSync(5);
await pair.SyncTicks(targetDelta: 1);
}
else
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Creating a new pair, no suitable pair found in pool");
pair = await CreateServerClientPair(poolSettings, testOut);
}
}
}
finally
{
if (pair != null && pair.TestHistory.Count > 0)
{
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History Start");
for (var i = 0; i < pair.TestHistory.Count; i++)
{
await testOut.WriteLineAsync($"- Pair {pair.Id} Test #{i}: {pair.TestHistory[i]}");
}
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History End");
}
}
if (!poolSettings.NoValidate) // Mono
pair.ValidateSettings(poolSettings);
var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
await testOut.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Retrieving pair {pair.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
pair.ClearModifiedCvars();
pair.Settings = poolSettings;
pair.TestHistory.Add(currentTestName);
pair.SetupSeed();
await testOut.WriteLineAsync(
$"{nameof(GetServerClientPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}");
pair.Watch.Restart();
return pair;
}
private static TestPair? GrabOptimalPair(PoolSettings poolSettings)
{
lock (PairLock)
{
TestPair? fallback = null;
foreach (var pair in Pairs.Keys)
{
if (Pairs[pair])
continue;
if (!pair.Settings.CanFastRecycle(poolSettings))
{
fallback = pair;
continue;
}
pair.Use();
Pairs[pair] = true;
return pair;
}
if (fallback != null)
{
fallback.Use();
Pairs[fallback!] = true;
}
return fallback;
}
}
/// <summary>
/// Used by TestPair after checking the server/client pair, Don't use this.
/// </summary>
public static void NoCheckReturn(TestPair pair)
{
lock (PairLock)
{
if (pair.State == TestPair.PairState.Dead)
Pairs.Remove(pair);
else if (pair.State == TestPair.PairState.Ready)
Pairs[pair] = false;
else
throw new InvalidOperationException($"Attempted to return a pair in an invalid state. Pair: {pair.Id}. State: {pair.State}.");
}
}
private static void DieIfPoolFailure()
{
if (_poolFailureReason != null)
{
// If the _poolFailureReason is not null, we can assume at least one test failed.
// So we say inconclusive so we don't add more failed tests to search through.
Assert.Inconclusive(@$"
In a different test, the pool manager had an exception when trying to create a server/client pair.
Instead of risking that the pool manager will fail at creating a server/client pairs for every single test,
we are just going to end this here to save a lot of time. This is the exception that started this:\n {_poolFailureReason}");
}
if (_dead)
{
// If Pairs is null, we ran out of time, we can't assume a test failed.
// So we are going to tell it all future tests are a failure.
Assert.Fail("The pool was shut down");
}
}
private static async Task<TestPair> CreateServerClientPair(PoolSettings poolSettings, TextWriter testOut)
{
try
{
var id = Interlocked.Increment(ref _pairId);
var pair = new TestPair(id);
await pair.Initialize(poolSettings, testOut, _testPrototypes);
pair.Use();
await pair.RunTicksSync(5);
await pair.SyncTicks(targetDelta: 1);
return pair;
}
catch (Exception ex)
{
_poolFailureReason = ex;
throw;
}
}
/// <summary>
/// Runs a server, or a client until a condition is true
@@ -421,29 +67,42 @@ we are just going to end this here to save a lot of time. This is the exception
Assert.That(passed);
}
/// <summary>
/// Initialize the pool manager.
/// </summary>
/// <param name="extraAssemblies">Assemblies to search for to discover extra prototypes and systems.</param>
public static void Startup(params Assembly[] extraAssemblies)
public static async Task<TestPair> GetServerClient(
PoolSettings? settings = null,
ITestContextLike? testContext = null)
{
if (_initialized)
throw new InvalidOperationException("Already initialized");
return await Instance.GetPair(settings, testContext);
}
_initialized = true;
_contentAssemblies =
[
typeof(Shared.Entry.EntryPoint).Assembly,
typeof(Server.Entry.EntryPoint).Assembly,
typeof(PoolManager).Assembly
];
_contentAssemblies.UnionWith(extraAssemblies);
public static void Startup(params Assembly[] extra)
=> Instance.Startup(extra);
_testPrototypes.Clear();
DiscoverTestPrototypes(typeof(PoolManager).Assembly);
foreach (var assembly in extraAssemblies)
{
DiscoverTestPrototypes(assembly);
}
public static void Shutdown() => Instance.Shutdown();
public static string DeathReport() => Instance.DeathReport();
}
/// <summary>
/// Making clients, and servers is slow, this manages a pool of them so tests can reuse them.
/// </summary>
public sealed class ContentPoolManager : PoolManager<TestPair>
{
public override PairSettings DefaultSettings => new PoolSettings();
protected override string GetDefaultTestName(ITestContextLike testContext)
{
return testContext.FullName.Replace("Content.IntegrationTests.Tests.", "");
}
public override void Startup(params Assembly[] extraAssemblies)
{
DefaultCvars.AddRange(PoolManager.TestCvars);
var shared = extraAssemblies
.Append(typeof(Shared.Entry.EntryPoint).Assembly)
.Append(typeof(PoolManager).Assembly)
.ToArray();
Startup([typeof(Client.Entry.EntryPoint).Assembly],
[typeof(Server.Entry.EntryPoint).Assembly],
shared);
}
}
+22 -106
View File
@@ -1,43 +1,31 @@
#nullable enable
namespace Content.IntegrationTests;
using Robust.Shared.Random;
namespace Content.IntegrationTests;
/// <summary>
/// Settings for the pooled server, and client pair.
/// Some options are for changing the pair, and others are
/// so the pool can properly clean up what you borrowed.
/// </summary>
public sealed class PoolSettings
/// <inheritdoc/>
public sealed class PoolSettings : PairSettings
{
/// <summary>
/// Set to true if the test will ruin the server/client pair.
/// </summary>
public bool Destructive { get; init; }
public override bool Connected
{
get => _connected || InLobby;
init => _connected = value;
}
/// <summary>
/// Set to true if the given server/client pair should be created fresh.
/// </summary>
public bool Fresh { get; init; }
private readonly bool _dummyTicker = true;
private readonly bool _connected;
/// <summary>
/// Set to true if the given server should be using a dummy ticker. Ignored if <see cref="InLobby"/> is true.
/// </summary>
public bool DummyTicker { get; init; } = true;
public bool DummyTicker
{
get => _dummyTicker && !InLobby;
init => _dummyTicker = value;
}
/// <summary>
/// If true, this enables the creation of admin logs during the test.
/// </summary>
public bool AdminLogsEnabled { get; init; }
/// <summary>
/// Set to true if the given server/client pair should be connected from each other.
/// Defaults to disconnected as it makes dirty recycling slightly faster.
/// If <see cref="InLobby"/> is true, this option is ignored.
/// </summary>
public bool Connected { get; init; }
/// <summary>
/// Set to true if the given server/client pair should be in the lobby.
/// If the pair is not in the lobby at the end of the test, this test must be marked as dirty.
@@ -53,94 +41,22 @@ public sealed class PoolSettings
/// </summary>
public bool NoLoadContent { get; init; }
/// <summary>
/// This will return a server-client pair that has not loaded test prototypes.
/// Try avoiding this whenever possible, as this will always create & destroy a new pair.
/// Use <see cref="Pair.TestPair.IsTestPrototype(Robust.Shared.Prototypes.EntityPrototype)"/> if you need to exclude test prototypees.
/// </summary>
public bool NoLoadTestPrototypes { get; init; }
/// <summary>
/// Set this to true to disable the NetInterp CVar on the given server/client pair
/// </summary>
public bool DisableInterpolate { get; init; }
/// <summary>
/// Set this to true to always clean up the server/client pair before giving it to another borrower
/// </summary>
public bool Dirty { get; init; }
/// <summary>
/// Set this to the path of a map to have the given server/client pair load the map.
/// </summary>
public string Map { get; init; } = PoolManager.TestMap;
/// <summary>
/// Overrides the test name detection, and uses this in the test history instead
/// </summary>
public string? TestName { get; set; }
/// <summary>
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
/// </summary>
public int? ServerSeed { get; set; }
/// <summary>
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
/// </summary>
public int? ClientSeed { get; set; }
/// <summary>
/// Mono - skips pair validation if set. Also stops termination on error.
/// </summary>
public bool NoValidate = false;
/// <summary>
/// Frontier: the preset to run the game in.
/// Set to secret for upstream tests to mimic upstream behaviour.
/// If you need to check adventure game rule things, set this to nfadventure or nfpirate.
/// </summary>
public string GameLobbyDefaultPreset { get; set; } = "secret";
#region Inferred Properties
/// <summary>
/// If the returned pair must not be reused
/// </summary>
public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
/// <summary>
/// If the given pair must be brand new
/// </summary>
public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
public bool UseDummyTicker => !InLobby && DummyTicker;
public bool ShouldBeConnected => InLobby || Connected;
#endregion
/// <summary>
/// Tries to guess if we can skip recycling the server/client pair.
/// </summary>
/// <param name="nextSettings">The next set of settings the old pair will be set to</param>
/// <returns>If we can skip cleaning it up</returns>
public bool CanFastRecycle(PoolSettings nextSettings)
public override bool CanFastRecycle(PairSettings nextSettings)
{
if (MustNotBeReused)
throw new InvalidOperationException("Attempting to recycle a non-reusable test.");
if (!base.CanFastRecycle(nextSettings))
return false;
if (nextSettings.MustBeNew)
throw new InvalidOperationException("Attempting to recycle a test while requesting a fresh test.");
if (Dirty)
if (nextSettings is not PoolSettings next)
return false;
// Check that certain settings match.
return !ShouldBeConnected == !nextSettings.ShouldBeConnected
&& UseDummyTicker == nextSettings.UseDummyTicker
&& Map == nextSettings.Map
&& InLobby == nextSettings.InLobby
&& GameLobbyDefaultPreset == nextSettings.GameLobbyDefaultPreset; // Frontier: swappable presets
return DummyTicker == next.DummyTicker
&& Map == next.Map
&& InLobby == next.InLobby;
}
}
@@ -1,79 +0,0 @@
using System.IO;
using Robust.Shared.Log;
using Robust.Shared.Timing;
using Serilog.Events;
namespace Content.IntegrationTests;
#nullable enable
/// <summary>
/// Log handler intended for pooled integration tests.
/// </summary>
/// <remarks>
/// <para>
/// This class logs to two places: an NUnit <see cref="TestContext"/>
/// (so it nicely gets attributed to a test in your IDE),
/// and an in-memory ring buffer for diagnostic purposes.
/// If test pooling breaks, the ring buffer can be used to see what the broken instance has gone through.
/// </para>
/// <para>
/// The active test context can be swapped out so pooled instances can correctly have their logs attributed.
/// </para>
/// </remarks>
public sealed class PoolTestLogHandler : ILogHandler
{
private readonly string? _prefix;
private RStopwatch _stopwatch;
public TextWriter? ActiveContext { get; private set; }
public LogLevel? FailureLevel { get; set; }
public PoolTestLogHandler(string? prefix)
{
_prefix = prefix != null ? $"{prefix}: " : "";
}
public bool ShuttingDown;
public void Log(string sawmillName, LogEvent message)
{
var level = message.Level.ToRobust();
if (ShuttingDown && (FailureLevel == null || level < FailureLevel))
return;
if (ActiveContext is not { } testContext)
{
// If this gets hit it means something is logging to this instance while it's "between" tests.
// This is a bug in either the game or the testing system, and must always be investigated.
throw new InvalidOperationException("Log to pool test log handler without active test context");
}
var name = LogMessage.LogLevelToName(level);
var seconds = _stopwatch.Elapsed.TotalSeconds;
var rendered = message.RenderMessage();
var line = $"{_prefix}{seconds:F3}s [{name}] {sawmillName}: {rendered}";
testContext.WriteLine(line);
if (FailureLevel == null || level < FailureLevel)
return;
testContext.Flush();
Assert.Fail($"{line} Exception: {message.Exception}");
}
public void ClearContext()
{
ActiveContext = null;
}
public void ActivateContext(TextWriter context)
{
_stopwatch.Restart();
ActiveContext = context;
}
}
@@ -1,12 +0,0 @@
using JetBrains.Annotations;
namespace Content.IntegrationTests;
/// <summary>
/// Attribute that indicates that a string contains yaml prototype data that should be loaded by integration tests.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
[MeansImplicitUse]
public sealed class TestPrototypesAttribute : Attribute
{
}
+97 -66
View File
@@ -1,7 +1,7 @@
#nullable enable
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using Robust.Shared;
using Robust.Shared.Audio.Components;
using Robust.Shared.Configuration;
@@ -10,6 +10,7 @@ using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.IntegrationTests.Tests
{
@@ -56,7 +57,7 @@ namespace Content.IntegrationTests.Tests
}
});
await server.WaitRunTicks(15);
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
await server.WaitPost(() =>
{
@@ -114,7 +115,7 @@ namespace Content.IntegrationTests.Tests
SpawnEntity(entityMan, protoId, map.GridCoords); // Monolith
}
});
await server.WaitRunTicks(15);
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
await server.WaitPost(() =>
{
static IEnumerable<(EntityUid, TComp)> Query<TComp>(IEntityManager entityMan)
@@ -278,74 +279,104 @@ namespace Content.IntegrationTests.Tests
await pair.RunTicksSync(3);
// We consider only non-audio entities, as some entities will just play sounds when they spawn.
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(e => !entMan.HasComponent<AudioComponent>(e));
foreach (var protoId in protoIds)
await Assert.MultipleAsync(async () =>
{
// TODO fix ninja
// Currently ninja fails to equip their own loadout.
if (protoId == "MobHumanSpaceNinja")
continue;
var count = Count(server.EntMan);
var clientCount = Count(client.EntMan);
EntityUid uid = default;
await server.WaitPost(() => uid = SpawnEntity(server.EntMan, protoId, coords)); // Monolith
await pair.RunTicksSync(3);
// If the entity deleted itself, check that it didn't spawn other entities
if (!server.EntMan.EntityExists(uid))
foreach (var protoId in protoIds)
{
if (Count(server.EntMan) != count)
var count = Count(server.EntMan);
var clientCount = Count(client.EntMan);
var serverEntities = new HashSet<EntityUid>(Entities(server.EntMan));
var clientEntities = new HashSet<EntityUid>(Entities(client.EntMan));
EntityUid uid = default;
await server.WaitPost(() => uid = SpawnEntity(server.EntMan, protoId, coords)); // Monolith
await pair.RunTicksSync(3);
// If the entity deleted itself, check that it didn't spawn other entities
if (!server.EntMan.EntityExists(uid))
{
Assert.Fail($"Server prototype {protoId} failed on deleting itself");
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deleting itself\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deleting itself\n" +
$"Expected {clientCount} and found {client.EntMan.EntityCount}.\n" +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
continue;
}
if (Count(client.EntMan) != clientCount)
{
Assert.Fail($"Client prototype {protoId} failed on deleting itself\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server was {count}.");
}
continue;
}
// Check that the number of entities has increased.
Assert.That(Count(server.EntMan), Is.GreaterThan(count), $"Server prototype {protoId} failed on spawning as entity count didn't increase\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(Count(client.EntMan), Is.GreaterThan(clientCount), $"Client prototype {protoId} failed on spawning as entity count didn't increase\n" +
$"Expected at least {clientCount} and found {client.EntMan.EntityCount}. " +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
// Check that the number of entities has increased.
if (Count(server.EntMan) <= count)
{
Assert.Fail($"Server prototype {protoId} failed on spawning as entity count didn't increase");
}
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
await pair.RunTicksSync(3);
if (Count(client.EntMan) <= clientCount)
{
Assert.Fail($"Client prototype {protoId} failed on spawning as entity count didn't increase" +
$"Expected at least {clientCount} and found {Count(client.EntMan)}. " +
$"Server was {count}");
// Check that the number of entities has gone back to the original value.
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deletion: count didn't reset properly\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deletion: count didn't reset properly:\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server count was {count}.\n" +
BuildDiffString(clientEntities, Entities(client.EntMan), client.EntMan));
}
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
await pair.RunTicksSync(3);
// Check that the number of entities has gone back to the original value.
if (Count(server.EntMan) != count)
{
// Frontier: add expected vs. actual
Assert.Fail($"Server prototype {protoId} failed on deletion count didn't reset properly:\n" +
$"Expected {count} and found {server.EntMan.EntityCount}.\n");
// End Frontier
}
if (Count(client.EntMan) != clientCount)
{
Assert.Fail($"Client prototype {protoId} failed on deletion count didn't reset properly:\n" +
$"Expected {clientCount} and found {Count(client.EntMan)}.\n" +
$"Server was {count}.");
}
}
});
await pair.CleanReturnAsync();
}
private static string BuildDiffString(IEnumerable<EntityUid> oldEnts, IEnumerable<EntityUid> newEnts, IEntityManager entMan)
{
var sb = new StringBuilder();
var addedEnts = newEnts.Except(oldEnts);
var removedEnts = oldEnts.Except(newEnts);
if (addedEnts.Any())
sb.AppendLine("Listing new entities:");
foreach (var addedEnt in addedEnts)
{
sb.AppendLine(entMan.ToPrettyString(addedEnt));
}
if (removedEnts.Any())
sb.AppendLine("Listing removed entities:");
foreach (var removedEnt in removedEnts)
{
sb.AppendLine("\t" + entMan.ToPrettyString(removedEnt));
}
return sb.ToString();
}
private static bool HasRequiredDataField(Component component)
{
foreach (var field in component.GetType().GetFields())
{
foreach (var attribute in field.GetCustomAttributes(true))
{
if (attribute is not DataFieldAttribute dataField)
continue;
if (dataField.Required)
return true;
}
}
foreach (var property in component.GetType().GetProperties())
{
foreach (var attribute in property.GetCustomAttributes(true))
{
if (attribute is not DataFieldAttribute dataField)
continue;
if (dataField.Required)
return true;
}
}
return false;
}
[Test]
public async Task AllComponentsOneToOneDeleteTest()
{
@@ -373,9 +404,6 @@ namespace Content.IntegrationTests.Tests
"GridSpawner", // mono - i wouldn't
};
// TODO TESTS
// auto ignore any components that have a "required" data field.
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entityManager = server.ResolveDependency<IEntityManager>();
@@ -393,9 +421,12 @@ namespace Content.IntegrationTests.Tests
foreach (var type in componentFactory.AllRegisteredTypes)
{
var component = (Component) componentFactory.GetComponent(type);
var component = (Component)componentFactory.GetComponent(type);
var name = componentFactory.GetComponentName(type);
if (HasRequiredDataField(component))
continue;
// If this component is ignored
if (skipComponents.Contains(name))
{
@@ -432,9 +463,9 @@ namespace Content.IntegrationTests.Tests
private EntityUid SpawnEntity(
IEntityManager entManager,
string? protoName,
string protoName,
MapCoordinates coordinates,
ComponentRegistry? registry = null)
ComponentRegistry registry = null)
{
try
{
@@ -451,9 +482,9 @@ namespace Content.IntegrationTests.Tests
private EntityUid SpawnEntity(
IEntityManager entManager,
string? protoName,
string protoName,
EntityCoordinates coordinates,
ComponentRegistry? registry = null)
ComponentRegistry registry = null)
{
try
{
@@ -61,7 +61,7 @@ public sealed class StorageInteractionTest : InteractionTest
await ClickControl(ctrl, ContentKeyFunctions.ActivateItemInWorld);
await RunTicks(10);
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
// Assert.That(IsUiOpen(PdaUiKey.Key), Is.True); // Mono heisenfail - TODO: fix
// Click on the pda to pick it up and remove it from the backpack.
await ClickControl(ctrl, ContentKeyFunctions.MoveStoredItem);
@@ -71,7 +71,7 @@ public sealed class StorageInteractionTest : InteractionTest
// UIs should still be open
Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
// Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
}
/// <summary>
@@ -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(() =>
{
@@ -25,6 +25,11 @@ public sealed class AdminTest : ToolshedTest
if (ignored.Contains(cmd.Cmd.GetType().Assembly))
continue;
// Only care about content commands.
var assemblyName = cmd.Cmd.GetType().Assembly.FullName;
if (assemblyName == null || !assemblyName.StartsWith("Content."))
continue;
Assert.That(admin.TryGetCommandFlags(cmd, out _), $"Command does not have admin permissions set up: {cmd.FullName()}");
}
});
@@ -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(() =>
{
+10 -2
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>
@@ -38,7 +38,6 @@ namespace Content.MapRenderer.Painters
Fresh = true,
// Seriously whoever made MapPainter use GameMapPrototype I wish you step on a lego one time.
Map = map,
NoValidate = true, // Mono
});
await foreach (var image in RenderPair(stopwatch, pair))
@@ -56,7 +55,6 @@ namespace Content.MapRenderer.Painters
DummyTicker = false,
Connected = true,
Fresh = false,
NoValidate = true, // Mono
});
var server = pair.Server;
+4 -4
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>
+80
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]);
}
}
}
+30 -33
View File
@@ -19,12 +19,15 @@ public static class ServerPackaging
new PlatformReg("osx-x64", "MacOS", true),
new PlatformReg("osx-arm64", "MacOS", true),
// Non-default platforms (i.e. for Watchdog Git)
new PlatformReg("win-x86", "Windows", false),
new PlatformReg("linux-x86", "Linux", false),
new PlatformReg("linux-arm", "Linux", false),
new PlatformReg("freebsd-x64", "FreeBSD", false),
};
private static IReadOnlySet<string> ServerContentIgnoresResources { get; } = new HashSet<string>
{
"ServerInfo",
"Changelog",
};
private static List<string> PlatformRids => Platforms
.Select(o => o.Rid)
.ToList();
@@ -34,25 +37,9 @@ public static class ServerPackaging
.Select(o => o.Rid)
.ToList();
private static readonly List<string> ServerContentAssemblies = new()
{
"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",
"Discord",
};
private static readonly List<string> ServerNotExtraAssemblies = new()
{
"Microsoft.CodeAnalysis",
"JetBrains.Annotations",
};
private static readonly HashSet<string> BinSkipFolders = new()
@@ -178,23 +165,13 @@ public static class ServerPackaging
var inputPassCore = graph.InputCore;
var inputPassResources = graph.InputResources;
var contentAssemblies = new List<string>(ServerContentAssemblies);
// 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",
@@ -211,7 +188,11 @@ public static class ServerPackaging
contentAssemblies,
cancel: cancel);
await RobustServerPackaging.WriteServerResources(contentDir, inputPassResources, cancel);
await RobustServerPackaging.WriteServerResources(
contentDir,
inputPassResources,
ServerContentIgnoresResources.Concat(RobustSharedPackaging.SharedIgnoredResources).ToHashSet(),
cancel);
if (hybridAcz)
{
@@ -222,5 +203,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);
}
@@ -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>
+6 -9
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>nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
</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>
@@ -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>
@@ -25,7 +25,7 @@ public sealed class PanicBunkerCommand : LocalizedCommands
{
if (args.Length > 1)
{
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
shell.WriteError(Robust.Shared.Localization.Loc.GetString("shell-need-between-arguments", ("lower", 0), ("upper", 1)));
return null;
}
@@ -38,7 +38,7 @@ public sealed class PanicBunkerCommand : LocalizedCommands
if (args.Length == 1 && !bool.TryParse(args[0], out enabled))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-boolean"));
shell.WriteError(Robust.Shared.Localization.Loc.GetString("shell-argument-must-be-boolean"));
return null;
}
@@ -27,8 +27,6 @@ public sealed partial class CargoSystem
{
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[ValidatePrototypeId<NameIdentifierGroupPrototype>]
private const string BountyNameIdentifierGroup = "Bounty";
@@ -310,10 +308,10 @@ public sealed partial class CargoSystem
/// <returns>true if <paramref name="entity"/> is a valid item for the bounty entry, otherwise false</returns>
public bool IsValidBountyEntry(EntityUid entity, CargoBountyItemEntry entry)
{
if (!_whitelistSys.IsValid(entry.Whitelist, entity))
if (!_whitelist.IsValid(entry.Whitelist, entity))
return false;
if (entry.Blacklist != null && _whitelistSys.IsValid(entry.Blacklist, entity))
if (entry.Blacklist != null && _whitelist.IsValid(entry.Blacklist, entity))
return false;
return true;
@@ -472,7 +470,7 @@ public sealed partial class CargoSystem
skipped
? CargoBountyHistoryData.BountyResult.Skipped
: CargoBountyHistoryData.BountyResult.Completed,
_gameTiming.CurTime,
_timing.CurTime,
actorName));
ent.Comp.Bounties.RemoveAt(i);
return true;
@@ -37,8 +37,6 @@ namespace Content.Server.Cargo.Systems
/// </summary>
private float _timer;
[Dependency] private readonly BankSystem _bankSystem = default!;
private void InitializeConsole()
{
SubscribeLocalEvent<CargoOrderConsoleComponent, CargoConsoleAddOrderMessage>(OnAddOrderMessage);
@@ -272,9 +270,9 @@ namespace Content.Server.Cargo.Systems
if (!float.IsFinite(taxCoeff) || taxCoeff <= 0.0f)
continue;
var tax = (int)Math.Floor(cost * taxCoeff);
_bankSystem.TrySectorDeposit(account, tax, LedgerEntryType.CargoTax);
_bank.TrySectorDeposit(account, tax, LedgerEntryType.CargoTax);
}
_bankSystem.TryBankWithdraw(player, cost);
_bank.TryBankWithdraw(player, cost);
// End Frontier
UpdateOrders(station.Value);
@@ -25,14 +25,10 @@ namespace Content.Server.Cargo.Systems;
public sealed partial class CargoSystem
{
[Dependency] BankSystem _bank = default!; // Mono
/*
* Handles cargo shuttle / trade mechanics.
*/
[Dependency] EntityWhitelistSystem _whitelist = default!; // Frontier
// Frontier addition:
// The maximum distance from the console to look for pallets.
private const int DefaultPalletDistance = 8;
+4 -4
View File
@@ -19,8 +19,8 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Random;
using Content.Server._NF.SectorServices; // Frontier
using Content.Shared._NF.Trade; // Frontier
using Content.Shared.Whitelist; // Frontier
using Content.Shared.Whitelist;
using Content.Server._NF.Bank; // Frontier
namespace Content.Server.Cargo.Systems;
@@ -44,9 +44,9 @@ public sealed partial class CargoSystem : SharedCargoSystem
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly RadioSystem _radio = default!;
[Dependency] private readonly SectorServiceSystem _sectorService = default!; // Frontier
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; // Frontier
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!; // Frontier
[Dependency] private readonly BankSystem _bank = default!;
private EntityQuery<TransformComponent> _xformQuery;
private EntityQuery<CargoSellBlacklistComponent> _blacklistQuery;
+9 -10
View File
@@ -1,31 +1,30 @@
<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>nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="Discord.Net" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<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" />
</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>
+6 -3
View File
@@ -641,7 +641,7 @@ namespace Content.Server.Database
// This allows us to semi-efficiently load all entities we need in a single DB query.
// Then we can update & insert without further round-trips to the DB.
var players = updates.Select(u => u.User.UserId).Distinct().ToArray();
var players = updates.Select(u => u.User.UserId).Distinct().ToList();
var dbTimes = (await db.DbContext.PlayTime
.Where(p => players.Contains(p.PlayerId))
.ToArrayAsync())
@@ -875,8 +875,10 @@ namespace Content.Server.Database
{
await using var db = await GetDb();
var playerIdsList = playerIds.ToList();
var players = await db.DbContext.Player
.Where(player => playerIds.Contains(player.UserId))
.Where(player => playerIdsList.Contains(player.UserId))
.ToListAsync();
var round = new Round
@@ -907,10 +909,11 @@ namespace Content.Server.Database
public async Task AddRoundPlayers(int id, Guid[] playerIds)
{
await using var db = await GetDb();
var playerIdsList = playerIds.ToList();
// ReSharper disable once SuggestVarOrType_Elsewhere
Dictionary<Guid, int> players = await db.DbContext.Player
.Where(player => playerIds.Contains(player.UserId))
.Where(player => playerIdsList.Contains(player.UserId))
.ToDictionaryAsync(player => player.UserId, player => player.Id);
foreach (var player in playerIds)
@@ -3,7 +3,6 @@ using Content.Server.Botany.Components;
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Serilog;
namespace Content.Server.EntityEffects.Effects;
@@ -12,9 +11,11 @@ namespace Content.Server.EntityEffects.Effects;
/// </summary>
public sealed partial class PlantSpeciesChange : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var log = IoCManager.Resolve<ILogManager>().GetSawmill(nameof(PlantSpeciesChange));
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
@@ -29,7 +30,7 @@ public sealed partial class PlantSpeciesChange : EntityEffect
if (protoSeed == null)
{
Log.Error($"Seed prototype could not be found: {targetProto}!");
log.Error($"Seed prototype could not be found: {targetProto}!");
return;
}
@@ -48,7 +48,6 @@ namespace Content.Server.Forensics
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly StackSystem _stackSystem = default!; // Frontier
[Dependency] private readonly SharedAudioSystem _audio = default!; // Frontier
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; // Frontier
[Dependency] private readonly RadioSystem _radio = default!; // Frontier
[Dependency] private readonly DeadDropSystem _deadDrop = default!; // Frontier
@@ -100,7 +99,7 @@ namespace Content.Server.Forensics
private void GiveReward(EntityUid uidOrigin, EntityUid target, int spesoAmount, FixedPoint2 fmcAmount, string msg)
{
SoundSpecifier confirmSound = new SoundPathSpecifier("/Audio/Effects/Cargo/ping.ogg");
_audio.PlayPvs(_audio.GetSound(confirmSound), uidOrigin);
_audioSystem.PlayPvs(_audioSystem.GetSound(confirmSound), uidOrigin);
if (spesoAmount > 0)
_bank.TrySectorDeposit(SectorBankAccount.Nfsd, spesoAmount, LedgerEntryType.AntiSmugglingBonus);
+1 -2
View File
@@ -33,7 +33,6 @@ public sealed class CrematoriumSystem : EntitySystem
[Dependency] private readonly SharedMindSystem _minds = default!;
[Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly MobStateSystem _mobState = default!; // Frontier
[Dependency] private readonly SharedMindSystem _mind = default!; // frontier
public override void Initialize()
{
@@ -128,7 +127,7 @@ public sealed class CrematoriumSystem : EntitySystem
return false;
if (TryComp<MobStateComponent>(entity, out var comp) && !_mobState.IsDead(entity, comp))
return false;
if (_mind.TryGetMind(entity, out var _, out var mind) && mind.Session?.State?.Status == SessionStatus.InGame)
if (_minds.TryGetMind(entity, out var _, out var mind) && mind.Session?.State?.Status == SessionStatus.InGame)
return false;
return Cremate(uid, component, storage);
@@ -13,7 +13,6 @@ using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Serilog;
namespace Content.Server.Players.JobWhitelist;
@@ -25,6 +24,9 @@ public sealed class JobWhitelistManager : IPostInjectInit
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly UserDbDataManager _userDb = default!;
[Dependency] private readonly ILogManager _log = default!;
private readonly ISawmill _sawmill = default!;
private readonly Dictionary<NetUserId, HashSet<string>> _whitelists = new();
private readonly Dictionary<NetUserId, bool> _globalWhitelists = new(); // Frontier
@@ -33,6 +35,8 @@ public sealed class JobWhitelistManager : IPostInjectInit
{
_net.RegisterNetMessage<MsgJobWhitelist>();
_net.RegisterNetMessage<MsgWhitelist>();
_log.GetSawmill(nameof(JobWhitelistManager));
}
private async Task LoadData(ICommonSession session, CancellationToken cancel)
@@ -93,7 +97,7 @@ public sealed class JobWhitelistManager : IPostInjectInit
if (!_whitelists.TryGetValue(player, out var whitelists) || // Frontier: added globalWhitelist check
!_globalWhitelists.TryGetValue(player, out var globalWhitelist)) // Frontier
{
Log.Error("Unable to check if player {Player} is whitelisted for {Job}. Stack trace:\\n{StackTrace}",
_sawmill.Error("Unable to check if player {Player} is whitelisted for {Job}. Stack trace:\\n{StackTrace}",
player,
job,
Environment.StackTrace);
@@ -153,7 +157,7 @@ public sealed class JobWhitelistManager : IPostInjectInit
if (!_whitelists.TryGetValue(player, out var whitelists) ||
!_globalWhitelists.TryGetValue(player, out var globalWhitelist))
{
Log.Error("Unable to check if player {Player} is whitelisted for {GhostRole}. Stack trace:\\n{StackTrace}",
_sawmill.Error("Unable to check if player {Player} is whitelisted for {GhostRole}. Stack trace:\\n{StackTrace}",
player,
ghostRole,
Environment.StackTrace);
@@ -190,7 +194,7 @@ public sealed class JobWhitelistManager : IPostInjectInit
if (!_globalWhitelists.TryGetValue(player, out var whitelist))
{
Log.Error("Unable to check if player {Player} is globally whitelisted. Stack trace:\\n{StackTrace}",
_sawmill.Error("Unable to check if player {Player} is globally whitelisted. Stack trace:\\n{StackTrace}",
player,
Environment.StackTrace);
return false;
@@ -0,0 +1,5 @@
using Content.Shared.Sprite;
namespace Content.Server.Sprite;
public sealed class ScaleVisualsSystem : SharedScaleVisualsSystem;
@@ -19,7 +19,6 @@ public sealed partial class StationJobsSystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IBanManager _banManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; // Frontier
[Dependency] private readonly PlayTimeTrackingSystem _playTime = default!; // Frontier
private Dictionary<int, HashSet<string>> _jobsByWeight = default!;
@@ -303,7 +302,7 @@ public sealed partial class StationJobsSystem
_random.Shuffle(givenStations);
// Frontier: get player session
_playerManager.TryGetSessionById(player, out var nfSession);
_player.TryGetSessionById(player, out var nfSession);
// End Frontier
foreach (var station in givenStations)
@@ -257,7 +257,7 @@ public sealed partial class StationJobsSystem : EntitySystem
// Enforce the MaxJobSlots limit
var clampedAmount = Math.Min(amount, MaxJobSlots);
var jobList = stationJobs.JobList;
switch (jobList.ContainsKey(jobPrototypeId))
@@ -537,7 +537,7 @@ public sealed partial class StationJobsSystem : EntitySystem
stationDisplay = new StationDisplayInformation(
stationSubtext: extraStationInformation.StationSubtext,
stationDescription: extraStationInformation.StationDescription,
stationIcon: extraStationInformation.IconPath,
stationIcon: extraStationInformation.Icon,
lobbySortOrder: extraStationInformation.LobbySortOrder
);
}
@@ -0,0 +1,69 @@
using System.Numerics;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Sprite;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Toolshed;
namespace Content.Server.Toolshed.Commands.Misc;
/// <summary>
/// Used to change an entity's sprite scale.
/// </summary>
[ToolshedCommand, AdminCommand(AdminFlags.VarEdit)]
public sealed class ScaleCommand : ToolshedCommand
{
private SharedScaleVisualsSystem? _scaleVisuals;
private SharedPhysicsSystem? _physics;
[CommandImplementation("set")]
public IEnumerable<EntityUid> Set([PipedArgument] IEnumerable<EntityUid> input, Vector2 scale)
{
_scaleVisuals ??= GetSys<SharedScaleVisualsSystem>();
foreach (var ent in input)
{
_scaleVisuals.SetSpriteScale(ent, scale);
yield return ent;
}
}
[CommandImplementation("multiply")]
public IEnumerable<EntityUid> Multiply([PipedArgument] IEnumerable<EntityUid> input, float factor)
{
_scaleVisuals ??= GetSys<SharedScaleVisualsSystem>();
foreach (var ent in input)
{
var scale = _scaleVisuals.GetSpriteScale(ent) * factor;
_scaleVisuals.SetSpriteScale(ent, scale);
yield return ent;
}
}
[CommandImplementation("multiplywithfixture")]
public IEnumerable<EntityUid> MultiplyWithFixture([PipedArgument] IEnumerable<EntityUid> input, float factor)
{
_scaleVisuals ??= GetSys<SharedScaleVisualsSystem>();
_physics ??= GetSys<SharedPhysicsSystem>();
foreach (var ent in input)
{
var scale = _scaleVisuals.GetSpriteScale(ent) * factor;
_scaleVisuals.SetSpriteScale(ent, scale);
_physics.ScaleFixtures(ent, factor);
yield return ent;
}
}
[CommandImplementation("get")]
public IEnumerable<Vector2> Get([PipedArgument] IEnumerable<EntityUid> input)
{
_scaleVisuals ??= GetSys<SharedScaleVisualsSystem>();
foreach (var ent in input)
{
yield return _scaleVisuals.GetSpriteScale(ent);
}
}
}
@@ -6,36 +6,36 @@ namespace Content.Server.Worldgen.Prototypes;
/// This is a prototype for a GC queue.
/// </summary>
[Prototype("gcQueue")]
public sealed class GCQueuePrototype : IPrototype
public sealed partial class GCQueuePrototype : IPrototype
{
/// <inheritdoc />
[IdDataField]
public string ID { get; } = default!;
public string ID { get; private set; } = default!;
/// <summary>
/// How deep the GC queue is at most. If this value is ever exceeded entities get processed automatically regardless of
/// tick-time cap.
/// </summary>
[DataField("depth", required: true)]
public int Depth { get; }
public int Depth { get; private set; }
/// <summary>
/// How many miliseconds to spend deleting objects per object in the queue above the MinDepth? Mono Dynamic Queueing
/// </summary>
[DataField]
public double TimeDeletePerObject { get; } = 0.1; // Mono - at 100 objects past the MinDepth will spend up to 10 milliseconds trying to do deletions
public double TimeDeletePerObject { get; private set; } = 0.1; // Mono - at 100 objects past the MinDepth will spend up to 10 milliseconds trying to do deletions
/// <summary>
/// The minimum depth before entities in the queue actually get processed for deletion.
/// </summary>
[DataField("minDepthToProcess", required: true)]
public int MinDepthToProcess { get; }
public int MinDepthToProcess { get; private set; }
/// <summary>
/// Whether or not the GC should fire an event on the entity to see if it's eligible to skip the queue.
/// Useful for making it so only objects a player has actually interacted with get put in the collection queue.
/// </summary>
[DataField("trySkipQueue")]
public bool TrySkipQueue { get; }
public bool TrySkipQueue { get; private set; }
}
@@ -95,7 +95,7 @@ namespace Content.Server._NF.M_Emp
}
[CopyByRef, DataRecord]
public record struct GeneratorState(GeneratorStateType StateType, TimeSpan Until)
public partial record struct GeneratorState(GeneratorStateType StateType, TimeSpan Until)
{
public static readonly GeneratorState Inactive = new (GeneratorStateType.Inactive, TimeSpan.Zero);
};
@@ -71,7 +71,6 @@ public sealed partial class ShipyardSystem : SharedShipyardSystem
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly EntityManager _entityManager = default!;
[Dependency] private readonly ShuttleRecordsSystem _shuttleRecordsSystem = default!;
[Dependency] private readonly ShuttleConsoleLockSystem _shuttleConsoleLock = default!;
@@ -5,6 +5,7 @@ using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Systems;
using Content.Shared._NF.SizeAttribute;
using Content.Shared.Nyanotrasen.Item.PseudoItem;
using Content.Shared.Sprite;
namespace Content.Server.SizeAttribute
{
@@ -7,7 +7,6 @@ namespace Content.Server._NF.Speech.Components;
public sealed partial class CavemanAccentComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("MaxWordLength")]
public static int MaxWordLength = 5; // so man not talk smart, any word up dis be gone
[ViewVariables]
@@ -6,7 +6,7 @@ namespace Content.Server._NF.Station.Components;
public sealed partial class ExtraStationInformationComponent : Component
{
[DataField]
public ResPath? IconPath;
public SpriteSpecifier? Icon;
[DataField]
public LocId? StationSubtext;
@@ -126,7 +126,7 @@ public enum BluespaceDatasetNameType
}
[DataRecord]
public sealed class BluespaceDungeonSpawnGroup : IBluespaceSpawnGroup
public sealed partial class BluespaceDungeonSpawnGroup : IBluespaceSpawnGroup
{
/// <summary>
/// Prototypes we can choose from to spawn.
@@ -169,7 +169,7 @@ public sealed class BluespaceDungeonSpawnGroup : IBluespaceSpawnGroup
}
[DataRecord]
public sealed class BluespaceGridSpawnGroup : IBluespaceSpawnGroup
public sealed partial class BluespaceGridSpawnGroup : IBluespaceSpawnGroup
{
public List<ResPath> Paths = new();
@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
</Project>
@@ -4,7 +4,7 @@ using Content.Shared.Atmos.Piping.Binary.Components; // Frontier
namespace Content.Shared.Atmos.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
public sealed partial class GasPressurePumpComponent : Component
{
[DataField, AutoNetworkedField]
@@ -11,7 +11,7 @@ namespace Content.Shared.Audio;
[Prototype]
public sealed partial class AmbientMusicPrototype : IPrototype
{
[IdDataField] public string ID { get; } = string.Empty;
[IdDataField] public string ID { get; private set; } = string.Empty;
[ViewVariables(VVAccess.ReadWrite), DataField("sound", required: true)]
public SoundSpecifier Sound = default!;
@@ -48,7 +48,6 @@ public partial class SharedBodySystem
[Dependency] private readonly GibbingSystem _gibbingSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly ItemSlotsSystem _slots = default!; // Shitmed Change
[Dependency] private readonly IGameTiming _gameTiming = default!; // Shitmed Change
private const float GibletLaunchImpulse = 8;
private const float GibletLaunchImpulseVariance = 3;
@@ -26,7 +26,6 @@ namespace Content.Shared.Body.Systems;
public partial class SharedBodySystem
{
[Dependency] private readonly RandomHelperSystem _randomHelper = default!; // Shitmed Change
[Dependency] private readonly InventorySystem _inventorySystem = default!; // Shitmed Change
private void InitializeParts()
{
@@ -135,7 +134,7 @@ public partial class SharedBodySystem
&& TryGetPartSlotContainerName(partEnt.Comp.PartType, out var containerNames))
{
foreach (var containerName in containerNames)
_inventorySystem.DropSlotContents(partEnt.Comp.Body.Value, containerName, inventory);
_inventory.DropSlotContents(partEnt.Comp.Body.Value, containerName, inventory);
}
}
@@ -205,7 +204,7 @@ public partial class SharedBodySystem
// I don't know if this can cause issues, since any part that's being detached HAS to have a Body.
// though I really just want the compiler to shut the fuck up.
var body = partEnt.Comp.Body.GetValueOrDefault();
if (TryComp(partEnt, out TransformComponent? transform) && _gameTiming.IsFirstTimePredicted)
if (TryComp(partEnt, out TransformComponent? transform) && _timing.IsFirstTimePredicted)
{
var enableEvent = new BodyPartEnableChangedEvent(false);
RaiseLocalEvent(partEnt, ref enableEvent);
@@ -13,7 +13,7 @@ namespace Content.Shared.Configurable
/// <remarks>
/// If you want a more detailed description ask the original coder.
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
public sealed partial class ConfigurationComponent : Component
{
/// <summary>
+8 -15
View File
@@ -1,28 +1,21 @@
<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>
<WarningsAsErrors>nullable</WarningsAsErrors>
<Nullable>enable</Nullable>
<WarningsAsErrors>RA0032;nullable</WarningsAsErrors>
</PropertyGroup>
<Import Project="../MSBuild/Content.props" />
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="YamlDotNet" />
</ItemGroup>
<Import Project="..\RobustToolbox\Imports\Lidgren.props" />
<Import Project="..\RobustToolbox\Imports\Shared.props" />
<ItemGroup>
<ProjectReference Include="..\Content.Shared.Database\Content.Shared.Database.csproj" />
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj">
<Private>false</Private>
</ProjectReference>
<ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj">
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
<Import Project="..\RobustToolbox\MSBuild\Robust.CompNetworkGenerator.targets" />
</Project>
@@ -4,7 +4,7 @@ namespace Content.Shared.Follower.Components;
[RegisterComponent]
[Access(typeof(FollowerSystem))]
[NetworkedComponent, AutoGenerateComponentState(RaiseAfterAutoHandleState = true)]
[NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
public sealed partial class FollowerComponent : Component
{
[AutoNetworkedField, DataField("following")]
@@ -179,13 +179,13 @@ namespace Content.Shared.GameTicking
public sealed class StationDisplayInformation(
LocId? stationSubtext,
LocId? stationDescription,
ResPath? stationIcon,
SpriteSpecifier? stationIcon,
int lobbySortOrder
)
{
public LocId? StationSubtext { get; } = stationSubtext;
public LocId? StationDescription { get; } = stationDescription;
public ResPath? StationIcon { get; } = stationIcon;
public SpriteSpecifier? StationIcon { get; } = stationIcon;
public int LobbySortOrder { get; } = lobbySortOrder;
}
@@ -17,6 +17,7 @@ using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
using Content.Shared.Sprite;
namespace Content.Shared.Humanoid;
+1 -2
View File
@@ -1,5 +1,4 @@
using System.Security.Cryptography;
using Microsoft.VisualBasic.CompilerServices;
using System.Numerics;
namespace Content.Shared.Humanoid;
@@ -8,7 +8,7 @@ namespace Content.Shared.Nyanotrasen.Kitchen.Prototypes;
[Prototype("crispinessLevelSet")]
public sealed partial class CrispinessLevelSetPrototype : IPrototype
{
[IdDataField] public string ID { get; } = default!;
[IdDataField] public string ID { get; private set; } = default!;
/// <summary>
/// Crispiness level strings. The index is the crispiness value used, starting with 0.
@@ -8,7 +8,7 @@ namespace Content.Shared.Overlays;
/// <summary>
/// This component allows you to see health bars above damageable mobs.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] // Shitmed Change
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] // Shitmed Change
public sealed partial class ShowHealthBarsComponent : Component
{
/// <summary>
@@ -7,7 +7,7 @@ namespace Content.Shared.Overlays;
/// <summary>
/// This component allows you to see health status icons above damageable mobs.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] // Shitmed Change
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] // Shitmed Change
public sealed partial class ShowHealthIconsComponent : Component
{
/// <summary>
@@ -40,7 +40,6 @@ public abstract partial class SharedProjectileSystem : EntitySystem
{
public const string ProjectileFixture = "projectile";
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
@@ -366,7 +365,7 @@ public abstract partial class SharedProjectileSystem : EntitySystem
private void OnEmbedRemove(Entity<EmbeddableProjectileComponent> embeddable, ref RemoveEmbeddedProjectileEvent args)
{
// Whacky prediction issues.
if (args.Cancelled || _netManager.IsClient)
if (args.Cancelled || _net.IsClient)
return;
EmbedDetach(embeddable, embeddable.Comp, args.User);
+1 -1
View File
@@ -106,7 +106,7 @@ namespace Content.Shared.Roles
/// Nyano/DV: For e.g. prisoners, they'll never use their latejoin spawner.
/// </summary>
[DataField("alwaysUseSpawner")]
public bool AlwaysUseSpawner { get; } = false;
public bool AlwaysUseSpawner { get; private set; } = false;
/// <summary>
/// The "weight" or importance of this job. If this number is large, the job system will assign this job

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