mirror of
https://github.com/corvax-team/ss14-wl.git
synced 2026-06-09 10:06:46 +02:00
Merge remote-tracking branch 'corvax/master' into upstream28.04
This commit is contained in:
+1
-1
@@ -12,4 +12,4 @@
|
||||
/Resources/Maps/_WL/** @0leshe
|
||||
|
||||
# Sprites
|
||||
/Resources/Textures/** @Morb0 @DIMMoon1 @SonicHDC
|
||||
/Resources/Textures/** @Morb0 @DIMMoon1 @MureixloI
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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>()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<T>(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<ItemComponent>(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
Reference in New Issue
Block a user