Merge remote-tracking branch 'corvax/master' into upstream28.04

This commit is contained in:
Charlotte Tezuka
2026-04-28 18:42:55 +02:00
1245 changed files with 26764 additions and 21814 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
/Resources/Maps/_WL/** @0leshe
# Sprites
/Resources/Textures/** @Morb0 @DIMMoon1 @SonicHDC
/Resources/Textures/** @Morb0 @DIMMoon1 @MureixloI
+33 -1
View File
@@ -33,13 +33,21 @@ jobs:
- 'Content.Shared/**'
- 'Content.Server/**'
- 'Content.Client/**'
- 'Resources/**'
- 'Resources/Prototypes/**'
- 'RobustToolbox/**'
prototypes:
- '.github/workflows/update-wiki.yml'
- 'Resources/Prototypes/**'
textures:
- '.github/workflows/update-wiki.yml'
- 'Resources/Textures/**'
locale:
- '.github/workflows/update-wiki.yml'
- 'Resources/Locale/**'
- name: Setup Submodule
run: |
git submodule update --init --recursive
@@ -67,6 +75,30 @@ jobs:
run: dotnet ./bin/Content.Server/Content.Server.dll --cvar autogen.destination_file=prototypes.json
continue-on-error: true
- name: Upload loc to wiki
if: ${{ github.event_name == 'workflow_dispatch' || steps.changes.outputs.locale == 'true' }}
continue-on-error: true
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/loc.json
edit_summary: Update loc.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/loc.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
- name: Update meta license
if: ${{ github.event_name == 'workflow_dispatch' || steps.changes.outputs.textures == 'true' }}
continue-on-error: true
uses: jtmullen/mediawiki-edit-action@v0.1.1
with:
wiki_text_file: ./bin/Content.Server/data/meta_license.json
edit_summary: Update meta_license.json via GitHub Actions
page_name: "${{ secrets.WIKI_PAGE_ROOT }}/meta_license.json"
api_url: ${{ secrets.WIKI_ROOT_URL }}/api.php
username: ${{ secrets.WIKI_BOT_USER }}
password: ${{ secrets.WIKI_BOT_PASS }}
# Проходит по всем JSON-файлам в директории BASE и загружает каждый файл как страницу в MediaWiki.
# Имя страницы формируется из относительного пути к файлу.
- name: Upload JSON files to wiki
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@@ -44,7 +45,7 @@ public class ComponentQueryBenchmark
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup(typeof(QueryBenchSystem).Assembly);
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult();
_entMan = _pair.Server.ResolveDependency<IEntityManager>();
_itemQuery = _entMan.GetEntityQuery<ItemComponent>();
+2 -1
View File
@@ -1,3 +1,4 @@
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
@@ -68,7 +69,7 @@ public class DeltaPressureBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
var server = _pair.Server;
var mapdata = await _pair.CreateTestMap();
+2 -1
View File
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
@@ -69,7 +70,7 @@ public class DestructibleBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
var server = _pair.Server;
_entMan = server.ResolveDependency<IEntityManager>();
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
@@ -60,7 +61,7 @@ public class DeviceNetworkingBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup(typeof(DeviceNetworkingBenchmark).Assembly);
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
var server = _pair.Server;
await server.WaitPost(() =>
+2 -1
View File
@@ -1,3 +1,4 @@
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
@@ -51,7 +52,7 @@ public class GasReactionBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
var server = _pair.Server;
// Create test map and grid
+3 -2
View File
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
@@ -27,7 +28,7 @@ public class HeatCapacityBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
await _pair.Connect();
_cEntMan = _pair.Client.ResolveDependency<IEntityManager>();
_sEntMan = _pair.Server.ResolveDependency<IEntityManager>();
+2 -1
View File
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@@ -29,7 +30,7 @@ public class MapLoadBenchmark
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult();
var server = _pair.Server;
Paths = server.ResolveDependency<IPrototypeManager>()
+2 -1
View File
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@@ -50,7 +51,7 @@ public class PvsBenchmark
#endif
PoolManager.Startup();
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult();
_entMan = _pair.Server.ResolveDependency<IEntityManager>();
_pair.Server.CfgMan.SetCVar(CVars.NetPVS, true);
_pair.Server.CfgMan.SetCVar(CVars.ThreadParallelCount, 0);
+2 -1
View File
@@ -1,4 +1,5 @@
#nullable enable
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
@@ -21,7 +22,7 @@ public class RaiseEventBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup(typeof(BenchSystem).Assembly);
_pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
_pair = PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).GetAwaiter().GetResult();
var entMan = _pair.Server.EntMan;
var fact = _pair.Server.ResolveDependency<IComponentFactory>();
var bus = (EntityEventBus)entMan.EventBus;
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.IO;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
@@ -36,7 +37,7 @@ public class SpawnEquipDeleteBenchmark
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
_pair = await PoolManager.GetServerClient(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null));
var server = _pair.Server;
var mapData = await _pair.CreateTestMap();
@@ -65,7 +65,7 @@ namespace Content.Client.Access.UI
_window?.UpdateState(castState);
}
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, ProtoId<JobPrototype> newJobPrototype)
public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, ProtoId<JobPrototype>? newJobPrototype)
{
SendMessage(new WriteToTargetIdMessage(
newFullName,
@@ -216,7 +216,7 @@ namespace Content.Client.Access.UI
JobTitleLineEdit.Text,
// Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
_accessButtons.ButtonsList.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(),
jobProtoDirty ? _jobPrototypeIds[JobPresetOptionButton.SelectedId] : string.Empty);
jobProtoDirty ? _jobPrototypeIds[JobPresetOptionButton.SelectedId] : null);
}
}
}
@@ -0,0 +1,36 @@
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
namespace Content.Client.Atmos.Components;
/// <summary>
/// This listens to appearance changes from <see cref="GasMaxPressureSystem{T}"/>
/// and applies sprite changes to a gas holder currently experiencing <see cref="IGasMaxPressureHolder.Integrity"/> loss.
/// </summary>
[RegisterComponent]
public sealed partial class MaxPressureVisualsComponent : Component
{
/// <summary>
/// What RsiState we use for our integrity visuals.
/// </summary>
[DataField]
public string? IntegrityState = "integrity";
/// <summary>
/// What RsiState we use for the mask that goes over integrity visuals.
/// </summary>
[DataField]
public string? IntegrityMask = "mask";
/// <summary>
/// How many steps there are
/// </summary>
[DataField("steps")]
public int IntegritySteps = 5;
}
public enum MaxPressureVisualLayers : byte
{
Base,
BaseUnshaded,
}
@@ -1,6 +1,7 @@
using Content.Client.Stylesheets;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Monitor;
using Content.Shared.FixedPoint;
using Content.Shared.Temperature;
@@ -22,6 +23,7 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
private readonly IEntityManager _entManager;
private readonly IResourceCache _cache;
private readonly SharedAtmosphereSystem _atmosphere;
private Dictionary<AtmosAlarmType, string> _alarmStrings = new Dictionary<AtmosAlarmType, string>()
{
@@ -37,6 +39,7 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
_entManager = IoCManager.Resolve<IEntityManager>();
_cache = IoCManager.Resolve<IResourceCache>();
_atmosphere = _entManager.System<SharedAtmosphereSystem>();
NetEntity = uid;
Coordinates = coordinates;
@@ -149,7 +152,7 @@ public sealed partial class AtmosAlarmEntryContainer : BoxContainer
foreach ((var gas, (var mol, var percent, var alert)) in keyValuePairs)
{
FixedPoint2 gasPercent = percent * 100f;
var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
var gasAbbreviation = Loc.GetString(_atmosphere.GetGas(gas).Abbreviation);
var gasLabel = new Label()
{
@@ -1,6 +1,7 @@
using Content.Client.Stylesheets;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.FixedPoint;
using Content.Shared.Temperature;
using Robust.Client.AutoGenerated;
@@ -19,12 +20,14 @@ public sealed partial class AtmosMonitoringEntryContainer : BoxContainer
private readonly IEntityManager _entManager;
private readonly IResourceCache _cache;
private readonly SharedAtmosphereSystem _atmosphere;
public AtmosMonitoringEntryContainer(AtmosMonitoringConsoleEntry data)
{
RobustXamlLoader.Load(this);
_entManager = IoCManager.Resolve<IEntityManager>();
_cache = IoCManager.Resolve<IResourceCache>();
_atmosphere = _entManager.System<SharedAtmosphereSystem>();
Data = data;
@@ -132,7 +135,7 @@ public sealed partial class AtmosMonitoringEntryContainer : BoxContainer
var gasPercent = (FixedPoint2)0f;
gasPercent = percent * 100f;
var gasAbbreviation = Atmospherics.GasAbbreviations.GetValueOrDefault(gas, Loc.GetString("gas-unknown-abbreviation"));
var gasAbbreviation = Loc.GetString(_atmosphere.GetGas(gas).Abbreviation);
var gasLabel = new Label()
{
@@ -37,6 +37,20 @@ public sealed partial class AtmosphereSystem
return NumericsHelpers.HorizontalAdd(tmp) > epsilon;
}
public override float GetMass(GasMixture mix)
{
return GetMass(mix.Moles);
}
public override float GetMass(float[] moles)
{
var tmp = new float[moles.Length];
NumericsHelpers.Multiply(moles, GasMolarMasses, tmp);
// Conversion of grams to kilograms.
return NumericsHelpers.HorizontalAdd(tmp) * Atmospherics.gToKg;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override float GetHeatCapacityCalculation(float[] moles, bool space)
{
@@ -51,7 +65,7 @@ public sealed partial class AtmosphereSystem
// though this isnt the hottest code path so it should be fine
// the gc can eat a little as a treat
var tmp = new float[moles.Length];
NumericsHelpers.Multiply(moles, GasSpecificHeats, tmp);
NumericsHelpers.Multiply(moles, GasMolarHeatCapacities, tmp);
// Adjust heat capacity by speedup, because this is primarily what
// determines how quickly gases heat up/cool.
return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity);
@@ -11,6 +11,12 @@ public sealed class GasTankSystem : SharedGasTankSystem
SubscribeLocalEvent<GasTankComponent, AfterAutoHandleStateEvent>(OnGasTankState);
}
protected override void DeviceUpdated(Entity<GasTankComponent> entity, ref AtmosDeviceUpdateEvent args)
{
// Atmos not predicted :(
throw new NotImplementedException();
}
private void OnGasTankState(Entity<GasTankComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UI.TryGetOpenUi(ent.Owner, SharedGasTankUiKey.Key, out var bui))
@@ -0,0 +1,30 @@
using Content.Client.Atmos.Overlays;
using JetBrains.Annotations;
using Robust.Client.Graphics;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// System responsible for rendering heat distortion using <see cref="GasTileHeatBlurOverlay"/>.
/// </summary>
[UsedImplicitly]
public sealed class GasTileHeatBlurOverlaySystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
private GasTileHeatBlurOverlay _gasTileHeatBlurOverlay = default!;
public override void Initialize()
{
base.Initialize();
_gasTileHeatBlurOverlay = new GasTileHeatBlurOverlay();
_overlayMan.AddOverlay(_gasTileHeatBlurOverlay);
}
public override void Shutdown()
{
base.Shutdown();
_overlayMan.RemoveOverlay<GasTileHeatBlurOverlay>();
}
}
@@ -0,0 +1,77 @@
using Content.Client.Atmos.Components;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Rounding;
using Robust.Client.GameObjects;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// This system handles sprite changes for a <see cref="IGasMaxPressureHolder"/>
/// with a <see cref="MaxPressureVisualsComponent"/> when its <see cref="IGasMaxPressureHolder.Integrity"/> changes.
/// </summary>
public sealed class MaxPressureVisualsSystem : EntitySystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<MaxPressureVisualsComponent, ComponentInit>(OnMaxPressureInit);
SubscribeLocalEvent<MaxPressureVisualsComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnMaxPressureInit(Entity<MaxPressureVisualsComponent> entity, ref ComponentInit args)
{
if (!TryComp<SpriteComponent>(entity, out var sprite))
return;
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(entity.Comp.IntegritySteps);
if (_sprite.LayerMapTryGet((entity, sprite), MaxPressureVisualLayers.Base, out _, false))
{
_sprite.LayerSetRsiState((entity, sprite), MaxPressureVisualLayers.Base, $"{entity.Comp.IntegrityMask}");
_sprite.LayerSetVisible((entity, sprite), MaxPressureVisualLayers.Base, false);
}
if (_sprite.LayerMapTryGet((entity, sprite), MaxPressureVisualLayers.BaseUnshaded, out _, false))
{
_sprite.LayerSetRsiState((entity, sprite), MaxPressureVisualLayers.BaseUnshaded, $"{entity.Comp.IntegrityState}-unshaded-0");
_sprite.LayerSetVisible((entity, sprite), MaxPressureVisualLayers.BaseUnshaded, false);
}
}
private void OnAppearanceChange(Entity<MaxPressureVisualsComponent> entity, ref AppearanceChangeEvent args)
{
if (args.Sprite is not { } sprite)
return;
if (!args.AppearanceData.TryGetValue(GasIntegrity.Integrity, out var obj) || obj is not float integrity)
return;
if (!args.AppearanceData.TryGetValue(GasIntegrity.MaxIntegrity, out obj) || obj is not float maxIntegrity)
return;
// We don't want visuals at max integrity, so we return if we're at max.
if (integrity >= maxIntegrity)
{
_sprite.LayerSetVisible((entity, sprite), MaxPressureVisualLayers.Base, false);
_sprite.LayerSetVisible((entity, sprite), MaxPressureVisualLayers.BaseUnshaded, false);
return;
}
_sprite.LayerSetVisible((entity, sprite), MaxPressureVisualLayers.Base, true);
_sprite.LayerSetVisible((entity, sprite), MaxPressureVisualLayers.BaseUnshaded, true);
// Subtract our integrity + 1 to get an accurate step count.
if (entity.Comp.IntegritySteps > 1)
{
var step = ContentHelpers.RoundToEqualLevels(maxIntegrity - integrity - 1, maxIntegrity, entity.Comp.IntegritySteps);
_sprite.LayerSetRsiState((entity, sprite), MaxPressureVisualLayers.BaseUnshaded, $"{entity.Comp.IntegrityState}-unshaded-{step}");
}
else
{
_sprite.LayerSetRsiState((entity, sprite), MaxPressureVisualLayers.BaseUnshaded, $"{entity.Comp.IntegrityState}-unshaded-0");
}
}
}
@@ -0,0 +1,263 @@
using Content.Client.Atmos.EntitySystems;
using Content.Client.Graphics;
using Content.Client.Resources;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using System.Numerics;
using Color = Robust.Shared.Maths.Color;
using Texture = Robust.Client.Graphics.Texture;
namespace Content.Client.Atmos.Overlays;
/// <summary>
/// Overlay responsible for rendering heat distortion shader.
/// </summary>
public sealed class GasTileHeatBlurOverlay : Overlay
{
public override bool RequestScreenTexture { get; set; } = true;
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private static readonly ProtoId<ShaderPrototype> HeatOverlayShader = "HeatBlur";
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
private readonly SharedTransformSystem _xformSys;
private readonly ShaderInstance _shader;
private readonly Texture _noiseTexture;
private readonly Texture _heatGradientTexture;
private List<Entity<MapGridComponent>> _intersectingGrids = new();
private readonly OverlayResourceCache<CachedResources> _resources = new();
// Overlay settings
private const float
ShaderSpilling = 2.5f; // for example 4f - spills shader one tile from hotspot, 2.5f - spills it half tile
private const float ShaderStrength = 0.04f; // Makes waves stronger
private const float ShaderScale = 1f; // Makes more waves
private const float ShaderSpeed = 0.4f; // Makes waves run faster
// Overlay settings for reduced motion setting
private const float ShaderStrengthForReducedMotion = 0.01f;
private const float ShaderScaleReducedMotion = 0.5f;
private const float ShaderSpeedReducedMotion = 0.25f;
private const int MinDistortionTemp = 300; // Distortion starts to show up at this temperature in Kelvins
private const int MaxDistortionTemp = 2000; // Maximum distortion strength at this temperature in Kelvins
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public GasTileHeatBlurOverlay()
{
IoCManager.InjectDependencies(this);
_xformSys = _entManager.System<SharedTransformSystem>();
_noiseTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/perlin_noise.png");
_heatGradientTexture = _resourceCache.GetTexture("/Textures/Effects/HeatBlur/soft_circle.png");
_shader = _proto.Index(HeatOverlayShader).InstanceUnique();
_configManager.OnValueChanged(CCVars.ReducedMotion, SetReducedMotion, invokeImmediately: true);
}
private void SetReducedMotion(bool reducedMotion)
{
_shader.SetParameter("strength_scale", reducedMotion ? ShaderStrengthForReducedMotion : ShaderStrength);
_shader.SetParameter("spatial_scale", reducedMotion ? ShaderScaleReducedMotion : ShaderScale);
_shader.SetParameter("speed_scale", reducedMotion ? ShaderSpeedReducedMotion : ShaderSpeed);
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (args.MapId == MapId.Nullspace)
return false;
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
var target = args.Viewport.RenderTarget;
// Probably the resolution of the game window changed, remake the textures.
if (res.HeatTarget?.Texture.Size != target.Size)
{
res.HeatTarget?.Dispose();
res.HeatTarget = _clyde.CreateRenderTarget(
target.Size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: nameof(GasTileHeatBlurOverlaySystem));
}
if (res.HeatBlurTarget?.Texture.Size != target.Size)
{
res.HeatBlurTarget?.Dispose();
res.HeatBlurTarget = _clyde.CreateRenderTarget(
target.Size,
new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb),
name: $"{nameof(GasTileHeatBlurOverlaySystem)}-blur");
}
var overlayQuery = _entManager.GetEntityQuery<GasTileOverlayComponent>();
args.WorldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
var mapId = args.MapId;
var worldAABB = args.WorldAABB;
var worldBounds = args.WorldBounds;
var worldHandle = args.WorldHandle;
var worldToViewportLocal = args.Viewport.GetWorldToLocalMatrix();
// If there is no distortion after checking all visible tiles, we can bail early
var anyDistortion = false;
// We're rendering in the context of the heat target texture, which will encode data as to where and how strong
// the heat distortion will be
args.WorldHandle.RenderInRenderTarget(res.HeatTarget,
() =>
{
_intersectingGrids.Clear();
_mapManager.FindGridsIntersecting(mapId, worldAABB, ref _intersectingGrids);
foreach (var grid in _intersectingGrids)
{
if (!overlayQuery.TryGetComponent(grid.Owner, out var comp))
continue;
var gridEntToWorld = _xformSys.GetWorldMatrix(grid.Owner);
var gridEntToViewportLocal = gridEntToWorld * worldToViewportLocal;
if (!Matrix3x2.Invert(gridEntToViewportLocal, out var viewportLocalToGridEnt))
continue;
var uvToUi = Matrix3Helpers.CreateScale(res.HeatTarget.Size.X, -res.HeatTarget.Size.Y);
var uvToGridEnt = uvToUi * viewportLocalToGridEnt;
// Because we want the actual distortion to be calculated based on the grid coordinates*, we need
// to pass a matrix transformation to go from the viewport coordinates to grid coordinates.
// * (why? because otherwise the effect would shimmer like crazy as you moved around, think
// moving a piece of warped glass above a picture instead of placing the warped glass on the
// paper and moving them together)
_shader.SetParameter("grid_ent_from_viewport_local", uvToGridEnt);
// Draw commands (like DrawRect) will be using grid coordinates from here
worldHandle.SetTransform(gridEntToViewportLocal);
// We only care about tiles that fit in these bounds
var worldToGridLocal = _xformSys.GetInvWorldMatrix(grid.Owner);
var floatBounds = worldToGridLocal.TransformBox(worldBounds).Enlarged(grid.Comp.TileSize);
var localBounds = new Box2i(
(int)MathF.Floor(floatBounds.Left),
(int)MathF.Floor(floatBounds.Bottom),
(int)MathF.Ceiling(floatBounds.Right),
(int)MathF.Ceiling(floatBounds.Top));
// for each tile and its gas --->
foreach (var chunk in comp.Chunks.Values)
{
var enumerator = new GasChunkEnumerator(chunk);
while (enumerator.MoveNext(out var tileGas))
{
// Check and make sure the tile is within the viewport/screen
var tilePosition = chunk.Origin + (enumerator.X, enumerator.Y);
if (!localBounds.Contains(tilePosition))
continue;
// Get the distortion strength from the temperature and bail if it's not hot enough
var strength = GetHeatDistortionStrength(tileGas.ByteGasTemperature);
if (strength <= 0f)
continue;
anyDistortion = true;
// Encode the strength in the red channel
// alpha set to 1 as tile is active
worldHandle.DrawTextureRect(
_heatGradientTexture,
Box2.CenteredAround(tilePosition + grid.Comp.TileSizeHalfVector,
grid.Comp.TileSizeVector * ShaderSpilling),
new Color(strength, 0f, 0f));
}
}
}
},
// This clears the buffer to all zero first...
new Color(0, 0, 0, 0));
// no distortion, no need to render
if (!anyDistortion)
{
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
return false;
}
return true;
}
protected override void Draw(in OverlayDrawArgs args)
{
var res = _resources.GetForViewport(args.Viewport, static _ => new CachedResources());
if (ScreenTexture is null || res.HeatTarget is null || res.HeatBlurTarget is null)
return;
_shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_shader.SetParameter("NOISE_TEXTURE", _noiseTexture);
args.WorldHandle.UseShader(_shader);
args.WorldHandle.DrawTextureRect(res.HeatTarget.Texture, args.WorldBounds);
args.WorldHandle.UseShader(null);
args.WorldHandle.SetTransform(Matrix3x2.Identity);
}
protected override void DisposeBehavior()
{
_resources.Dispose();
_configManager.UnsubValueChanged(CCVars.ReducedMotion, SetReducedMotion);
base.DisposeBehavior();
}
/// <summary>
/// Gets the strength of the heat distortion effect based on the temperature of the tile.
/// The strength is a value between 0 and 1, where 0 means no distortion and 1 means maximum distortion.
/// </summary>
/// <param name="temp">The temperature of the tile.</param>
/// <returns>The strength of the heat distortion effect.</returns>
/// <seealso cref="ThermalByte"/>
private static float GetHeatDistortionStrength(ThermalByte temp)
{
if (!temp.TryGetTemperature(out var kelvinTemp))
{
return 0f;
}
var strength = (kelvinTemp - MinDistortionTemp) / (MaxDistortionTemp - MinDistortionTemp);
return MathHelper.Clamp01(strength);
}
internal sealed class CachedResources : IDisposable
{
public IRenderTexture? HeatTarget;
public IRenderTexture? HeatBlurTarget;
public void Dispose()
{
HeatTarget?.Dispose();
HeatBlurTarget?.Dispose();
}
}
}
@@ -72,17 +72,7 @@ public sealed class GasTileVisibleGasOverlay : Overlay
{
var gasPrototype = _atmosphereSystem.GetGas(_gasTileOverlaySystem.VisibleGasId[i]);
SpriteSpecifier overlay;
if (!string.IsNullOrEmpty(gasPrototype.GasOverlaySprite) &&
!string.IsNullOrEmpty(gasPrototype.GasOverlayState))
overlay = new SpriteSpecifier.Rsi(new(gasPrototype.GasOverlaySprite), gasPrototype.GasOverlayState);
else if (!string.IsNullOrEmpty(gasPrototype.GasOverlayTexture))
overlay = new SpriteSpecifier.Texture(new(gasPrototype.GasOverlayTexture));
else
continue;
switch (overlay)
switch (gasPrototype.GasOverlaySprite)
{
case SpriteSpecifier.Rsi animated:
var rsi = _resourceCache.GetResource<RSIResource>(animated.RsiPath).RSI;
@@ -1,4 +1,5 @@
using Content.Client.Atmos.UI;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Atmos.Piping.Unary.Systems;
@@ -14,6 +15,12 @@ public sealed class GasCanisterSystem : SharedGasCanisterSystem
SubscribeLocalEvent<GasCanisterComponent, AfterAutoHandleStateEvent>(OnGasState);
}
protected override void DeviceUpdated(Entity<GasCanisterComponent> entity, ref AtmosDeviceUpdateEvent args)
{
// Atmos not predicted :(
throw new NotImplementedException();
}
private void OnGasState(Entity<GasCanisterComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UI.TryGetOpenUi<GasCanisterBoundUserInterface>(ent.Owner, GasCanisterUiKey.Key, out var bui))
@@ -1,41 +1,33 @@
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
using Content.Shared.Atmos.Components;
namespace Content.Client.Atmos.UI
namespace Content.Client.Atmos.UI;
public sealed class GasAnalyzerBoundUserInterface : BoundUserInterface
{
public sealed class GasAnalyzerBoundUserInterface : BoundUserInterface
[ViewVariables]
private GasAnalyzerWindow? _window;
public GasAnalyzerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
[ViewVariables]
private GasAnalyzerWindow? _window;
}
public GasAnalyzerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
protected override void Open()
{
base.Open();
_window = this.CreateWindowCenteredLeft<GasAnalyzerWindow>();
_window.OnClose += Close;
}
_window = this.CreateWindowCenteredLeft<GasAnalyzerWindow>();
_window.OnClose += Close;
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
if (_window == null)
return;
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
if (_window == null)
return;
if (message is not GasAnalyzerUserMessage cast)
return;
_window.Populate(cast);
}
if (message is not GasAnalyzerUserMessage cast)
return;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
_window?.Dispose();
}
_window.Populate(cast);
}
}
@@ -1,6 +1,8 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Temperature;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -8,7 +10,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using static Content.Shared.Atmos.Components.GasAnalyzerComponent;
using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Atmos.UI
@@ -16,25 +17,17 @@ namespace Content.Client.Atmos.UI
[GenerateTypedNameReferences]
public sealed partial class GasAnalyzerWindow : DefaultWindow
{
private readonly SharedAtmosphereSystem _atmosphere;
private NetEntity _currentEntity = NetEntity.Invalid;
public GasAnalyzerWindow()
{
RobustXamlLoader.Load(this);
_atmosphere = IoCManager.Resolve<IEntityManager>().System<SharedAtmosphereSystem>();
}
public void Populate(GasAnalyzerUserMessage msg)
{
if (msg.Error != null)
{
CTopBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-error-text", ("errorText", msg.Error)),
FontColorOverride = Color.Red
});
return;
}
if (msg.NodeGasMixes.Length == 0)
{
CTopBox.AddChild(new Label
@@ -329,31 +322,31 @@ namespace Content.Client.Atmos.UI
for (var j = 0; j < gasMix.Gases.Length; j++)
{
var gas = gasMix.Gases[j];
var color = Color.FromHex($"#{gas.Color}", Color.White);
var gasEntry = gasMix.Gases[j];
var gasProto = _atmosphere.GetGas(gasEntry.Gas);
// Add to the table
tableKey.AddChild(new Label
{
Text = Loc.GetString(gas.Name)
Text = Loc.GetString(gasProto.Name)
});
tableVal.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-molarity-text",
("mol", $"{gas.Amount:0.00}")),
("mol", $"{gasEntry.Amount:0.00}")),
Align = Label.AlignMode.Right,
});
tablePercent.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-percentage-text",
("percentage", $"{(gas.Amount / totalGasAmount * 100):0.0}")),
("percentage", $"{(gasEntry.Amount / totalGasAmount * 100):0.0}")),
Align = Label.AlignMode.Right
});
// Add to the gas bar //TODO: highlight the currently hover one
gasBar.AddEntry(gas.Amount, color, tooltip: Loc.GetString("gas-analyzer-window-molarity-percentage-text",
("gasName", gas.Name),
("amount", $"{gas.Amount:0.##}"),
("percentage", $"{(gas.Amount / totalGasAmount * 100):0.#}")));
gasBar.AddEntry(gasEntry.Amount, gasProto.Color, tooltip: Loc.GetString("gas-analyzer-window-molarity-percentage-text",
("gasName", Loc.GetString(gasProto.Name)),
("amount", $"{gasEntry.Amount:0.##}"),
("percentage", $"{(gasEntry.Amount / totalGasAmount * 100):0.#}")));
}
dataContainer.AddChild(gasBar);
@@ -74,7 +74,7 @@ namespace Content.Client.Atmos.UI
_window.SetTankPressure(cast.TankPressure);
_window.SetReleasePressureRange(component.MinReleasePressure, component.MaxReleasePressure);
_window.SetReleasePressure(component.ReleasePressure);
_window.SetReleaseValve(component.ReleaseValve);
_window.SetReleaseValve(component.ReleaseValveOpen);
}
protected override void Dispose(bool disposing)
+3 -5
View File
@@ -235,8 +235,6 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
/// </summary>
private void ProcessNearbyAmbience(TransformComponent playerXform)
{
var query = GetEntityQuery<TransformComponent>();
var metaQuery = GetEntityQuery<MetaDataComponent>();
var mapPos = _xformSystem.GetMapCoordinates(playerXform);
// Remove out-of-range ambiences
@@ -249,9 +247,9 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
if (comp.Enabled &&
// Don't keep playing sounds that have changed since.
sound.Sound == comp.Sound &&
query.TryGetComponent(owner, out var xform) &&
TryComp(owner, out TransformComponent? xform) &&
xform.MapID == playerXform.MapID &&
!metaQuery.GetComponent(owner).EntityPaused)
!Paused(owner))
{
// TODO: This is just trydistance for coordinates.
var distance = (xform.ParentUid == playerXform.ParentUid)
@@ -294,7 +292,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
var comp = sourceEntity.Comp;
if (_playingSounds.ContainsKey(sourceEntity) ||
metaQuery.GetComponent(uid).EntityPaused)
Paused(uid))
continue;
var audioParams = _params
@@ -23,8 +23,7 @@ public sealed class AmbientSoundTreeSystem : ComponentTreeSystem<AmbientSoundTre
var pos = XformSystem.GetRelativePosition(
entry.Transform,
entry.Component.TreeUid.Value,
GetEntityQuery<TransformComponent>());
entry.Component.TreeUid.Value);
return ExtractAabb(in entry, pos, default);
}
+24 -4
View File
@@ -1,4 +1,5 @@
using System.Linq;
using Content.Client.DisplacementMap;
using Content.Shared.Body;
using Content.Shared.CCVar;
using Content.Shared.Humanoid.Markings;
@@ -15,6 +16,7 @@ public sealed class VisualBodySystem : SharedVisualBodySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly DisplacementMapSystem _displacement = default!;
[Dependency] private readonly MarkingManager _marking = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
@@ -167,8 +169,11 @@ public sealed class VisualBodySystem : SharedVisualBodySystem
}
}
private void ApplyMarkings(Entity<VisualOrganMarkingsComponent> ent, EntityUid target)
private void ApplyMarkings(Entity<VisualOrganMarkingsComponent> ent, Entity<SpriteComponent?> target)
{
if (!Resolve(target, ref target.Comp))
return;
var applied = new List<Marking>();
foreach (var marking in AllMarkings(ent))
{
@@ -178,6 +183,8 @@ public sealed class VisualBodySystem : SharedVisualBodySystem
if (!_sprite.LayerMapTryGet(target, proto.BodyPart, out var index, true))
continue;
ent.Comp.MarkingsDisplacement.TryGetValue(proto.BodyPart, out var displacement);
for (var i = 0; i < proto.Sprites.Count; i++)
{
var sprite = proto.Sprites[i];
@@ -190,8 +197,8 @@ public sealed class VisualBodySystem : SharedVisualBodySystem
if (!_sprite.LayerMapTryGet(target, layerId, out _, false))
{
var layer = _sprite.AddLayer(target, sprite, index + i + 1);
_sprite.LayerMapSet(target, layerId, layer);
var spriteLayer = _sprite.AddLayer(target, sprite, index + i + 1);
_sprite.LayerMapSet(target, layerId, spriteLayer);
_sprite.LayerSetSprite(target, layerId, rsi);
}
@@ -199,6 +206,9 @@ public sealed class VisualBodySystem : SharedVisualBodySystem
_sprite.LayerSetColor(target, layerId, marking.MarkingColors[i]);
else
_sprite.LayerSetColor(target, layerId, Color.White);
if (displacement != null && proto.CanBeDisplaced)
_displacement.TryAddDisplacement(displacement, (target, target.Comp), index + i + 1, layerId, out _);
}
applied.Add(marking);
@@ -206,8 +216,11 @@ public sealed class VisualBodySystem : SharedVisualBodySystem
ent.Comp.AppliedMarkings = applied;
}
private void RemoveMarkings(Entity<VisualOrganMarkingsComponent> ent, EntityUid target)
private void RemoveMarkings(Entity<VisualOrganMarkingsComponent> ent, Entity<SpriteComponent?> target)
{
if (!Resolve(target, ref target.Comp))
return;
foreach (var marking in ent.Comp.AppliedMarkings)
{
if (!_marking.TryGetMarking(marking, out var proto))
@@ -221,6 +234,13 @@ public sealed class VisualBodySystem : SharedVisualBodySystem
var layerId = $"{proto.ID}-{rsi.RsiState}";
// If this marking is one that can be displaced, we need to remove the displacement as well; otherwise
// altering a marking at runtime can lead to the renderer falling over.
// The Vulps must be shaved.
// (https://github.com/space-wizards/space-station-14/issues/40135).
if (proto.CanBeDisplaced)
_displacement.EnsureDisplacementIsNotOnSprite((target, target.Comp), layerId);
if (!_sprite.LayerMapTryGet(target, layerId, out var index, false))
continue;
@@ -14,15 +14,12 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
private EntityQuery<MobStateComponent> _mobStateQuery;
[Dependency] private readonly EntityQuery<MobStateComponent> _mobStateQuery = default!;
public override void Initialize()
{
base.Initialize();
_mobStateQuery = GetEntityQuery<MobStateComponent>();
SubscribeNetworkEvent<PlayBoxEffectMessage>(OnBoxEffect);
}
@@ -33,11 +30,7 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
if (!TryComp<CardboardBoxComponent>(source, out var box))
return;
var xformQuery = GetEntityQuery<TransformComponent>();
if (!xformQuery.TryGetComponent(source, out var xform))
return;
var xform = Transform(source);
var sourcePos = _transform.GetMapCoordinates(source, xform);
//Any mob that can move should be surprised?
@@ -72,7 +65,7 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
var ent = Spawn(box.Effect, mapPos);
if (!xformQuery.TryGetComponent(ent, out var entTransform) || !TryComp<SpriteComponent>(ent, out var sprite))
if (!TryComp(ent, out TransformComponent? entTransform) || !TryComp<SpriteComponent>(ent, out var sprite))
continue;
_sprite.SetOffset((ent, sprite), new Vector2(0, 1));
@@ -4,6 +4,7 @@ using Content.Shared.Changeling.Components;
using Content.Shared.Changeling.Systems;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Shared.Utility;
namespace Content.Client.Changeling.UI;
@@ -12,7 +13,9 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne
{
private SimpleRadialMenu? _menu;
private static readonly Color SelectedOptionBackground = Palettes.Green.Element.WithAlpha(128);
private static readonly Color DisabledOptionBackground = Palettes.Slate.Element.WithAlpha(128);
private static readonly Color SelectedOptionHoverBackground = Palettes.Green.HoveredElement.WithAlpha(128);
private static readonly Color DisabledOptionHoverBackground = Palettes.Slate.HoveredElement.WithAlpha(128);
protected override void Open()
{
@@ -23,7 +26,6 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne
_menu.OpenOverMouseScreenPosition();
}
public override void Update()
{
if (_menu == null)
@@ -32,7 +34,7 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne
if (!EntMan.TryGetComponent<ChangelingIdentityComponent>(Owner, out var lingIdentity))
return;
var models = ConvertToButtons(lingIdentity.ConsumedIdentities, lingIdentity?.CurrentIdentity);
var models = ConvertToButtons(lingIdentity.ConsumedIdentities.Keys, lingIdentity?.CurrentIdentity);
_menu.SetButtons(models);
}
@@ -43,21 +45,41 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne
)
{
var buttons = new List<RadialMenuOptionBase>();
var dropButtons = new List<RadialMenuOptionBase>();
foreach (var identity in identities)
{
if (!EntMan.TryGetComponent<MetaDataComponent>(identity, out var metadata))
continue;
// Options for selecting identities.
var option = new RadialMenuActionOption<NetEntity>(SendIdentitySelect, EntMan.GetNetEntity(identity))
{
IconSpecifier = RadialMenuIconSpecifier.With(identity),
ToolTip = metadata.EntityName,
BackgroundColor = (currentIdentity == identity) ? SelectedOptionBackground : null,
ToolTip = Loc.GetString("changeling-transform-bui-select-entity", ("entity", identity)),
BackgroundColor = (currentIdentity == identity) ? SelectedOptionBackground : null, // mark as selected
HoverBackgroundColor = (currentIdentity == identity) ? SelectedOptionHoverBackground : null
};
buttons.Add(option);
// Options for dropping identities.
var dropOption = new RadialMenuActionOption<NetEntity>(SendIdentityDrop, EntMan.GetNetEntity(identity))
{
IconSpecifier = RadialMenuIconSpecifier.With(identity),
ToolTip = (currentIdentity == identity)
? Loc.GetString("changeling-transform-bui-drop-identity-cannot-drop")
: Loc.GetString("changeling-transform-bui-drop-identity-entity", ("entity", identity)),
BackgroundColor = (currentIdentity == identity) ? DisabledOptionBackground : null, // cannot drop your current identity
HoverBackgroundColor = (currentIdentity == identity) ? DisabledOptionHoverBackground : null
};
dropButtons.Add(dropOption);
}
// Menu category for dropping identities.
var dropMenuButton = new RadialMenuNestedLayerOption(dropButtons)
{
IconSpecifier = RadialMenuIconSpecifier.With(new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/delete.svg.192dpi.png"))),
ToolTip = Loc.GetString("changeling-transform-bui-drop-identity-menu")
};
buttons.Add(dropMenuButton);
return buttons;
}
@@ -65,4 +87,9 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne
{
SendPredictedMessage(new ChangelingTransformIdentitySelectMessage(identityId));
}
private void SendIdentityDrop(NetEntity identityId)
{
SendPredictedMessage(new ChangelingTransformIdentityDropMessage(identityId));
}
}
+3 -11
View File
@@ -16,17 +16,9 @@ public sealed class ClickableSystem : EntitySystem
[Dependency] private readonly SharedTransformSystem _transforms = default!;
[Dependency] private readonly SpriteSystem _sprites = default!;
private EntityQuery<ClickableComponent> _clickableQuery;
private EntityQuery<TransformComponent> _xformQuery;
private EntityQuery<FadingSpriteComponent> _fadingSpriteQuery;
public override void Initialize()
{
base.Initialize();
_clickableQuery = GetEntityQuery<ClickableComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
_fadingSpriteQuery = GetEntityQuery<FadingSpriteComponent>();
}
[Dependency] private readonly EntityQuery<ClickableComponent> _clickableQuery = default!;
[Dependency] private readonly EntityQuery<TransformComponent> _xformQuery = default!;
[Dependency] private readonly EntityQuery<FadingSpriteComponent> _fadingSpriteQuery = default!;
/// <summary>
/// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding
@@ -223,7 +223,7 @@ public sealed class ClientClothingSystem : ClothingSystem
{
base.OnGotEquipped(uid, component, args);
RenderEquipment(args.Equipee, uid, args.Slot, clothingComponent: component);
RenderEquipment(args.EquipTarget, uid, args.Slot, clothingComponent: component);
}
private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot,
@@ -1,17 +1,23 @@
using System.Linq;
using System.Threading.Tasks;
using Content.Client.Gameplay;
using Content.Shared.CCVar;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Corvax.GuideGenerator;
using Content.Client.Gameplay;
using Content.Shared.Prototypes;
using Robust.Client;
using Robust.Client.GameObjects;
using Robust.Client.State;
using Robust.Client.Timing;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Utility;
namespace Content.Client.Corvax.ExportSprites;
@@ -29,6 +35,7 @@ public sealed class EntityScreenshotGenerator
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
private ISawmill _sawmill = default!;
@@ -120,7 +127,7 @@ public sealed class EntityScreenshotGenerator
var prototypes = _prototypeManager.EnumeratePrototypes<EntityPrototype>()
.Where(proto =>
!proto.Abstract &&
proto.Components.ContainsKey("Sprite") &&
HasExportableSprite(proto) &&
EntityProjectHelper.MatchesAllowedIds(proto.ID, allowedIds))
.OrderBy(proto => proto.ID)
.ToList();
@@ -136,11 +143,18 @@ public sealed class EntityScreenshotGenerator
try
{
entity = _entityManager.SpawnEntity(proto.ID, new EntityCoordinates(previewGrid.Owner, default));
if (proto.HasComponent<SpriteComponent>(_entityManager.ComponentFactory))
{
entity = _entityManager.SpawnEntity(proto.ID, new EntityCoordinates(previewGrid.Owner, default));
await WaitForEntityAppearanceAsync(entity);
ApplyPrototypeAppearance(entity, proto);
await WaitForEntityAppearanceAsync(entity, 1);
await WaitForEntityAppearanceAsync(entity);
ApplyPrototypeAppearance(entity, proto);
await WaitForEntityAppearanceAsync(entity, 1);
}
else
{
entity = SpawnIconEntity(proto);
}
await _renderService.Export(entity, Direction.South, outputDir / $"{proto.ID}.png");
exported++;
@@ -241,4 +255,128 @@ public sealed class EntityScreenshotGenerator
if (solution.GetPrimaryReagentId() is { } reagent)
appearanceSystem.SetData(entity, SolutionContainerVisuals.BaseOverride, reagent.ToString(), appearance);
}
private bool HasExportableSprite(EntityPrototype prototype)
{
if (prototype.HasComponent<SpriteComponent>(_entityManager.ComponentFactory))
return true;
return TryGetPrototypeIcon(prototype, out _);
}
private EntityUid SpawnIconEntity(EntityPrototype prototype)
{
if (!TryGetPrototypeIcon(prototype, out var icon) || icon == null)
throw new InvalidOperationException($"Prototype {prototype.ID} has no exportable icon.");
var entity = _entityManager.SpawnEntity(null, MapCoordinates.Nullspace);
var sprite = _entityManager.EnsureComponent<SpriteComponent>(entity);
var spriteSystem = _entitySystemManager.GetEntitySystem<SpriteSystem>();
spriteSystem.AddBlankLayer((entity, sprite), 0);
if (icon is SpriteSpecifier.EntityPrototype entityIcon)
spriteSystem.LayerSetTexture((entity, sprite), 0, spriteSystem.Frame0(new SpriteSpecifier.EntityPrototype(entityIcon.EntityPrototypeId)));
else
spriteSystem.LayerSetSprite((entity, sprite), 0, icon);
sprite.LayerSetShader(0, "unshaded");
spriteSystem.LayerSetVisible((entity, sprite), 0, true);
return entity;
}
private bool TryGetPrototypeIcon(EntityPrototype prototype, out SpriteSpecifier? icon)
{
icon = null;
foreach (var (_, entry) in prototype.Components)
{
if (TryExtractSpriteSpecifier(entry.Component.GetType(), entry.Mapping, out icon))
return true;
}
return false;
}
private bool TryExtractSpriteSpecifier(Type? expectedType, DataNode? node, out SpriteSpecifier? icon)
{
icon = null;
if (node == null)
return false;
if (expectedType != null &&
typeof(SpriteSpecifier).IsAssignableFrom(expectedType) &&
TryParseSpriteSpecifier(node, out icon))
{
return true;
}
if (node is MappingDataNode mapping)
{
foreach (var (key, child) in mapping.Children)
{
Type? childType = null;
if (expectedType != null &&
_serialization.TryGetVariableType(expectedType, key, out var resolvedType))
{
childType = resolvedType;
}
if (TryExtractSpriteSpecifier(childType, child, out icon))
return true;
}
return false;
}
if (node is SequenceDataNode sequence)
{
var elementType = GetSequenceElementType(expectedType);
foreach (var child in sequence.Sequence)
{
if (TryExtractSpriteSpecifier(elementType, child, out icon))
return true;
}
}
return false;
}
private static Type? GetSequenceElementType(Type? type)
{
if (type == null)
return null;
if (type.IsArray)
return type.GetElementType();
var genericArguments = type.GenericTypeArguments;
if (genericArguments.Length == 1)
return genericArguments[0];
return null;
}
private bool TryParseSpriteSpecifier(DataNode node, out SpriteSpecifier? icon)
{
icon = null;
try
{
icon = _serialization.Read<SpriteSpecifier>(node, notNullableOverride: true);
if (icon == SpriteSpecifier.Invalid)
{
icon = null;
return false;
}
return true;
}
catch
{
icon = null;
return false;
}
}
}
@@ -3,8 +3,8 @@ using System.Threading;
using System.Threading.Tasks;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Utility;
using Robust.Client.UserInterface;
using Robust.Client.Utility;
using Robust.Shared.ContentPack;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -27,6 +27,7 @@ public sealed class EntityScreenshotRenderService
private EntityScreenshotRenderControl? _control;
private bool _initialized;
private readonly Dictionary<(ResPath Path, string State), Image<Rgba32>> _rsiStateImageCache = new();
private readonly Dictionary<Texture, Image<Rgba32>> _textureImageCache = new();
private ISawmill _sawmill = default!;
public void Initialize()
@@ -48,6 +49,13 @@ public sealed class EntityScreenshotRenderService
_rsiStateImageCache.Clear();
foreach (var image in _textureImageCache.Values)
{
image.Dispose();
}
_textureImageCache.Clear();
if (_control == null)
return;
@@ -360,7 +368,7 @@ public sealed class EntityScreenshotRenderService
private static int ToDelayMilliseconds(float seconds)
{
return Math.Max(1, (int)MathF.Round(seconds * 1000f));
return Math.Max(1, (int) MathF.Round(seconds * 1000f));
}
private void WriteAnimationMetadata(ResPath animationDir, IReadOnlyList<AnimationFrameInfo> animationFrames)
@@ -388,7 +396,9 @@ public sealed class EntityScreenshotRenderService
return false;
// Keep the old render-target path for uncommon transformed sprites.
if (spriteComp.Scale != Vector2.One || spriteComp.Rotation != Angle.Zero)
if (spriteComp.Scale != Vector2.One ||
spriteComp.Rotation != Angle.Zero ||
spriteComp.EnableDirectionOverride)
return false;
var size = renderBounds.Size;
@@ -407,7 +417,7 @@ public sealed class EntityScreenshotRenderService
return false;
if (!TryGetLayerImage(spriteLayer, direction, out var sourceImage, out var sourceRect))
continue;
return false;
var drawColor = spriteComp.Color * spriteLayer.Color;
var drawOffset = ToPixelOffset(spriteComp.Offset + spriteLayer.Offset) - renderBounds.Min;
@@ -429,7 +439,7 @@ public sealed class EntityScreenshotRenderService
private static void BlitImage(
Image<Rgba32> sourceImage,
Rectangle sourceRect,
PixelRect sourceRect,
Color modulation,
Span<Rgba32> destination,
Vector2i destinationSize,
@@ -519,14 +529,17 @@ public sealed class EntityScreenshotRenderService
SpriteComponent.Layer layer,
Direction direction,
out Image<Rgba32> image,
out Rectangle sourceRect)
out PixelRect sourceRect)
{
image = default!;
sourceRect = default;
// Raw texture layers need a separate cache path. Use render target fallback for them.
if (layer.Texture != null)
return false;
{
image = GetTextureImage(layer.Texture);
sourceRect = new PixelRect(0, 0, image.Width, image.Height);
return true;
}
var rsi = layer.ActualRsi;
var stateId = ((ISpriteLayer) layer).RsiState;
@@ -569,10 +582,33 @@ public sealed class EntityScreenshotRenderService
var target = (int) rsiDirection * framesPerDirection + frame;
var targetY = target / statesX;
var targetX = target % statesX;
sourceRect = new Rectangle(targetX * frameWidth, targetY * frameHeight, frameWidth, frameHeight);
sourceRect = new PixelRect(targetX * frameWidth, targetY * frameHeight, frameWidth, frameHeight);
return true;
}
private Image<Rgba32> GetTextureImage(Texture texture)
{
if (_textureImageCache.TryGetValue(texture, out var cached))
return cached;
var image = new Image<Rgba32>(texture.Width, texture.Height);
var pixels = image.GetPixelSpan();
for (var y = 0; y < texture.Height; y++)
{
for (var x = 0; x < texture.Width; x++)
{
var color = texture.GetPixel(x, y);
pixels[y * texture.Width + x] = new Rgba32(color.RByte, color.GByte, color.BByte, color.AByte);
}
}
_textureImageCache[texture] = image;
return image;
}
private readonly record struct PixelRect(int Left, int Top, int Width, int Height);
private sealed class EntityScreenshotRenderControl : Control
{
private static readonly Color ExportBackgroundColor = new(128, 128, 128, 0);
@@ -2,14 +2,18 @@ using System.Diagnostics.CodeAnalysis;
using Content.Shared.DisplacementMap;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
namespace Content.Client.DisplacementMap;
public sealed class DisplacementMapSystem : EntitySystem
{
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly ISerializationManager _serialization = null!;
[Dependency] private readonly SpriteSystem _sprite = null!;
//needs to be replaced later: see comment on line 48
private static readonly ProtoId<ShaderPrototype> UnshadedID = "unshaded";
private static string? BuildDisplacementLayerKey(object key)
{
@@ -40,7 +44,16 @@ public sealed class DisplacementMapSystem : EntitySystem
EnsureDisplacementIsNotOnSprite(sprite, key);
if (data.ShaderOverride is not null)
sprite.Comp.LayerSetShader(index, data.ShaderOverride);
{
//TODO : this is a kinda janky workaround for the fact that the current rendering pipeline does not have
//proper support for multiple shaders on a given layer (or an ubershader to handle stacking all of the effects well)
//should be replaced by an engine-level solution, but this is an adequate temporary solution.
//what's that phrase about temporary solutions?
sprite.Comp.LayerSetShader(index,
(sprite.Comp[index] is SpriteComponent.Layer layer && layer.ShaderPrototype == UnshadedID)
? data.ShaderOverrideUnshaded
: data.ShaderOverride);
}
//allows you not to write it every time in the YML
foreach (var pair in data.SizeMaps)
+1 -3
View File
@@ -50,9 +50,7 @@ public sealed class DoAfterSystem : SharedDoAfterSystem
var time = GameTiming.CurTime;
var comp = Comp<DoAfterComponent>(playerEntity.Value);
var xformQuery = GetEntityQuery<TransformComponent>();
var handsQuery = GetEntityQuery<HandsComponent>();
Update(playerEntity.Value, active, comp, time, xformQuery, handsQuery);
Update(playerEntity.Value, active, comp, time);
}
/// <summary>
+5
View File
@@ -61,6 +61,11 @@ public sealed class AirlockSystem : SharedAirlockSystem
if (!comp.AnimatePanel)
return;
// For some reason the open panel sprite is used for both open and
// closed sprites. I really don't get it.
door.OpenSpriteStates.Add((WiresVisualLayers.MaintenancePanel, comp.OpenPanelSpriteState));
door.ClosedSpriteStates.Add((WiresVisualLayers.MaintenancePanel, comp.OpenPanelSpriteState));
((Animation)door.OpeningAnimation).AnimationTracks.Add(new AnimationTrackSpriteFlick()
{
LayerKey = WiresVisualLayers.MaintenancePanel,
+56 -10
View File
@@ -18,13 +18,14 @@ public sealed class DoorSystem : SharedDoorSystem
{
base.Initialize();
SubscribeLocalEvent<DoorComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<DoorComponent, AnimationCompletedEvent>(OnAnimationCompleted);
}
protected override void OnComponentInit(Entity<DoorComponent> ent, ref ComponentInit args)
{
var comp = ent.Comp;
comp.OpenSpriteStates = new List<(DoorVisualLayers, string)>(2);
comp.ClosedSpriteStates = new List<(DoorVisualLayers, string)>(2);
comp.OpenSpriteStates = new List<(Enum, string)>(2);
comp.ClosedSpriteStates = new List<(Enum, string)>(2);
comp.OpenSpriteStates.Add((DoorVisualLayers.Base, comp.OpenSpriteState));
comp.ClosedSpriteStates.Add((DoorVisualLayers.Base, comp.ClosedSpriteState));
@@ -78,6 +79,32 @@ public sealed class DoorSystem : SharedDoorSystem
};
}
private void OnAnimationCompleted(Entity<DoorComponent> ent, ref AnimationCompletedEvent args)
{
if (args.Key != DoorComponent.OpenCloseKey || !TryComp<SpriteComponent>(ent, out var sprite))
return;
switch (ent.Comp.State)
{
case DoorState.Open:
foreach (var (layer, layerState) in ent.Comp.OpenSpriteStates)
{
_sprite.LayerSetRsiState((ent.Owner, sprite), layer, layerState);
}
break;
case DoorState.Closed:
foreach (var (layer, layerState) in ent.Comp.ClosedSpriteStates)
{
_sprite.LayerSetRsiState((ent.Owner, sprite), layer, layerState);
}
break;
}
}
private void OnAppearanceChange(Entity<DoorComponent> entity, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
@@ -89,9 +116,6 @@ public sealed class DoorSystem : SharedDoorSystem
if (AppearanceSystem.TryGetData<string>(entity, PaintableVisuals.Prototype, out var prototype, args.Component))
UpdateSpriteLayers((entity.Owner, args.Sprite), prototype);
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey))
_animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey);
// We are checking beforehand since some doors may not have an emagging visual layer, and we don't want LayerSetVisible to throw an error.
if (_sprite.TryGetLayer(entity.Owner, DoorVisualLayers.BaseEmagging, out var _, false))
_sprite.LayerSetVisible(entity.Owner, DoorVisualLayers.BaseEmagging, state == DoorState.Emagging);
@@ -106,15 +130,25 @@ public sealed class DoorSystem : SharedDoorSystem
switch (state)
{
case DoorState.Open:
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenCloseKey))
return;
foreach (var (layer, layerState) in entity.Comp.OpenSpriteStates)
{
// Allow animations to play while it's open (e.g., pinion);
// the animation unsets this so we gotta set it again.
_sprite.LayerSetAutoAnimated((entity.Owner, sprite), layer, true);
_sprite.LayerSetRsiState((entity.Owner, sprite), layer, layerState);
}
return;
case DoorState.Closed:
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenCloseKey))
return;
foreach (var (layer, layerState) in entity.Comp.ClosedSpriteStates)
{
_sprite.LayerSetAutoAnimated((entity.Owner, sprite), layer, true);
_sprite.LayerSetRsiState((entity.Owner, sprite), layer, layerState);
}
@@ -123,24 +157,36 @@ public sealed class DoorSystem : SharedDoorSystem
if (entity.Comp.OpeningAnimationTime == TimeSpan.Zero)
return;
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.AnimationKey);
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenCloseKey))
return;
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.OpenCloseKey);
return;
case DoorState.Closing:
if (entity.Comp.ClosingAnimationTime == TimeSpan.Zero || entity.Comp.CurrentlyCrushing.Count != 0)
if (entity.Comp.ClosingAnimationTime == TimeSpan.Zero)
return;
_animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.AnimationKey);
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.OpenCloseKey))
return;
_animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.OpenCloseKey);
return;
case DoorState.Denying:
_animationSystem.Play(entity, (Animation)entity.Comp.DenyingAnimation, DoorComponent.AnimationKey);
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.DenyKey))
return;
_animationSystem.Play(entity, (Animation)entity.Comp.DenyingAnimation, DoorComponent.DenyKey);
return;
case DoorState.Emagging:
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.EmagKey))
return;
// We are checking beforehand since some doors may not have an emagging visual layer.
if (_sprite.TryGetLayer(entity.Owner, DoorVisualLayers.BaseEmagging, out var _, false))
_animationSystem.Play(entity, (Animation)entity.Comp.EmaggingAnimation, DoorComponent.AnimationKey);
_animationSystem.Play(entity, (Animation)entity.Comp.EmaggingAnimation, DoorComponent.EmagKey);
return;
}
+27 -10
View File
@@ -1,8 +1,8 @@
using Content.Shared.CCVar;
using Content.Shared.Drunk;
using Content.Shared.StatusEffect;
using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -11,19 +11,23 @@ namespace Content.Client.Drunk;
public sealed class DrunkOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> Shader = "Drunk";
private static readonly ProtoId<ShaderPrototype> DrunkShader = "Drunk";
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
private readonly Shared.StatusEffectNew.StatusEffectsSystem _statusEffectsSystem;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
private readonly ShaderInstance _drunkShader;
public float CurrentBoozePower = 0.0f;
// Starting phase for the rotation effect.
// Needed so it doesn't always look the same for 0 motion.
public float Phase = 0f;
private const float VisualThreshold = 10.0f;
private const float PowerDivisor = 250.0f;
@@ -37,12 +41,22 @@ public sealed class DrunkOverlay : Overlay
private const float BoozePowerScale = 8f;
private float _visualScale = 0;
private float _visualScale = 0f;
private float _timeScale = 1f;
private float _distortionScale = 1f;
public DrunkOverlay()
{
IoCManager.InjectDependencies(this);
_drunkShader = _prototypeManager.Index(Shader).InstanceUnique();
_statusEffectsSystem = _entityManager.System<Shared.StatusEffectNew.StatusEffectsSystem>();
_drunkShader = _prototypeManager.Index(DrunkShader).InstanceUnique();
_configManager.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true);
}
private void OnReducedMotionChanged(bool reducedMotion)
{
_timeScale = reducedMotion ? 0.0f : 1.0f;
_distortionScale = reducedMotion ? 4.0f : 1.0f; // Make the offset stronger to compensate the lack of motion.
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -53,15 +67,14 @@ public sealed class DrunkOverlay : Overlay
if (playerEntity == null)
return;
var statusSys = _sysMan.GetEntitySystem<Shared.StatusEffectNew.StatusEffectsSystem>();
if (!statusSys.TryGetMaxTime<DrunkStatusEffectComponent>(playerEntity.Value, out var status))
if (!_statusEffectsSystem.TryGetMaxTime<DrunkStatusEffectComponent>(playerEntity.Value, out var status))
return;
var time = status.Item2;
var power = time == null ? MaxBoozePower : (float) Math.Min((time - _timing.CurTime).Value.TotalSeconds, MaxBoozePower);
var power = time == null ? MaxBoozePower : (float)Math.Min((time - _timing.CurTime).Value.TotalSeconds, MaxBoozePower);
CurrentBoozePower += BoozePowerScale * (power - CurrentBoozePower) * args.DeltaSeconds / (power+1);
CurrentBoozePower += BoozePowerScale * (power - CurrentBoozePower) * args.DeltaSeconds / (power + 1);
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
@@ -82,8 +95,12 @@ public sealed class DrunkOverlay : Overlay
return;
var handle = args.WorldHandle;
_drunkShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_drunkShader.SetParameter("boozePower", _visualScale);
_drunkShader.SetParameter("timeScale", _timeScale);
_drunkShader.SetParameter("distortionScale", _distortionScale);
_drunkShader.SetParameter("phase", Phase);
handle.UseShader(_drunkShader);
handle.DrawRect(args.WorldBounds, Color.White);
handle.UseShader(null);
+6
View File
@@ -3,6 +3,7 @@ using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Client.Drunk;
@@ -10,6 +11,7 @@ public sealed class DrunkSystem : SharedDrunkSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private DrunkOverlay _overlay = default!;
@@ -29,7 +31,10 @@ public sealed class DrunkSystem : SharedDrunkSystem
private void OnStatusApplied(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectAppliedEvent args)
{
if (!_overlayMan.HasOverlay<DrunkOverlay>())
{
_overlay.Phase = _random.NextFloat(MathF.Tau); // random starting phase for movement effect
_overlayMan.AddOverlay(_overlay);
}
}
private void OnStatusRemoved(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectRemovedEvent args)
@@ -47,6 +52,7 @@ public sealed class DrunkSystem : SharedDrunkSystem
private void OnPlayerAttached(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectRelayedEvent<LocalPlayerAttachedEvent> args)
{
_overlayMan.AddOverlay(_overlay);
}
private void OnPlayerDetached(Entity<DrunkStatusEffectComponent> entity, ref StatusEffectRelayedEvent<LocalPlayerDetachedEvent> args)
-1
View File
@@ -121,7 +121,6 @@ namespace Content.Client.Entry
_prototypeManager.RegisterIgnore("noiseChannel");
_prototypeManager.RegisterIgnore("playerConnectionWhitelist");
_prototypeManager.RegisterIgnore("spaceBiome");
_prototypeManager.RegisterIgnore("worldgenConfig");
_prototypeManager.RegisterIgnore("gameRule");
_prototypeManager.RegisterIgnore("worldSpell");
_prototypeManager.RegisterIgnore("entitySpell");
@@ -31,7 +31,7 @@ public sealed class ClientFeedbackManager : SharedFeedbackManager
/// <inheritdoc />
public override void Display(List<ProtoId<FeedbackPopupPrototype>>? prototypes)
{
if (prototypes == null || !NetManager.IsClient)
if (prototypes == null)
return;
var count = _displayedPopups.Count;
@@ -42,9 +42,6 @@ public sealed class ClientFeedbackManager : SharedFeedbackManager
/// <inheritdoc />
public override void Remove(List<ProtoId<FeedbackPopupPrototype>>? prototypes)
{
if (!NetManager.IsClient)
return;
if (prototypes == null)
{
_displayedPopups.Clear();
+3 -3
View File
@@ -173,9 +173,9 @@ public sealed partial class HolopadWindow : FancyWindow
var callerId = _telephoneSystem.GetFormattedCallerIdForEntity(telephone.LastCallerId.Item1, telephone.LastCallerId.Item2, Color.LightGray, "Default", 11);
var holoapdId = _telephoneSystem.GetFormattedDeviceIdForEntity(telephone.LastCallerId.Item3, Color.LightGray, "Default", 11);
CallerIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
HolopadIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(holoapdId));
LockOutIdText.SetMessage(FormattedMessage.FromMarkupOrThrow(callerId));
CallerIdText.SetMessage(FormattedMessage.FromMarkupPermissive(callerId));
HolopadIdText.SetMessage(FormattedMessage.FromMarkupPermissive(holoapdId));
LockOutIdText.SetMessage(FormattedMessage.FromMarkupPermissive(callerId));
// Sort holopads alphabetically
var holopadArray = holopads.ToArray();
@@ -18,6 +18,8 @@ namespace Content.Client.IconSmoothing
{
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly EntityQuery<IconSmoothComponent> _iconSmoothQuery = default!;
[Dependency] private readonly EntityQuery<SpriteComponent> _spriteQuery = default!;
private readonly Queue<EntityUid> _dirtyEntities = new();
private readonly Queue<EntityUid> _anchorChangedEntities = new();
@@ -106,13 +108,10 @@ namespace Content.Client.IconSmoothing
{
base.FrameUpdate(frameTime);
var xformQuery = GetEntityQuery<TransformComponent>();
var smoothQuery = GetEntityQuery<IconSmoothComponent>();
// first process anchor state changes.
while (_anchorChangedEntities.TryDequeue(out var uid))
{
if (!xformQuery.TryGetComponent(uid, out var xform))
if (!TryComp(uid, out TransformComponent? xform))
continue;
if (xform.MapID == MapId.Nullspace)
@@ -123,7 +122,7 @@ namespace Content.Client.IconSmoothing
continue;
}
DirtyNeighbours(uid, comp: null, xform, smoothQuery);
DirtyNeighbours(uid, comp: null, xform);
}
// Next, update actual sprites.
@@ -132,19 +131,16 @@ namespace Content.Client.IconSmoothing
_generation += 1;
var spriteQuery = GetEntityQuery<SpriteComponent>();
// Performance: This could be spread over multiple updates, or made parallel.
while (_dirtyEntities.TryDequeue(out var uid))
{
CalculateNewSprite(uid, spriteQuery, smoothQuery, xformQuery);
CalculateNewSprite(uid);
}
}
public void DirtyNeighbours(EntityUid uid, IconSmoothComponent? comp = null, TransformComponent? transform = null, EntityQuery<IconSmoothComponent>? smoothQuery = null)
public void DirtyNeighbours(EntityUid uid, IconSmoothComponent? comp = null, TransformComponent? transform = null)
{
smoothQuery ??= GetEntityQuery<IconSmoothComponent>();
if (!smoothQuery.Value.Resolve(uid, ref comp) || !comp.Running)
if (!_iconSmoothQuery.Resolve(uid, ref comp) || !comp.Running)
return;
_dirtyEntities.Enqueue(uid);
@@ -206,11 +202,7 @@ namespace Content.Client.IconSmoothing
_anchorChangedEntities.Enqueue(uid);
}
private void CalculateNewSprite(EntityUid uid,
EntityQuery<SpriteComponent> spriteQuery,
EntityQuery<IconSmoothComponent> smoothQuery,
EntityQuery<TransformComponent> xformQuery,
IconSmoothComponent? smooth = null)
private void CalculateNewSprite(EntityUid uid, IconSmoothComponent? smooth = null)
{
TransformComponent? xform;
Entity<MapGridComponent>? gridEntity = null;
@@ -218,7 +210,7 @@ namespace Content.Client.IconSmoothing
// The generation check prevents updating an entity multiple times per tick.
// As it stands now, it's totally possible for something to get queued twice.
// Generation on the component is set after an update so we can cull updates that happened this generation.
if (!smoothQuery.Resolve(uid, ref smooth, false)
if (!_iconSmoothQuery.Resolve(uid, ref smooth, false)
|| smooth.Mode == IconSmoothingMode.NoSprite
|| smooth.UpdateGeneration == _generation
|| !smooth.Enabled
@@ -226,7 +218,7 @@ namespace Content.Client.IconSmoothing
{
if (smooth is { Enabled: true } &&
TryComp<SmoothEdgeComponent>(uid, out var edge) &&
xformQuery.TryGetComponent(uid, out xform))
TryComp(uid, out xform))
{
var directions = DirectionFlag.None;
@@ -237,13 +229,13 @@ namespace Content.Client.IconSmoothing
gridEntity = (gridUid, grid);
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North))))
directions |= DirectionFlag.North;
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South))))
directions |= DirectionFlag.South;
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East))))
directions |= DirectionFlag.East;
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West))))
directions |= DirectionFlag.West;
}
@@ -253,10 +245,10 @@ namespace Content.Client.IconSmoothing
return;
}
xform = xformQuery.GetComponent(uid);
xform = Transform(uid);
smooth.UpdateGeneration = _generation;
if (!spriteQuery.TryGetComponent(uid, out var sprite))
if (!_spriteQuery.TryGetComponent(uid, out var sprite))
{
Log.Error($"Encountered a icon-smoothing entity without a sprite: {ToPrettyString(uid)}");
RemCompDeferred(uid, smooth);
@@ -281,13 +273,13 @@ namespace Content.Client.IconSmoothing
switch (smooth.Mode)
{
case IconSmoothingMode.Corners:
CalculateNewSpriteCorners(gridEntity, smooth, spriteEnt, xform, smoothQuery);
CalculateNewSpriteCorners(gridEntity, smooth, spriteEnt, xform);
break;
case IconSmoothingMode.CardinalFlags:
CalculateNewSpriteCardinal(gridEntity, smooth, spriteEnt, xform, smoothQuery);
CalculateNewSpriteCardinal(gridEntity, smooth, spriteEnt, xform);
break;
case IconSmoothingMode.Diagonal:
CalculateNewSpriteDiagonal(gridEntity, smooth, spriteEnt, xform, smoothQuery);
CalculateNewSpriteDiagonal(gridEntity, smooth, spriteEnt, xform);
break;
default:
throw new ArgumentOutOfRangeException();
@@ -295,7 +287,7 @@ namespace Content.Client.IconSmoothing
}
private void CalculateNewSpriteDiagonal(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth,
Entity<SpriteComponent> sprite, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
Entity<SpriteComponent> sprite, TransformComponent xform)
{
if (gridEntity == null)
{
@@ -320,7 +312,7 @@ namespace Content.Client.IconSmoothing
for (var i = 0; i < neighbors.Length; i++)
{
var neighbor = (Vector2i)rotation.RotateVec(neighbors[i]);
matching = matching && MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos + neighbor), smoothQuery);
matching = matching && MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos + neighbor));
}
if (matching)
@@ -333,7 +325,7 @@ namespace Content.Client.IconSmoothing
}
}
private void CalculateNewSpriteCardinal(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth, Entity<SpriteComponent> sprite, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
private void CalculateNewSpriteCardinal(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth, Entity<SpriteComponent> sprite, TransformComponent xform)
{
var dirs = CardinalConnectDirs.None;
@@ -347,13 +339,13 @@ namespace Content.Client.IconSmoothing
var grid = gridEntity.Value.Comp;
var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates);
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North))))
dirs |= CardinalConnectDirs.North;
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South))))
dirs |= CardinalConnectDirs.South;
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East))))
dirs |= CardinalConnectDirs.East;
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery))
if (MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West))))
dirs |= CardinalConnectDirs.West;
_sprite.LayerSetRsiState(sprite.AsNullable(), 0, $"{smooth.StateBase}{(int)dirs}");
@@ -372,11 +364,11 @@ namespace Content.Client.IconSmoothing
CalculateEdge(sprite, directions, sprite);
}
private bool MatchingEntity(IconSmoothComponent smooth, AnchoredEntitiesEnumerator candidates, EntityQuery<IconSmoothComponent> smoothQuery)
private bool MatchingEntity(IconSmoothComponent smooth, AnchoredEntitiesEnumerator candidates)
{
while (candidates.MoveNext(out var entity))
{
if (smoothQuery.TryGetComponent(entity, out var other) &&
if (_iconSmoothQuery.TryGetComponent(entity, out var other) &&
other.SmoothKey != null &&
(other.SmoothKey == smooth.SmoothKey || smooth.AdditionalKeys.Contains(other.SmoothKey)) &&
other.Enabled)
@@ -388,11 +380,11 @@ namespace Content.Client.IconSmoothing
return false;
}
private void CalculateNewSpriteCorners(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth, Entity<SpriteComponent> spriteEnt, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
private void CalculateNewSpriteCorners(Entity<MapGridComponent>? gridEntity, IconSmoothComponent smooth, Entity<SpriteComponent> spriteEnt, TransformComponent xform)
{
var (cornerNE, cornerNW, cornerSW, cornerSE) = gridEntity == null
? (CornerFill.None, CornerFill.None, CornerFill.None, CornerFill.None)
: CalculateCornerFill(gridEntity.Value, smooth, xform, smoothQuery);
: CalculateCornerFill(gridEntity.Value, smooth, xform);
// TODO figure out a better way to set multiple sprite layers.
// This will currently re-calculate the sprite bounding box 4 times.
@@ -422,20 +414,20 @@ namespace Content.Client.IconSmoothing
CalculateEdge(spriteEnt, directions, sprite);
}
private (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(Entity<MapGridComponent> gridEntity, IconSmoothComponent smooth, TransformComponent xform, EntityQuery<IconSmoothComponent> smoothQuery)
private (CornerFill ne, CornerFill nw, CornerFill sw, CornerFill se) CalculateCornerFill(Entity<MapGridComponent> gridEntity, IconSmoothComponent smooth, TransformComponent xform)
{
var gridUid = gridEntity.Owner;
var grid = gridEntity.Comp;
var pos = _mapSystem.TileIndicesFor(gridUid, grid, xform.Coordinates);
var n = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)), smoothQuery);
var ne = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthEast)), smoothQuery);
var e = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)), smoothQuery);
var se = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthEast)), smoothQuery);
var s = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)), smoothQuery);
var sw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthWest)), smoothQuery);
var w = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)), smoothQuery);
var nw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthWest)), smoothQuery);
var n = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.North)));
var ne = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthEast)));
var e = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.East)));
var se = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthEast)));
var s = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.South)));
var sw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.SouthWest)));
var w = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.West)));
var nw = MatchingEntity(smooth, _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, pos.Offset(Direction.NorthWest)));
// ReSharper disable InconsistentNaming
var cornerNE = CornerFill.None;
+2 -3
View File
@@ -48,6 +48,7 @@ public sealed class DragDropSystem : SharedDragDropSystem
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly EntityQuery<SpriteComponent> _spriteQuery = default!;
// how often to recheck possible targets (prevents calling expensive
// check logic each update)
@@ -433,11 +434,9 @@ public sealed class DragDropSystem : SharedDragDropSystem
var bounds = new Box2(mousePos.Position - expansion, mousePos.Position + expansion);
var pvsEntities = _lookup.GetEntitiesIntersecting(mousePos.MapId, bounds);
var spriteQuery = GetEntityQuery<SpriteComponent>();
foreach (var entity in pvsEntities)
{
if (!spriteQuery.TryGetComponent(entity, out var inRangeSprite) ||
if (!_spriteQuery.TryGetComponent(entity, out var inRangeSprite) ||
!inRangeSprite.Visible ||
entity == _draggedEntity)
{
@@ -73,8 +73,8 @@ namespace Content.Client.Inventory
private void OnDidUnequip(InventorySlotsComponent component, DidUnequipEvent args)
{
UpdateSlot(args.Equipee, component, args.Slot);
if (args.Equipee != _playerManager.LocalEntity)
UpdateSlot(args.EquipTarget, component, args.Slot);
if (args.EquipTarget != _playerManager.LocalEntity)
return;
var update = new SlotSpriteUpdate(null, args.SlotGroup, args.Slot, false);
OnSpriteUpdate?.Invoke(update);
@@ -82,8 +82,8 @@ namespace Content.Client.Inventory
private void OnDidEquip(InventorySlotsComponent component, DidEquipEvent args)
{
UpdateSlot(args.Equipee, component, args.Slot);
if (args.Equipee != _playerManager.LocalEntity)
UpdateSlot(args.EquipTarget, component, args.Slot);
if (args.EquipTarget != _playerManager.LocalEntity)
return;
var update = new SlotSpriteUpdate(args.Equipment, args.SlotGroup, args.Slot,
HasComp<StorageComponent>(args.Equipment));
+3 -2
View File
@@ -1,8 +1,9 @@
<DefaultWindow
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="clr-namespace:Content.Client.Materials.UI"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'lathe-menu-title'}"
MinSize="550 450"
SetSize="750 500">
@@ -156,4 +157,4 @@
</BoxContainer>
</DefaultWindow>
</controls:FancyWindow>
+3 -2
View File
@@ -1,6 +1,7 @@
using System.Linq;
using System.Text;
using Content.Client.Materials;
using Content.Client.UserInterface.Controls;
using Content.Shared.Lathe;
using Content.Shared.Lathe.Prototypes;
using Content.Shared.Research.Prototypes;
@@ -8,7 +9,6 @@ using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -16,7 +16,7 @@ using Robust.Shared.Utility;
namespace Content.Client.Lathe.UI;
[GenerateTypedNameReferences]
public sealed partial class LatheMenu : DefaultWindow
public sealed partial class LatheMenu : FancyWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -75,6 +75,7 @@ public sealed partial class LatheMenu : DefaultWindow
public void SetEntity(EntityUid uid)
{
Entity = uid;
this.SetInfoFromEntity(_entityManager, Entity);
if (_entityManager.TryGetComponent<LatheComponent>(Entity, out var latheComponent))
{
@@ -58,7 +58,7 @@ public sealed class MultipartMachineSystem : SharedMultipartMachineSystem
var entityCoords = new EntityCoordinates(ent.Owner, part.Offset);
var ghostEnt = Spawn(_ghostPrototype, entityCoords);
if (!XformQuery.TryGetComponent(ghostEnt, out var xform))
if (!TryComp(ghostEnt, out TransformComponent? xform))
break;
xform.LocalRotation = part.Rotation;
@@ -2,9 +2,8 @@ using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.EntityConditions.Conditions;
using Content.Shared.FixedPoint;
using Content.Shared.Medical.Cryogenics;
@@ -20,6 +19,7 @@ public sealed partial class CryoPodWindow : FancyWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly SharedAtmosphereSystem _atmosphere = default!;
public event Action? OnEjectPatientPressed;
public event Action? OnEjectBeakerPressed;
@@ -29,6 +29,7 @@ public sealed partial class CryoPodWindow : FancyWindow
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_atmosphere = _entityManager.System<SharedAtmosphereSystem>();
EjectPatientButton.OnPressed += _ => OnEjectPatientPressed?.Invoke();
EjectBeakerButton.OnPressed += _ => OnEjectBeakerPressed?.Invoke();
Inject1.OnPressed += _ => OnInjectPressed?.Invoke(1);
@@ -71,16 +72,16 @@ public sealed partial class CryoPodWindow : FancyWindow
{
var totalGasAmount = msg.GasMix.Gases.Sum(gas => gas.Amount);
foreach (var gas in msg.GasMix.Gases)
foreach (var gasEntry in msg.GasMix.Gases)
{
var color = Color.FromHex($"#{gas.Color}", Color.White);
var percent = gas.Amount / totalGasAmount * 100;
var localizedName = Loc.GetString(gas.Name);
var gasProto = _atmosphere.GetGas(gasEntry.Gas);
var percent = gasEntry.Amount / totalGasAmount * 100;
var localizedName = Loc.GetString(gasProto.Name);
var tooltip = Loc.GetString("gas-analyzer-window-molarity-percentage-text",
("gasName", localizedName),
("amount", $"{gas.Amount:0.##}"),
("amount", $"{gasEntry.Amount:0.##}"),
("percentage", $"{percent:0.#}"));
GasMixChart.AddEntry(gas.Amount, color, tooltip: tooltip);
GasMixChart.AddEntry(gasEntry.Amount, gasProto.Color, tooltip: tooltip);
}
}
@@ -10,15 +10,12 @@ namespace Content.Client.Movement.Systems;
public sealed class ClientSpriteMovementSystem : SharedSpriteMovementSystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
private EntityQuery<SpriteComponent> _spriteQuery;
[Dependency] private readonly EntityQuery<SpriteComponent> _spriteQuery = default!;
public override void Initialize()
{
base.Initialize();
_spriteQuery = GetEntityQuery<SpriteComponent>();
SubscribeLocalEvent<SpriteMovementComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
}
@@ -12,14 +12,12 @@ public sealed class FloorOcclusionSystem : SharedFloorOcclusionSystem
[Dependency] private readonly IPrototypeManager _proto = default!;
private EntityQuery<SpriteComponent> _spriteQuery;
[Dependency] private readonly EntityQuery<SpriteComponent> _spriteQuery = default!;
public override void Initialize()
{
base.Initialize();
_spriteQuery = GetEntityQuery<SpriteComponent>();
SubscribeLocalEvent<FloorOcclusionComponent, ComponentStartup>(OnOcclusionStartup);
SubscribeLocalEvent<FloorOcclusionComponent, ComponentShutdown>(OnOcclusionShutdown);
SubscribeLocalEvent<FloorOcclusionComponent, AfterAutoHandleStateEvent>(OnOcclusionAuto);
@@ -0,0 +1,66 @@
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Client.GameObjects;
namespace Content.Client.Nutrition.EntitySystems;
public sealed class CreamPieSystem : SharedCreamPieSystem
{
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly AppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CreamPiedComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<CreamPiedComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<CreamPiedComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<CreamPiedComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
}
private void OnComponentInit(Entity<CreamPiedComponent> ent, ref ComponentInit args)
{
UpdateAppearance(ent);
}
private void OnComponentShutdown(Entity<CreamPiedComponent> ent, ref ComponentShutdown args)
{
_sprite.RemoveLayer(ent.Owner, CreamPiedVisualLayer.Key);
}
private void OnAppearanceChange(Entity<CreamPiedComponent> ent, ref AppearanceChangeEvent args)
{
UpdateAppearance((ent.Owner, ent.Comp, args.Sprite, args.Component));
}
private void OnAfterAutoHandleState(Entity<CreamPiedComponent> ent, ref AfterAutoHandleStateEvent args)
{
// Update when the sprite datafield is changed so that changelings can transform properly.
UpdateAppearance(ent);
}
private void UpdateAppearance(Entity<CreamPiedComponent, SpriteComponent?, AppearanceComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp2, false) || !Resolve(ent, ref ent.Comp3, false))
return;
var creamPied = ent.Comp1;
var sprite = ent.Comp2;
var appearance = ent.Comp3;
// If there is no sprite to use, remove the layer. Otherwise ensure that it exists and set the visuals accordingly.
int index;
if (creamPied.Sprite == null)
{
_sprite.RemoveLayer((ent.Owner, sprite), CreamPiedVisualLayer.Key);
return;
}
index = _sprite.LayerMapReserve((ent.Owner, sprite), CreamPiedVisualLayer.Key);
_appearance.TryGetData<bool>(ent.Owner, CreamPiedVisuals.Creamed, out var isCreamPied, appearance);
_sprite.LayerSetSprite((ent.Owner, sprite), index, creamPied.Sprite);
_sprite.LayerSetVisible((ent.Owner, sprite), index, isCreamPied);
}
}
@@ -1,10 +0,0 @@
using Content.Shared.Nutrition.EntitySystems;
using JetBrains.Annotations;
namespace Content.Client.Nutrition.EntitySystems
{
[UsedImplicitly]
public sealed class CreamPiedSystem : SharedCreamPieSystem
{
}
}
@@ -27,6 +27,7 @@ public sealed class TargetOutlineSystem : EntitySystem
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly EntityQuery<SpriteComponent> _spriteQuery = default!;
private bool _enabled = false;
@@ -130,11 +131,10 @@ public sealed class TargetOutlineSystem : EntitySystem
var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition).Position;
var bounds = new Box2(mousePos - LookupVector, mousePos + LookupVector);
var pvsEntities = _lookup.GetEntitiesIntersecting(_eyeManager.CurrentEye.Position.MapId, bounds, LookupFlags.Approximate | LookupFlags.Static);
var spriteQuery = GetEntityQuery<SpriteComponent>();
foreach (var entity in pvsEntities)
{
if (!spriteQuery.TryGetComponent(entity, out var sprite) || !sprite.Visible)
if (!_spriteQuery.TryGetComponent(entity, out var sprite) || !sprite.Visible)
continue;
// Check the predicate
@@ -4,7 +4,6 @@ using Content.Shared.Chemistry.Components;
using Content.Shared.Polymorph.Components;
using Content.Shared.Polymorph.Systems;
using Robust.Client.GameObjects;
using Robust.Shared.Player;
namespace Content.Client.Polymorph.Systems;
@@ -13,16 +12,13 @@ public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
private EntityQuery<AppearanceComponent> _appearanceQuery;
private EntityQuery<SpriteComponent> _spriteQuery;
[Dependency] private readonly EntityQuery<AppearanceComponent> _appearanceQuery = default!;
[Dependency] private readonly EntityQuery<SpriteComponent> _spriteQuery = default!;
public override void Initialize()
{
base.Initialize();
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
_spriteQuery = GetEntityQuery<SpriteComponent>();
SubscribeLocalEvent<ChameleonDisguiseComponent, AfterAutoHandleStateEvent>(OnHandleState);
SubscribeLocalEvent<ChameleonDisguisedComponent, ComponentStartup>(OnStartup);
@@ -5,7 +5,7 @@ using Robust.Client.Graphics;
namespace Content.Client.Radiation.Systems;
public sealed class RadiationSystem : EntitySystem
public sealed class RadiationSystem : SharedRadiationSystem
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
@@ -74,8 +74,7 @@ public sealed partial class ReplaySpectatorSystem
if ((Direction & DirectionFlag.East) != 0)
effectiveDir &= ~DirectionFlag.West;
var query = GetEntityQuery<TransformComponent>();
var xform = query.GetComponent(player);
var xform = Transform(player);
var pos = _transform.GetWorldPosition(xform);
if (!xform.ParentUid.IsValid())
@@ -13,16 +13,11 @@ public sealed partial class BorgSystem
// Don't put this on the component because we only need to track the time for a single entity
// and we don't want to TryComp it every single tick.
private TimeSpan _nextAlertUpdate = TimeSpan.Zero;
private EntityQuery<BorgChassisComponent> _chassisQuery;
private EntityQuery<PowerCellSlotComponent> _slotQuery;
public void InitializeBattery()
{
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<BorgChassisComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
_chassisQuery = GetEntityQuery<BorgChassisComponent>();
_slotQuery = GetEntityQuery<PowerCellSlotComponent>();
}
private void OnPlayerAttached(Entity<BorgChassisComponent> ent, ref LocalPlayerAttachedEvent args)
@@ -2,6 +2,7 @@
using Content.Shared.Mobs;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Content.Shared.Silicons.Borgs;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Client.GameObjects;
@@ -22,6 +23,8 @@ public sealed partial class BorgSystem : SharedBorgSystem
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly EntityQuery<BorgChassisComponent> _chassisQuery = default!;
[Dependency] private readonly EntityQuery<PowerCellSlotComponent> _slotQuery = default!;
public override void Initialize()
{
@@ -1,9 +1,11 @@
using System.Numerics;
using Content.Shared.CCVar;
using Content.Shared.Singularity.Components;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using System.Numerics;
namespace Content.Client.Singularity
{
@@ -13,6 +15,7 @@ namespace Content.Client.Singularity
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
private SharedTransformSystem? _xformSystem = null;
/// <summary>
@@ -28,6 +31,8 @@ namespace Content.Client.Singularity
private readonly ShaderInstance _shader;
private bool _reducedMotion;
public SingularityOverlay()
{
IoCManager.InjectDependencies(this);
@@ -35,6 +40,8 @@ namespace Content.Client.Singularity
_shader.SetParameter("maxDistance", MaxDistance * EyeManager.PixelsPerMeter);
_entMan.EventBus.SubscribeEvent<PixelToMapEvent>(EventSource.Local, this, OnProjectFromScreenToMap);
ZIndex = 101; // Should be drawn after the placement overlay so admins placing items near the singularity can tell where they're going.
_configManager.OnValueChanged(CCVars.ReducedMotion, (b) => { _reducedMotion = b; }, invokeImmediately: true);
}
private readonly Vector2[] _positions = new Vector2[MaxCount];
@@ -44,6 +51,8 @@ namespace Content.Client.Singularity
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (_reducedMotion)
return false;
if (args.Viewport.Eye == null)
return false;
if (_xformSystem is null && !_entMan.TrySystem(out _xformSystem))
@@ -102,6 +111,8 @@ namespace Content.Client.Singularity
/// </summary>
private void OnProjectFromScreenToMap(ref PixelToMapEvent args)
{ // Mostly copypasta from the singularity shader.
if (_reducedMotion)
return;
if (args.Viewport.Eye == null)
return;
var maxDistance = MaxDistance * EyeManager.PixelsPerMeter;
+4 -10
View File
@@ -27,16 +27,15 @@ public sealed class SpriteFadeSystem : EntitySystem
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly EntityQuery<SpriteComponent> _spriteQuery = default!;
[Dependency] private readonly EntityQuery<SpriteFadeComponent> _fadeQuery = default!;
[Dependency] private readonly EntityQuery<FadingSpriteComponent> _fadingQuery = default!;
[Dependency] private readonly EntityQuery<FixturesComponent> _fixturesQuery = default!;
private List<(MapCoordinates Point, bool ExcludeBoundingBox)> _points = new();
private readonly HashSet<FadingSpriteComponent> _comps = new();
private EntityQuery<SpriteComponent> _spriteQuery;
private EntityQuery<SpriteFadeComponent> _fadeQuery;
private EntityQuery<FadingSpriteComponent> _fadingQuery;
private EntityQuery<FixturesComponent> _fixturesQuery;
private const float TargetAlpha = 0.4f;
private const float ChangeRate = 1f;
@@ -44,11 +43,6 @@ public sealed class SpriteFadeSystem : EntitySystem
{
base.Initialize();
_spriteQuery = GetEntityQuery<SpriteComponent>();
_fadeQuery = GetEntityQuery<SpriteFadeComponent>();
_fadingQuery = GetEntityQuery<FadingSpriteComponent>();
_fixturesQuery = GetEntityQuery<FixturesComponent>();
SubscribeLocalEvent<FadingSpriteComponent, ComponentShutdown>(OnFadingShutdown);
}
@@ -5,14 +5,12 @@ namespace Content.Client.Sticky.Visualizers;
public sealed class StickyVisualizerSystem : VisualizerSystem<StickyVisualizerComponent>
{
private EntityQuery<SpriteComponent> _spriteQuery;
[Dependency] private readonly EntityQuery<SpriteComponent> _spriteQuery = default!;
public override void Initialize()
{
base.Initialize();
_spriteQuery = GetEntityQuery<SpriteComponent>();
SubscribeLocalEvent<StickyVisualizerComponent, ComponentInit>(OnInit);
}
+5
View File
@@ -0,0 +1,5 @@
using Content.Shared.Store;
namespace Content.Client.Store;
public sealed class StoreSystem : SharedStoreSystem;
@@ -1,7 +1,6 @@
using System.Linq;
using Content.Shared.Store;
using JetBrains.Annotations;
using System.Linq;
using Content.Shared.Store.Components;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
@@ -11,6 +10,7 @@ namespace Content.Client.Store.Ui;
public sealed class StoreBoundUserInterface : BoundUserInterface
{
private IPrototypeManager _prototypeManager = default!;
private readonly StoreSystem _storeSystem = default!;
[ViewVariables]
private StoreMenu? _menu;
@@ -23,6 +23,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
public StoreBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_storeSystem = EntMan.System<StoreSystem>();
}
protected override void Open()
@@ -30,12 +31,12 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
base.Open();
_menu = this.CreateWindow<StoreMenu>();
if (EntMan.TryGetComponent<StoreComponent>(Owner, out var store))
_menu.Title = Loc.GetString(store.Name);
if (_storeSystem.TryGetStore(Owner, out var store))
_menu.Title = Loc.GetString(store.Value.Comp.Name);
_menu.OnListingButtonPressed += (_, listing) =>
{
SendMessage(new StoreBuyListingMessage(listing.ID));
SendMessage(new StoreBuyListingMessage(listing.ID, EntMan.GetNetEntity(Owner)));
};
_menu.OnCategoryButtonPressed += (_, category) =>
@@ -2,7 +2,7 @@ using Content.Client.Stylesheets.Palette;
namespace Content.Client.Stylesheets.Stylesheets;
public sealed partial class NanotrasenStylesheet
public partial class NanotrasenStylesheet
{
//WL-Change-start
public override ColorPalette PrimaryPalette => Palettes.WL2;
+7 -8
View File
@@ -20,6 +20,8 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly TrayScanRevealSystem _trayScanReveal = default!;
[Dependency] private readonly EntityQuery<TrayScannerComponent> _trayScannerQuery = default!;
[Dependency] private readonly EntityQuery<SubFloorHideComponent> _subFloorHideQuery = default!;
private const string TRayAnimationKey = "trays";
private const double AnimationLength = 0.3;
@@ -35,16 +37,14 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
// TODO: Multiple viewports or w/e
var player = _player.LocalEntity;
var xformQuery = GetEntityQuery<TransformComponent>();
if (!xformQuery.TryGetComponent(player, out var playerXform))
if (!TryComp(player, out TransformComponent? playerXform))
return;
var playerPos = _transform.GetWorldPosition(playerXform, xformQuery);
var playerPos = _transform.GetWorldPosition(playerXform);
var playerMap = playerXform.MapID;
var range = 0f;
HashSet<Entity<SubFloorHideComponent>> inRange;
var scannerQuery = GetEntityQuery<TrayScannerComponent>();
// TODO: Should probably sub to player attached changes / inventory changes but inventory's
// API is extremely skrungly. If this ever shows up on dottrace ping me and laugh.
@@ -57,7 +57,7 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
{
foreach (var ent in slot.ContainedEntities)
{
if (!scannerQuery.TryGetComponent(ent, out var sneakScanner) || !sneakScanner.Enabled)
if (!_trayScannerQuery.TryGetComponent(ent, out var sneakScanner) || !sneakScanner.Enabled)
continue;
canSee = true;
@@ -71,7 +71,7 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
if (!_hands.TryGetHeldItem(player.Value, hand, out var heldEntity))
continue;
if (!scannerQuery.TryGetComponent(heldEntity, out var heldScanner) || !heldScanner.Enabled)
if (!_trayScannerQuery.TryGetComponent(heldEntity, out var heldScanner) || !heldScanner.Enabled)
continue;
range = MathF.Max(heldScanner.Range, range);
@@ -93,13 +93,12 @@ public sealed class TrayScannerSystem : SharedTrayScannerSystem
}
var revealedQuery = AllEntityQuery<TrayRevealedComponent, SpriteComponent>();
var subfloorQuery = GetEntityQuery<SubFloorHideComponent>();
while (revealedQuery.MoveNext(out var uid, out _, out var sprite))
{
// Revealing
// Add buffer range to avoid flickers.
if (subfloorQuery.TryGetComponent(uid, out var subfloor) &&
if (_subFloorHideQuery.TryGetComponent(uid, out var subfloor) &&
inRange.Contains((uid, subfloor)))
{
// Due to the fact client is predicting this server states will reset it constantly
@@ -33,15 +33,7 @@ public sealed class BuiPreTickUpdateSystem : EntitySystem
[Dependency] private readonly IPlayerManager _playerManager = null!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = null!;
[Dependency] private readonly IGameTiming _gameTiming = null!;
private EntityQuery<UserInterfaceUserComponent> _userQuery;
public override void Initialize()
{
base.Initialize();
_userQuery = GetEntityQuery<UserInterfaceUserComponent>();
}
[Dependency] private readonly EntityQuery<UserInterfaceUserComponent> _userQuery = default!;
public void RunUpdates()
{
@@ -228,7 +228,6 @@ public class RadialMenu : BaseWindow
/// Base class for radial menu buttons. Excludes all actions except clicks and alt-clicks
/// from interactions.
/// </summary>
[Virtual]
public abstract class RadialMenuButtonBase : BaseButton
{
/// <inheritdoc />
@@ -9,7 +9,6 @@ using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface.Controls
{
[Virtual]
public abstract class SlotControl : Control, IEntityControl
{
public static int DefaultButtonSize = 64;
@@ -46,7 +46,7 @@ namespace Content.Client.UserInterface.Systems.Atmos.GasTank
if (EntMan.TryGetComponent(Owner, out GasTankComponent? component))
{
var canConnect = EntMan.System<SharedGasTankSystem>().CanConnectToInternals((Owner, component));
_window?.Update(canConnect, component.IsConnected, component.OutputPressure);
_window?.Update(canConnect, component.IsConnected, component.ReleasePressure);
}
if (state is GasTankBoundUserInterfaceState cast)
@@ -11,7 +11,6 @@ public interface IItemslotUIContainer
public bool TryAddButton(SlotControl control);
}
[Virtual]
public abstract class ItemSlotUIContainer<T> : GridContainer, IItemslotUIContainer where T : SlotControl
{
private readonly Dictionary<string, T> _buttons = new();
+2 -2
View File
@@ -34,6 +34,7 @@ namespace Content.Client.Verbs
[Dependency] private readonly SharedContainerSystem _containers = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly EntityQuery<SpriteComponent> _spriteQuery = default!;
private float _lookupSize;
@@ -159,10 +160,9 @@ namespace Content.Client.Verbs
if (container == null && (visibility & MenuVisibility.InContainer) == 0)
return entities.Count != 0;
var spriteQuery = GetEntityQuery<SpriteComponent>();
for (var i = entities.Count - 1; i >= 0; i--)
{
if (!spriteQuery.TryGetComponent(entities[i], out var spriteComponent) || !spriteComponent.Visible)
if (!_spriteQuery.TryGetComponent(entities[i], out var spriteComponent) || !spriteComponent.Visible)
entities.RemoveSwap(i);
}
@@ -32,7 +32,7 @@ public sealed partial class MeleeWeaponSystem
if (localPos == Vector2.Zero || animation == null)
return;
if (!_xformQuery.TryGetComponent(user, out var userXform) || userXform.MapID == MapId.Nullspace)
if (!TryComp(user, out TransformComponent? userXform) || userXform.MapID == MapId.Nullspace)
return;
var animationUid = Spawn(animation, userXform.Coordinates);
@@ -64,7 +64,7 @@ public sealed partial class MeleeWeaponSystem
}
_sprite.SetRotation((animationUid, sprite), localPos.ToWorldAngle());
var xform = _xformQuery.GetComponent(animationUid);
var xform = Transform(animationUid);
TrackUserComponent track;
switch (arcComponent.Animation)
@@ -35,14 +35,12 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private EntityQuery<TransformComponent> _xformQuery;
private const string MeleeLungeKey = "melee-lunge";
public override void Initialize()
{
base.Initialize();
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeNetworkEvent<MeleeLungeEvent>(OnMeleeLunge);
UpdatesOutsidePrediction = true;
}
@@ -177,7 +175,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
private void ClientHeavyAttack(EntityUid user, EntityCoordinates coordinates, EntityUid meleeUid, MeleeWeaponComponent component)
{
// Only run on first prediction to avoid the potential raycast entities changing.
if (!_xformQuery.TryGetComponent(user, out var userXform) ||
if (!TryComp(user, out TransformComponent? userXform) ||
!Timing.IsFirstTimePredicted)
{
return;
+3 -7
View File
@@ -20,19 +20,15 @@ public sealed class WeatherSystem : SharedWeatherSystem
[Dependency] private readonly MapSystem _mapSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
private EntityQuery<AudioComponent> _audioQuery;
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<RoofComponent> _roofQuery;
[Dependency] private readonly EntityQuery<AudioComponent> _audioQuery = default!;
[Dependency] private readonly EntityQuery<MapGridComponent> _gridQuery = default!;
[Dependency] private readonly EntityQuery<RoofComponent> _roofQuery = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WeatherStatusEffectComponent, ComponentShutdown>(OnComponentShutdown);
_audioQuery = GetEntityQuery<AudioComponent>();
_gridQuery = GetEntityQuery<MapGridComponent>();
_roofQuery = GetEntityQuery<RoofComponent>();
}
private void OnComponentShutdown(Entity<WeatherStatusEffectComponent> ent, ref ComponentShutdown args)
@@ -0,0 +1,74 @@
#nullable enable
using System.Reflection;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using Robust.Shared.Configuration;
namespace Content.IntegrationTests.Fixtures.Attributes;
/// <summary>
/// Ensures the given CVar, on the given side (or both), is the given value.
/// Attribute version of <see cref="GameTest.OverrideCVar{T}"/>, and stores the old value the same way.
/// </summary>
/// <remarks>This only works with <see cref="GameTest"/> fixtures.</remarks>
/// <param name="side">The side to set the CVar on, or both.</param>
/// <param name="definitionType">The type the CVar is defined on.</param>
/// <param name="fieldName">The name of the static field defining the CVar.</param>
/// <param name="value">The value to set the CVar to.</param>
/// <example>
/// <code>
/// [Test]
/// [EnsureCVar(Side.Server, typeof(CCVars), nameof(CCVars.FlavorText), true)]
/// public async Task MyTest()
/// {
/// // CVar is set for you inside the test, and automatically un-set on teardown.
/// }
/// </code>
/// </example>
/// <seealso cref="GameTest"/>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public sealed class EnsureCVarAttribute(Side side, Type definitionType, string fieldName, object value) : Attribute, IGameTestModifier, IApplyToTest
{
public const string ClientEnsuredCVarsProperty = "ClientEnsuredCVars";
public const string ServerEnsuredCVarsProperty = "ServerEnsuredCVars";
Task IGameTestModifier.ApplyToTest(GameTest test)
{
var cvar = LookupCVar();
test.PreTestAddOverride(side, cvar.Name, value);
return Task.CompletedTask;
}
private CVarDef LookupCVar()
{
var field = definitionType.GetField(fieldName, BindingFlags.Static | BindingFlags.Public);
if (field is null)
throw new ArgumentException($"Couldn't find a public, static field named {fieldName} on {definitionType}");
var obj = field.GetValue(null);
if (obj is not CVarDef cvar)
{
throw new ArgumentException(
$"Expected a CVar definition on {definitionType}.{fieldName}, but it was a {obj?.GetType().FullName ?? "null"}");
}
if (value.GetType() != cvar.DefaultValue.GetType())
throw new NotSupportedException($"Cannot set {cvar.Name} to {value}, it's the wrong type.");
return cvar;
}
void IApplyToTest.ApplyToTest(Test test)
{
var cvar = LookupCVar();
if ((side & Side.Client) != 0)
test.Properties.Add(ClientEnsuredCVarsProperty, $"{cvar.Name} = {value}");
if ((side & Side.Server) != 0)
test.Properties.Add(ServerEnsuredCVarsProperty, $"{cvar.Name} = {value}");
}
}
@@ -0,0 +1,20 @@
namespace Content.IntegrationTests.Fixtures.Attributes;
/// <summary>
/// Marks an attribute as a modifier for <see cref="GameTest"/> fixtures.
/// These attributes can be applied to both test methods and fixtures.
/// </summary>
/// <remarks>
/// GameTest modifiers are <b>encouraged</b> to also implement IApplyToTest and add properties to the test
/// indicating their presence.
/// </remarks>
public interface IGameTestModifier
{
/// <summary>
/// Method called by GameTest on itself when applying <see cref="GameTest"/> modifiers.
/// </summary>
/// <param name="test">The test being modified</param>
/// <returns>Async task to await.</returns>
Task ApplyToTest(GameTest test);
}
@@ -0,0 +1,25 @@
#nullable enable
namespace Content.IntegrationTests.Fixtures.Attributes;
/// <summary>
/// Interface used for <see cref="GameTest"/> pair configuration attributes.
/// This allows such attributes to modify the pair settings, and also describe what parts of pairs they modify
/// so odd configuration choices can be spotted.
/// </summary>
public interface IGameTestPairConfigModifier
{
/// <summary>
/// Whether this modifier is exclusive and should conflict with other exclusive modifiers.
/// Essentially, fail immediately if other IGameTestPairConfigModifier attributes are present if this is set.
/// </summary>
bool Exclusive { get; }
/// <summary>
/// Called when GameTest needs its <see cref="PoolSettings"/> modified by the modifier.
/// </summary>
/// <param name="test">The test we're applying to.</param>
/// <param name="settings">The settings object to modify.</param>
void ApplyToPairSettings(GameTest test, ref PoolSettings settings);
}
@@ -0,0 +1,53 @@
#nullable enable
using System.Reflection;
namespace Content.IntegrationTests.Fixtures.Attributes;
/// <summary>
/// Configures the test pair using settings from the given type (by default the current test) and static property member.
/// </summary>
/// <param name="sourceType">The type to look up the member on, if any.</param>
/// <param name="sourceMember">The static property to read the settings from.</param>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public sealed class PairConfigAttribute(Type? sourceType, string sourceMember) : Attribute, IGameTestPairConfigModifier
{
public bool Exclusive => true;
public readonly Type? SourceType = sourceType;
public readonly string SourceMember = sourceMember;
private const BindingFlags PropertyBindingFlags = BindingFlags.Static
| BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.FlattenHierarchy;
public PairConfigAttribute(string sourceMember) : this(null, sourceMember)
{
}
public void ApplyToPairSettings(GameTest test, ref PoolSettings settings)
{
var sourceType = SourceType ?? test.GetType();
var property = sourceType.GetProperty(SourceMember, PropertyBindingFlags);
if (property is null)
{
if (sourceType.GetField(SourceMember, PropertyBindingFlags) is not null)
{
throw new ArgumentException(
$"Couldn't find static property {SourceMember} on {sourceType.Name}, but could find a field. Only properties are allowed.");
}
throw new ArgumentException($"Couldn't find static property {SourceMember} on {sourceType.Name}");
}
if (!property.PropertyType.IsAssignableTo(typeof(PoolSettings)))
{
throw new ArgumentException(
$"{sourceType.Name}.{SourceMember} is not assignable to {nameof(PoolSettings)} and cannot be used.");
}
settings = (PoolSettings)property.GetValue(null)!;
}
}
@@ -0,0 +1,79 @@
#nullable enable
using Content.IntegrationTests.NUnit.Utilities;
using NUnit.Framework.Interfaces;
using NUnit.Framework.Internal;
using NUnit.Framework.Internal.Commands;
namespace Content.IntegrationTests.Fixtures.Attributes;
/// <summary>
/// Ensures a test method runs on the given side (client or server, not neither nor both).
/// </summary>
/// <remarks>
/// This only works for <see cref="GameTest"/> fixtures.
/// </remarks>
/// <seealso cref="GameTest"/>
[AttributeUsage(AttributeTargets.Method)]
public sealed class RunOnSideAttribute : Attribute, IWrapTestMethod, IImplyFixture, IApplyToTest
{
public const string RunOnSideProperty = "RanOnSide";
/// <summary>
/// Which side to run the inner test code on, if not the test thread.
/// </summary>
public Side RunOnSide { get; }
public RunOnSideAttribute(Side side)
{
RunOnSide = side;
if (side is not Side.Client and not Side.Server)
throw new NotSupportedException("Test run-on-side can only be the client or server, not both or neither.");
}
TestCommand ICommandWrapper.Wrap(TestCommand command)
{
return new SidedTestCommand(command, RunOnSide);
}
private sealed class SidedTestCommand : DelegatingTestCommand
{
private readonly Side _side;
public SidedTestCommand(TestCommand inner, Side side) : base(inner)
{
_side = side;
}
public override TestResult Execute(TestExecutionContext context)
{
innerCommand.Test.EnsureFixtureIsGameTest(typeof(RunOnSideAttribute), out var gt);
if (_side is not Side.Client and not Side.Server)
throw new NotSupportedException($"Sided tests need to specify a specific side. {Test}");
if (_side is Side.Client)
{
gt.Client.WaitAssertion(() =>
{
context.CurrentResult = innerCommand.Execute(context);
})
.Wait();
}
else
{
gt.Server.WaitAssertion(() =>
{
context.CurrentResult = innerCommand.Execute(context);
})
.Wait();
}
return context.CurrentResult;
}
}
public void ApplyToTest(Test test)
{
test.Properties.Add(RunOnSideProperty, RunOnSide.ToString());
}
}
@@ -0,0 +1,27 @@
#nullable enable
namespace Content.IntegrationTests.Fixtures.Attributes;
/// <summary>
/// A flag enum representing a side of a testpair.
/// </summary>
[Flags]
public enum Side : byte
{
/// <summary>
/// Bitflag representing the client side of a testpair.
/// </summary>
Client = 1,
/// <summary>
/// Bitflag representing the server side of a testpair.
/// </summary>
Server = 2,
/// <summary>
/// A value indicating no side was specified. You shouldn't use this outside of checking for it as an error.
/// </summary>
Neither = 0,
/// <summary>
/// A value indicating both sides were specified.
/// </summary>
Both = Client | Server,
}
@@ -0,0 +1,23 @@
#nullable enable
namespace Content.IntegrationTests.Fixtures.Attributes;
/// <summary>
/// Marks a field on a <see cref="GameTest"/> fixture as needing to be populated with an IoC dependency from the given side.
/// </summary>
/// <seealso cref="GameTest"/>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class SidedDependencyAttribute : Attribute
{
public SidedDependencyAttribute(Side side)
{
Side = side;
if (side is not Side.Client and not Side.Server)
{
throw new NotSupportedException($"Expected either the client or the server as a side, got {side}.");
}
}
public Side Side { get; }
}
@@ -0,0 +1,53 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
namespace Content.IntegrationTests.Fixtures.Attributes;
/// <summary>
/// <para>
/// An attribute meant to attach an issue (usually related to the test) to a given test or test fixture.
/// This sets the <c>TrackingIssue</c> property on the test, and helps developers find why a test exists or why it
/// is broken.
/// </para>
/// <para>
/// This attribute should be used if a test corresponds directly to a bug in some way, either demonstrating it or
/// ensuring it remains fixed. Only URLs should be provided, lone issue numbers are not accepted.
/// </para>
/// <para>
/// If the bug was never given an issue, the fix PR containing the test is another acceptable thing to link, and the
/// PR should clearly explain the bug it is fixing for future readers.
/// </para>
/// </summary>
public sealed class TrackingIssueAttribute : PropertyAttribute
{
/// <summary>
/// Domains we allow for tracking issues, to avoid people putting discord or discourse links.
/// </summary>
private static readonly string[] _validDomains =
[
"github.com"
];
private static readonly Regex GithubStyleIssueMatch = new(@"^\/[a-z\d\-\$\#]*\/[a-z\d\-\$\#]*\/(issues|pulls)\/\d*$",
RegexOptions.Compiled | RegexOptions.NonBacktracking | RegexOptions.IgnoreCase);
public TrackingIssueAttribute([StringSyntax(StringSyntaxAttribute.Uri)] string url) : base(url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
throw new ArgumentException($"Expected a valid URL for {nameof(TrackingIssueAttribute)}, got {url}");
// Assert the domain is reasonable.
if (!_validDomains.Contains(uri.Host, StringComparer.InvariantCultureIgnoreCase))
{
throw new ArgumentException(
$"Didn't recognize the domain used for the tracking issue, got {uri.Host}. We support: {string.Join(", ", _validDomains)}");
}
// Assert that the URL is reasonable.
if (!GithubStyleIssueMatch.IsMatch(uri.AbsolutePath))
{
throw new ArgumentException(
$"Didn't recognize the provided github link, it should point to a specific pull request or issue. Got {uri.AbsolutePath}");
}
}
}
@@ -0,0 +1,83 @@
#nullable enable
using System.Collections.Generic;
using Content.IntegrationTests.Fixtures.Attributes;
using Robust.Shared.Configuration;
namespace Content.IntegrationTests.Fixtures;
// REMARK: You may be wondering why this doesn't bother storing the old CVars.
// This is because TestPair actually has some not-well-known functionality to
// automatically restore CVars to what they were pre-test for you.
//
// So instead of rolling that twice, this lets TestPair handle it.
public abstract partial class GameTest
{
[SidedDependency(Side.Server)] private readonly IConfigurationManager _serverCfg = default!;
[SidedDependency(Side.Client)] private readonly IConfigurationManager _clientCfg = default!;
private readonly Dictionary<string, object> _clientCVarOverrides = new();
private readonly Dictionary<string, object> _serverCVarOverrides = new();
/// <summary>
/// Adds a setup-time override for a given cvar, for use by <see cref="IGameTestModifier"/>s.
/// </summary>
public void PreTestAddOverride(Side side, string cVar, object value)
{
// ReSharper disable once ConditionIsAlwaysTrueOrFalse
if (_setupDone)
throw new NotSupportedException("Cannot use PreTest functions after test SetUp.");
if (side is Side.Neither)
throw new NotSupportedException($"Must specify a side, or both, for {nameof(PreTestAddOverride)}");
if ((side & Side.Server) != 0)
_serverCVarOverrides.Add(cVar, value);
if ((side & Side.Client) != 0)
_clientCVarOverrides.Add(cVar, value);
}
private async Task DoPreTestOverrides()
{
foreach (var (cvar, value) in _clientCVarOverrides)
{
await OverrideCVarByName(Side.Client, cvar, value, false);
}
foreach (var (cvar, value) in _serverCVarOverrides)
{
await OverrideCVarByName(Side.Server, cvar, value, false);
}
await Pair.RunUntilSynced();
}
/// <summary>
/// Sets a given CVar for the provided side.
/// </summary>
/// <remarks>Does its own cleanup, you do not need to set the CVar back yourself.</remarks>
public async Task OverrideCVar<T>(Side side, CVarDef<T> cvar, T value, bool sync = true)
where T: notnull
{
await OverrideCVarByName(side, cvar.Name, value, sync);
}
private async Task OverrideCVarByName(Side side, string cVar, object value, bool sync)
{
if (side is Side.Client)
{
_clientCfg.SetCVar(cVar, value);
}
else if (side is Side.Server)
{
_serverCfg.SetCVar(cVar, value);
}
else
{
throw new NotSupportedException($"Expected a specific side, got {side}.");
}
if (sync)
await Pair.RunUntilSynced();
}
}
@@ -0,0 +1,12 @@
namespace Content.IntegrationTests.Fixtures;
public abstract partial class GameTest
{
/// <summary>
/// All-default-settings PoolSettings, with the client and server disconnected.
/// </summary>
protected static PoolSettings PsDisconnected => new()
{
Connected = false,
};
}
@@ -0,0 +1,228 @@
#nullable enable
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
namespace Content.IntegrationTests.Fixtures;
public abstract partial class GameTest
{
/// <summary>
/// Contains all server entities spawned using GameTest proxy methods.
/// </summary>
private readonly List<EntityUid> _serverEntitiesToClean = new();
/// <summary>
/// Contains all client entities spawned using GameTest proxy methods.
/// </summary>
private readonly List<EntityUid> _clientEntitiesToClean = new();
private async Task CleanUpEntities()
{
await Task.WhenAll(
Server.WaitAssertion(() =>
{
foreach (var junk in _serverEntitiesToClean)
{
if (!SEntMan.Deleted(junk))
SEntMan.DeleteEntity(junk);
}
}),
Client.WaitAssertion(() =>
{
foreach (var junk in _clientEntitiesToClean)
{
if (!CEntMan.Deleted(junk))
CEntMan.DeleteEntity(junk);
}
})
);
}
/// <summary>
/// Returns a string representation of an entity for the server.
/// </summary>
public string SToPrettyString(EntityUid uid)
{
return Pair.Server.EntMan.ToPrettyString(uid);
}
/// <summary>
/// Returns a string representation of an entity for the client.
/// </summary>
public string CToPrettyString(EntityUid uid)
{
return Pair.Client.EntMan.ToPrettyString(uid);
}
/// <summary>
/// Converts a server EntityUid into the client-side equivalent entity.
/// </summary>
public EntityUid ToClientUid(EntityUid serverUid)
{
return Pair.ToClientUid(serverUid);
}
/// <summary>
/// Converts a client EntityUid into the server-side equivalent entity.
/// </summary>
public EntityUid ToServerUid(EntityUid clientUid)
{
return Pair.ToServerUid(clientUid);
}
/// <summary>
/// Retrieves the given component from an entity on the server.
/// </summary>
public T SComp<T>(EntityUid target)
where T : IComponent
{
return SEntMan.GetComponent<T>(target);
}
/// <summary>
/// Attempts to retrieve the given component from an entity on the server.
/// </summary>
public bool STryComp<T>(EntityUid? target, [NotNullWhen(true)] out T? component)
where T : IComponent
{
return SEntMan.TryGetComponent(target, out component);
}
/// <summary>
/// Retrieves the given component from an entity on the client.
/// </summary>
public T CComp<T>(EntityUid target)
where T : IComponent
{
return CEntMan.GetComponent<T>(target);
}
/// <summary>
/// Attempts to retrieve the given component from an entity on the server.
/// </summary>
public bool CTryComp<T>(EntityUid? target, [NotNullWhen(true)] out T? component)
where T : IComponent
{
return SEntMan.TryGetComponent(target, out component);
}
/// <summary>
/// Pairs an EntityUid with the given component, from the server.
/// </summary>
public Entity<T> SEntity<T>(EntityUid target)
where T : IComponent
{
return new(target, SEntMan.GetComponent<T>(target));
}
/// <summary>
/// Pairs an EntityUid with the given component, from the client.
/// </summary>
public Entity<T> CEntity<T>(EntityUid target)
where T : IComponent
{
return new(target, CEntMan.GetComponent<T>(target));
}
/// <summary>
/// Spawns an entity on the server.
/// </summary>
/// <remarks>This tracks the entity for post-test cleanup.</remarks>
public EntityUid SSpawn(string? id)
{
var res = SEntMan.Spawn(id);
_serverEntitiesToClean.Add(res);
return res;
}
/// <summary>
/// Spawns an entity on the server at a location.
/// </summary>
/// <remarks>This tracks the entity for post-test cleanup.</remarks>
public EntityUid SSpawnAtPosition(string? id, EntityCoordinates coordinates)
{
var res = SEntMan.SpawnAtPosition(id, coordinates);
_serverEntitiesToClean.Add(res);
return res;
}
/// <summary>
/// Spawns an entity on the client.
/// </summary>
/// <remarks>This tracks the entity for post-test cleanup.</remarks>
public EntityUid CSpawn(string? id)
{
var res = CEntMan.Spawn(id);
_clientEntitiesToClean.Add(res);
return res;
}
/// <summary>
/// Spawns an entity on the server at a location.
/// </summary>
/// <remarks>This tracks the entity for post-test cleanup.</remarks>
public EntityUid CSpawnAtPosition(string? id, EntityCoordinates coordinates)
{
var res = CEntMan.SpawnAtPosition(id, coordinates);
_clientEntitiesToClean.Add(res);
return res;
}
/// <summary>
/// Asynchronously spawns an entity on the server.
/// </summary>
public async Task<EntityUid> Spawn(string? id)
{
var ent = EntityUid.Invalid;
await Server.WaitPost(() => ent = SSpawn(id));
return ent;
}
/// <summary>
/// Asynchronously spawns an entity on the server at the given position.
/// </summary>
public async Task<EntityUid> SpawnAtPosition(string? id, EntityCoordinates coords)
{
var ent = EntityUid.Invalid;
await Server.WaitPost(() => ent = SSpawnAtPosition(id, coords));
return ent;
}
/// <summary>
/// Deletes an entity on the server immediately.
/// </summary>
public void SDeleteNow(EntityUid id)
{
SEntMan.DeleteEntity(id);
}
/// <summary>
/// Deletes an entity on the client immediately.
/// </summary>
public void CDeleteNow(EntityUid id)
{
CEntMan.DeleteEntity(id);
}
/// <summary>
/// Queues an entity for deletion at the end of the tick on the server.
/// </summary>
public void SQueueDel(EntityUid id)
{
SEntMan.QueueDeleteEntity(id);
}
/// <summary>
/// Queues an entity for deletion at the end of the tick on the client.
/// </summary>
public void CQueueDel(EntityUid id)
{
CEntMan.QueueDeleteEntity(id);
}
}
@@ -0,0 +1,36 @@
namespace Content.IntegrationTests.Fixtures;
public abstract partial class GameTest
{
/// <summary>
/// Runs the client and server for the given number of ticks, in lockstep.
/// </summary>
/// <remarks>
/// Do not use this as a barrier for client-server synchronization, use <see cref="RunUntilSynced"/>.
/// </remarks>
public Task RunTicksSync(int ticks)
{
return Pair.RunTicksSync(ticks);
}
/// <summary>
/// Runs the pairs just long enough for PVS to send entities, ensuring the client's current tick is what the
/// server's was at call time.
/// </summary>
public async Task RunUntilSynced()
{
await Pair.RunUntilSynced();
}
/// <summary>
/// Runs the test pair for a number of (simulated) seconds.
/// </summary>
/// <remarks>
/// Does not actually take N seconds to evaluate, the game ticks as fast as possible.
/// Do not use this as a barrier for client-server synchronization, use <see cref="RunUntilSynced"/>.
/// </remarks>
public Task RunSeconds(float seconds)
{
return Pair.RunSeconds(seconds);
}
}
@@ -0,0 +1,265 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.IntegrationTests.NUnit.Constraints;
using Content.IntegrationTests.Pair;
using Content.IntegrationTests.Utility;
using NUnit.Framework.Interfaces;
using Robust.Client.Timing;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.UnitTesting;
namespace Content.IntegrationTests.Fixtures;
/// <summary>
/// <para>
/// A test fixture with an integrated <see cref="GameTest.Pair">test pair</see>,
/// proxy methods for efficient test writing, utilities for ensuring tests clean up correctly,
/// and dependency injection (<see cref="SidedDependencyAttribute"/>).
/// </para>
/// <para>
/// Tests using GameTest support some additional class and method level attributes, namely
/// <see cref="RunOnSideAttribute"/>.
/// Attributes can be used to control how the test runs.
/// </para>
/// </summary>
/// <seealso cref="CompConstraintExtensions"/>
/// <seealso cref="LifeStageConstraintExtensions"/>
[TestFixture]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
[Property(TestProperties.TestFrameKind, nameof(GameTest))]
[SuppressMessage("Structure", "NUnit1028:The non-test method is public")]
public abstract partial class GameTest
{
/// <summary>
/// Set if the test manually marks itself dirty.
/// </summary>
private bool _pairDestroyed;
/// <summary>
/// Tests-testing-tests assistant to run right before the pair is returned.
/// </summary>
public event Action? PreFinalizeHook;
/// <summary>
/// The main thread of the game server.
/// </summary>
public Thread ServerThread { get; private set; } = null!; // NULLABILITY: This is always set during test setup.
/// <summary>
/// The main thread of the game client.
/// </summary>
public Thread ClientThread { get; private set; } = null!; // NULLABILITY: This is always set during test setup.
/// <summary>
/// Settings for the client/server pair.
/// By default, this gets you a client and server that have connected together.
/// </summary>
/// <remarks>
/// Always return a new instance whenever this is read. In other words, no backing field please. Arrow syntax only.
/// </remarks>
public virtual PoolSettings PoolSettings => new() { Connected = true };
/// <summary>
/// The client and server pair.
/// </summary>
public TestPair Pair { get; private set; } = default!; // NULLABILITY: This is always set during test setup.
/// <summary>
/// The game server instance.
/// </summary>
public RobustIntegrationTest.ServerIntegrationInstance Server => Pair.Server;
/// <summary>
/// The game client instance.
/// </summary>
public RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client;
/// <summary>
/// The test player's server session, if any.
/// </summary>
public ICommonSession? ServerSession => Pair.Player;
/// <summary>
/// The server-side entity manager.
/// </summary>
[SidedDependency(Side.Server)]
public IEntityManager SEntMan = null!;
/// <summary>
/// The client-side entity manager.
/// </summary>
[SidedDependency(Side.Client)]
public IEntityManager CEntMan = null!;
/// <summary>
/// The server-side prototype manager.
/// </summary>
[SidedDependency(Side.Server)]
public IPrototypeManager SProtoMan = null!;
/// <summary>
/// The client-side prototype manager.
/// </summary>
[SidedDependency(Side.Client)]
public IPrototypeManager CProtoMan = null!;
/// <summary>
/// The server-side game-timing manager.
/// </summary>
[SidedDependency(Side.Server)]
public IGameTiming SGameTiming = null!;
/// <summary>
/// The client-side game-timing manager.
/// </summary>
[SidedDependency(Side.Client)]
public IClientGameTiming CGameTiming = null!;
/// <summary>
/// The test map we're using, if any.
/// </summary>
public TestMapData? TestMap => Pair.TestMap;
private bool _setupDone = false;
/// <summary>
/// Primary setup task for the fixture.
/// Custom setup must run after this.
/// </summary>
[SetUp]
public virtual async Task DoSetup()
{
_pairDestroyed = false;
var testContext = TestContext.CurrentContext;
var test = testContext.Test;
var settings = PoolSettings;
var pairAttribs = test.Method!.GetCustomAttributes<IGameTestPairConfigModifier>(false);
var pairSuiteAttribs = test.Method!.TypeInfo.GetCustomAttributes<IGameTestPairConfigModifier>(true);
if (pairAttribs.Length > 1 && pairAttribs.Any(x => x.Exclusive))
{
throw new InvalidOperationException(
"More than one exclusive pair config attribute is present on the test member.");
}
if (pairSuiteAttribs.Length > 1 && pairSuiteAttribs.Any(x => x.Exclusive))
{
throw new InvalidOperationException(
"More than one exclusive pair config attribute is present on the test fixture.");
}
foreach (var attribute in pairSuiteAttribs.Concat(pairAttribs))
{
attribute.ApplyToPairSettings(this, ref settings);
}
Pair = await PoolManager.GetServerClient(settings, new NUnitTestContextWrap(testContext, TestContext.Out));
Task.WaitAll(
Server.WaitPost(() => ServerThread = Thread.CurrentThread),
Client.WaitPost(() => ClientThread = Thread.CurrentThread)
);
await Pair.ReallyBeIdle(5); // Arbitrary setup time wait.
InjectDependencies(this);
var attribs = test.Method!.GetCustomAttributes<IGameTestModifier>(false);
var suiteAttribs = test.Method!.TypeInfo.GetCustomAttributes<IGameTestModifier>(true);
foreach (var attribute in suiteAttribs.Concat(attribs))
{
await attribute.ApplyToTest(this);
}
_setupDone = true;
await DoPreTestOverrides();
await Pair.RunUntilSynced();
}
/// <summary>
/// Injects <see cref="SidedDependencyAttribute"/> dependencies into the target object.
/// </summary>
/// <remarks>
/// This is called on the GameTest itself automatically. Don't call it twice on the same object.
/// </remarks>
/// <param name="target">The object to inject into.</param>
public void InjectDependencies(object target)
{
foreach (var field in target.GetType().GetAllFields())
{
if (field.GetCustomAttribute<SidedDependencyAttribute>() is { } depAttrib)
{
// ReSharper disable once ConvertIfStatementToConditionalTernaryExpression
if (depAttrib.Side is Side.Server)
{
field.SetValue(target, Server.EntMan.EntitySysManager.DependencyCollection.ResolveType(field.FieldType));
}
else
{
// Must be initially connected for this...
if (Client.Session is not null)
field.SetValue(target, Client.EntMan.EntitySysManager.DependencyCollection.ResolveType(field.FieldType));
else
field.SetValue(target, Client.InstanceDependencyCollection.ResolveType(field.FieldType));
}
}
}
}
/// <summary>
/// Primary teardown task for the fixture.
/// Custom teardown must run before this.
/// </summary>
[TearDown]
public virtual async Task DoTeardown()
{
try
{
// In some cool future we might be able to make this only throw out the pair
// if the test threw exceptions. But that'd require fixing all of them to do cleanup properly on failure.
//
// So not yet.
if (TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Failed)
{
_pairDestroyed = true; // Blow it up, we failed and it might be screwed.
return;
}
// Roll forward til sync for teardown.
await Pair.RunUntilSynced();
await CleanUpEntities();
// And other teardown logic will go here. Eventually.
}
catch (Exception)
{
_pairDestroyed = true;
throw;
}
finally
{
PreFinalizeHook?.Invoke();
if (!_pairDestroyed)
await Pair.CleanReturnAsync();
else
await Pair.DisposeAsync();
}
}
}
@@ -0,0 +1,33 @@
using NUnit.Framework.Constraints;
using NUnit.Framework.Internal;
using Robust.UnitTesting;
namespace Content.IntegrationTests.NUnit.Constraints;
/// <summary>
/// A prefix constraint like <see cref="PropertyConstraint"/>, for entity components.
/// </summary>
/// <seealso cref="CompConstraintExtensions"/>
public sealed class CompConstraint(Type tComp, IIntegrationInstance instance, IConstraint baseConstraint)
: PrefixConstraint(baseConstraint, $"component {tComp.Name}")
{
public override ConstraintResult ApplyTo<TActual>(TActual actual)
{
if (!ConstraintHelpers.TryActualAsEnt(actual, instance, out var ent, out var error))
{
if (error)
{
throw new NotImplementedException(
$"The input type {typeof(TActual)} to {nameof(CompExistsConstraint)} is not a supported entity id.");
}
return new ConstraintResult(this, actual, ConstraintStatus.Failure);
}
if (!instance.EntMan.TryGetComponent(ent, tComp, out var comp))
return new ConstraintResult(this, actual, ConstraintStatus.Failure);
var baseResult = Reflect.InvokeApplyTo(constraint: baseConstraint, tComp, comp);
return new ConstraintResult(this, baseResult.ActualValue, baseResult.Status);
}
}
@@ -0,0 +1,48 @@
#nullable enable
using Content.IntegrationTests.NUnit.Operators;
using NUnit.Framework.Constraints;
using Robust.Shared.GameObjects;
using Robust.UnitTesting;
namespace Content.IntegrationTests.NUnit.Constraints;
/// <summary>
/// Provides <see cref="M:Content.IntegrationTests.NUnit.Constraints.CompConstraintExtensions.extension(NUnit.Framework.Has).Comp``1(Robust.UnitTesting.IIntegrationInstance)">Has.Comp&lt;T&gt;(side)</see>,
/// a constraint that allows you to check for the presence of, or operate on, a component.
/// </summary>
/// <example>
/// <code>
/// // Assert that the server sided entity myEntity has ItemComponent on the server.
/// Assert.That(myEntity, Has.Comp&lt;ItemComponent&gt;(Server));
/// </code>
/// </example>
public static class CompConstraintExtensions
{
extension(Has)
{
public static ResolvableConstraintExpression Comp<T>(IIntegrationInstance instance)
where T : IComponent
{
return new ConstraintExpression().Comp<T>(instance);
}
public static ResolvableConstraintExpression Comp(Type t, IIntegrationInstance instance)
{
return new ConstraintExpression().Comp(t, instance);
}
}
extension(ConstraintExpression expr)
{
public ResolvableConstraintExpression Comp<T>(IIntegrationInstance instance)
where T : IComponent
{
return expr.Append(new CompOperator(typeof(T), instance));
}
public ResolvableConstraintExpression Comp(Type t, IIntegrationInstance instance)
{
return expr.Append(new CompOperator(t, instance));
}
}
}
@@ -0,0 +1,30 @@
#nullable enable
using NUnit.Framework.Constraints;
using Robust.UnitTesting;
namespace Content.IntegrationTests.NUnit.Constraints;
/// <summary>
/// Constraint for whether a component exists.
/// </summary>
/// <seealso cref="CompConstraintExtensions"/>
public sealed class CompExistsConstraint(Type component, IIntegrationInstance instance) : Constraint
{
public override ConstraintResult ApplyTo<TActual>(TActual actual)
{
if (!ConstraintHelpers.TryActualAsEnt(actual, instance, out var ent, out var error))
{
if (error)
{
throw new NotImplementedException(
$"The input type {typeof(TActual)} to {nameof(CompExistsConstraint)} is not a supported entity id.");
}
return new ConstraintResult(this, actual, ConstraintStatus.Failure);
}
return new ConstraintResult(this, actual, instance.EntMan.HasComponent(ent, component));
}
public override string Description => $"has the component {component.Name}";
}
@@ -0,0 +1,66 @@
#nullable enable
using System.Diagnostics.CodeAnalysis;
using Content.IntegrationTests.NUnit.Utilities;
using Robust.Shared.GameObjects;
using Robust.Shared.Toolshed.TypeParsers;
using Robust.UnitTesting;
namespace Content.IntegrationTests.NUnit.Constraints;
public static class ConstraintHelpers
{
/// <summary>
/// A constraint implementation helper to convert TActual into an entityuid.
/// </summary>
/// <param name="t">The input value to try to get an entity uid from.</param>
/// <param name="instance">The integration test instance to resolve the entity from.</param>
/// <param name="ent">The resulting entity uid.</param>
/// <param name="validType">Whether TActual is recognized to begin with.</param>
/// <typeparam name="TActual">The type to cast out of.</typeparam>
public static bool TryActualAsEnt<TActual>(TActual t, IIntegrationInstance instance, [NotNullWhen(true)] out EntityUid? ent, out bool validType)
{
if (t is EntityUid u)
{
ent = u;
validType = false;
return true;
}
if (t is IAsType<EntityUid> asTy)
{
ent = asTy.AsType();
validType = false;
return true;
}
if (t is IResolvesToEntity resolvable)
{
if (instance is IServerIntegrationInstance)
{
ent = resolvable.SEntity;
}
else if (instance is IClientIntegrationInstance)
{
ent = resolvable.CEntity;
}
else
{
throw new NotSupportedException($"{t.GetType()} is not a valid kind of IIntegrationInstance");
}
validType = false;
return ent is not null;
}
if (t is null)
{
ent = null;
validType = false;
return false;
}
ent = null;
validType = true; // Dunno what this type is!
return false;
}
}
@@ -0,0 +1,136 @@
#nullable enable
using NUnit.Framework.Constraints;
using Robust.Shared.GameObjects;
using Robust.UnitTesting;
namespace Content.IntegrationTests.NUnit.Constraints;
/// <summary>
/// A constraint for an entity's lifestage.
/// </summary>
/// <seealso cref="LifeStageConstraintExtensions"/>
public sealed class LifeStageConstraint(EntityLifeStage stage, IIntegrationInstance instance) : Constraint
{
public override ConstraintResult ApplyTo<TActual>(TActual actual)
{
if (!ConstraintHelpers.TryActualAsEnt(actual, instance, out var ent, out var error))
{
if (error)
{
throw new NotImplementedException(
$"The input type {typeof(TActual)} to {nameof(CompExistsConstraint)} is not a supported entity id.");
}
return new ConstraintResult(this, actual, ConstraintStatus.Failure);
}
var lifestage = instance.EntMan.GetComponentOrNull<MetaDataComponent>(ent.Value)?.EntityLifeStage;
return new ConstraintResult(this,
lifestage,
lifestage == stage || (lifestage is null && stage is EntityLifeStage.Deleted));
}
public override string Description => stage switch
{
EntityLifeStage.PreInit => "preinitialized",
EntityLifeStage.Initializing => "initializing",
EntityLifeStage.Initialized => "initialized",
EntityLifeStage.MapInitialized => "map initialized",
EntityLifeStage.Terminating => "terminating",
EntityLifeStage.Deleted => "deleted",
_ => throw new ArgumentOutOfRangeException(nameof(stage), stage, null),
};
}
/// <summary>
/// Provides constraints for testing if an entity is in the given lifestage.
/// </summary>
/// <example>
/// <code>
/// // Assert that the server sided entity myEntity is MapInitialized.
/// Assert.That(myEntity, Is.MapInitialized(Server));
/// </code>
/// </example>
public static class LifeStageConstraintExtensions
{
extension(Is)
{
public static LifeStageConstraint LifeStage(EntityLifeStage stage, IIntegrationInstance instance)
{
return new LifeStageConstraint(stage, instance);
}
public static LifeStageConstraint PreInit(IIntegrationInstance instance)
{
return Is.LifeStage(EntityLifeStage.PreInit, instance);
}
public static LifeStageConstraint Initializing(IIntegrationInstance instance)
{
return Is.LifeStage(EntityLifeStage.Initializing, instance);
}
public static LifeStageConstraint Initialized(IIntegrationInstance instance)
{
return Is.LifeStage(EntityLifeStage.Initialized, instance);
}
public static LifeStageConstraint MapInitialized(IIntegrationInstance instance)
{
return Is.LifeStage(EntityLifeStage.MapInitialized, instance);
}
public static LifeStageConstraint Terminating(IIntegrationInstance instance)
{
return Is.LifeStage(EntityLifeStage.Terminating, instance);
}
public static LifeStageConstraint Deleted(IIntegrationInstance instance)
{
return Is.LifeStage(EntityLifeStage.Deleted, instance);
}
}
extension(ConstraintExpression expr)
{
public LifeStageConstraint LifeStage(EntityLifeStage stage, IIntegrationInstance instance)
{
var c = new LifeStageConstraint(stage, instance);
expr.Append(c);
return c;
}
public LifeStageConstraint PreInit(IIntegrationInstance instance)
{
return expr.LifeStage(EntityLifeStage.PreInit, instance);
}
public LifeStageConstraint Initializing(IIntegrationInstance instance)
{
return expr.LifeStage(EntityLifeStage.Initializing, instance);
}
public LifeStageConstraint Initialized(IIntegrationInstance instance)
{
return expr.LifeStage(EntityLifeStage.Initialized, instance);
}
public LifeStageConstraint MapInitialized(IIntegrationInstance instance)
{
return expr.LifeStage(EntityLifeStage.MapInitialized, instance);
}
public LifeStageConstraint Terminating(IIntegrationInstance instance)
{
return expr.LifeStage(EntityLifeStage.Terminating, instance);
}
public LifeStageConstraint Deleted(IIntegrationInstance instance)
{
return expr.LifeStage(EntityLifeStage.Deleted, instance);
}
}
}
@@ -0,0 +1,31 @@
using Content.IntegrationTests.NUnit.Constraints;
using NUnit.Framework.Constraints;
using Robust.UnitTesting;
namespace Content.IntegrationTests.NUnit.Operators;
/// <summary>
/// An operator for use by nunit constraint resolution.
/// </summary>
/// <seealso cref="CompExistsConstraint"/>
public sealed class CompOperator : SelfResolvingOperator
{
private readonly Type _tComp;
private readonly IIntegrationInstance _instance;
public CompOperator(Type tComp, IIntegrationInstance instance)
{
_tComp = tComp;
_instance = instance;
left_precedence = right_precedence = 1;
}
public override void Reduce(ConstraintBuilder.ConstraintStack stack)
{
if (RightContext is null or BinaryOperator)
stack.Push(new CompExistsConstraint(_tComp, _instance));
else
stack.Push(new CompConstraint(_tComp, _instance, stack.Pop()));
}
}
@@ -0,0 +1,19 @@
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.NUnit.Utilities;
/// <summary>
/// An interface for objects that NUnit constraints should treat as a sided entity.
/// </summary>
public interface IResolvesToEntity
{
/// <summary>
/// The server-sided entity, if any.
/// </summary>
EntityUid? SEntity { get; }
/// <summary>
/// The client-sided entity, if any.
/// </summary>
EntityUid? CEntity { get; }
}
@@ -0,0 +1,28 @@
using Content.IntegrationTests.Fixtures;
using NUnit.Framework.Interfaces;
namespace Content.IntegrationTests.NUnit.Utilities;
public static class ITestExtensions
{
extension<T>(T test)
where T : ITest
{
/// <summary>
/// Ensures the given fixture is a <see cref="GameTest"/>, and if not gives a nice error message.
/// </summary>
/// <param name="callingType">The caller's type, usually an attribute.</param>
/// <param name="gt">The <see cref="GameTest"/>.</param>
/// <exception cref="NotSupportedException">Thrown when the given test isn't a <see cref="GameTest"/></exception>
public void EnsureFixtureIsGameTest(Type callingType, out GameTest gt)
{
if (test.Fixture is not GameTest gameTest)
{
throw new NotSupportedException(
$"The fixture {test.Fixture?.GetType()} needs to be a GameTest for {callingType.Name} to work.");
}
gt = gameTest;
}
}
}
@@ -25,7 +25,6 @@ public static partial class PoolManager
(CCVars.ArrivalsShuttles.Name, "false"),
(CCVars.EmergencyShuttleEnabled.Name, "false"),
(CCVars.ProcgenPreload.Name, "false"),
(CCVars.WorldgenEnabled.Name, "false"),
(CCVars.GatewayGeneratorEnabled.Name, "false"),
(CCVars.GameDummyTicker.Name, "true"),
(CCVars.GameLobbyEnabled.Name, "false"),

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