master 21.7 syns

This commit is contained in:
Dmitry
2025-07-21 00:27:46 +07:00
1361 changed files with 25466 additions and 16320 deletions

View File

@@ -77,6 +77,8 @@ csharp_style_expression_bodied_methods = false:suggestion
#csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_namespace_declarations = file_scoped:suggestion
# Pattern matching preferences
#csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
#csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion

View File

@@ -8,6 +8,7 @@ using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Benchmarks;
@@ -18,9 +19,11 @@ namespace Content.Benchmarks;
[Virtual, MemoryDiagnoser]
public class SpawnEquipDeleteBenchmark
{
private static readonly EntProtoId Mob = "MobHuman";
private static readonly ProtoId<StartingGearPrototype> CaptainStartingGear = "CaptainGear";
private TestPair _pair = default!;
private StationSpawningSystem _spawnSys = default!;
private const string Mob = "MobHuman";
private StartingGearPrototype _gear = default!;
private EntityUid _entity;
private EntityCoordinates _coords;
@@ -39,7 +42,7 @@ public class SpawnEquipDeleteBenchmark
var mapData = await _pair.CreateTestMap();
_coords = mapData.GridCoords;
_spawnSys = server.System<StationSpawningSystem>();
_gear = server.ProtoMan.Index<StartingGearPrototype>("CaptainGear");
_gear = server.ProtoMan.Index(CaptainStartingGear);
}
[GlobalCleanup]

View File

@@ -15,6 +15,8 @@ namespace Content.Client.Access.UI;
[GenerateTypedNameReferences]
public sealed partial class GroupedAccessLevelChecklist : BoxContainer
{
private static readonly ProtoId<AccessGroupPrototype> GeneralAccessGroup = "General";
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private bool _isMonotone;
@@ -63,7 +65,7 @@ public sealed partial class GroupedAccessLevelChecklist : BoxContainer
// Ensure that the 'general' access group is added to handle
// misc. access levels that aren't associated with any group
if (_protoManager.TryIndex<AccessGroupPrototype>("General", out var generalAccessProto))
if (_protoManager.TryIndex(GeneralAccessGroup, out var generalAccessProto))
_groupedAccessLevels.TryAdd(generalAccessProto, new());
// Assign known access levels with their associated groups

View File

@@ -8,6 +8,21 @@
<OptionButton Name="SolutionOption" HorizontalExpand="True"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 4">
<Button Name="VVButton"
Text="{Loc 'admin-solutions-window-vv-button'}"
ToolTip="{Loc 'admin-solutions-window-vv-button-tooltip'}"
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenRight"/>
<Button Name="SolutionButton"
Text="{Loc 'admin-solutions-window-solution-button'}"
ToolTip="{Loc 'admin-solutions-window-solution-button-tooltip'}"
Disabled="True"
HorizontalExpand="True"
StyleClasses="OpenLeft"/>
</BoxContainer>
<!-- The total volume / capacity of the solution -->
<BoxContainer Name="VolumeBox" Orientation="Vertical" HorizontalExpand="True" Margin="0 4"/>

View File

@@ -1,3 +1,4 @@
using Content.Client.Administration.Managers;
using Content.Shared.Administration;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
@@ -20,6 +21,7 @@ namespace Content.Client.Administration.UI.ManageSolutions
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
[Dependency] private readonly IClientAdminManager _admin = default!;
private NetEntity _target = NetEntity.Invalid;
private string? _selectedSolution;
@@ -34,6 +36,11 @@ namespace Content.Client.Administration.UI.ManageSolutions
SolutionOption.OnItemSelected += SolutionSelected;
AddButton.OnPressed += OpenAddReagentWindow;
VVButton.OnPressed += OpenVVWindow;
SolutionButton.OnPressed += OpenSolutionWindow;
VVButton.Disabled = !_admin.CanViewVar();
SolutionButton.Disabled = !_admin.CanViewVar();
}
public override void Close()
@@ -271,6 +278,32 @@ namespace Content.Client.Administration.UI.ManageSolutions
_addReagentWindow.OpenCentered();
}
/// <summary>
/// Open the corresponding solution entity in a ViewVariables window.
/// </summary>
private void OpenVVWindow(BaseButton.ButtonEventArgs obj)
{
if (_solutions == null
|| _selectedSolution == null
|| !_solutions.TryGetValue(_selectedSolution, out var uid)
|| !_entityManager.TryGetNetEntity(uid, out var netEntity))
return;
_consoleHost.ExecuteCommand($"vv {netEntity}");
}
/// <summary>
/// Open the corresponding Solution instance in a ViewVariables window.
/// </summary>
private void OpenSolutionWindow(BaseButton.ButtonEventArgs obj)
{
if (_solutions == null
|| _selectedSolution == null
|| !_solutions.TryGetValue(_selectedSolution, out var uid)
|| !_entityManager.TryGetNetEntity(uid, out var netEntity))
return;
_consoleHost.ExecuteCommand($"vv /entity/{netEntity}/Solution/Solution");
}
/// <summary>
/// When a new solution is selected, set _selectedSolution and update the reagent list.
/// </summary>

View File

@@ -62,11 +62,11 @@ public sealed class AnomalySystem : SharedAnomalySystem
{
base.Update(frameTime);
var query = EntityQueryEnumerator<AnomalySupercriticalComponent, SpriteComponent>();
var query = EntityQueryEnumerator<AnomalyComponent, AnomalySupercriticalComponent, SpriteComponent>();
while (query.MoveNext(out var uid, out var super, out var sprite))
while (query.MoveNext(out var uid, out var anomaly, out var super, out var sprite))
{
var completion = 1f - (float)((super.EndTime - _timing.CurTime) / super.SupercriticalDuration);
var completion = 1f - (float) ((super.EndTime - _timing.CurTime) / anomaly.SupercriticalDuration);
var scale = completion * (super.MaxScaleAmount - 1f) + 1f;
_sprite.SetScale((uid, sprite), new Vector2(scale, scale));

View File

@@ -0,0 +1,28 @@
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// Used to change the appearance of gas canisters.
/// </summary>
public sealed class GasCanisterAppearanceSystem : VisualizerSystem<GasCanisterComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
protected override void OnAppearanceChange(EntityUid uid, GasCanisterComponent component, ref AppearanceChangeEvent args)
{
if (!AppearanceSystem.TryGetData<string>(uid, PaintableVisuals.Prototype, out var protoName, args.Component) || args.Sprite is not { } old)
return;
if (!_prototypeManager.HasIndex(protoName))
return;
// Create the given prototype and get its first layer.
var tempUid = Spawn(protoName);
SpriteSystem.LayerSetRsiState(uid, 0, SpriteSystem.LayerGetRsiState(tempUid, 0));
QueueDel(tempUid);
}
}

View File

@@ -1,11 +1,46 @@
using Content.Client.Atmos.Components;
using Robust.Client.GameObjects;
using Content.Client.UserInterface.Systems.Storage.Controls;
using Content.Shared.Atmos.Piping;
using Content.Shared.Hands;
using Content.Shared.Atmos.Components;
using Content.Shared.Item;
namespace Content.Client.Atmos.EntitySystems;
public sealed class PipeColorVisualizerSystem : VisualizerSystem<PipeColorVisualsComponent>
{
[Dependency] private readonly SharedItemSystem _itemSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PipeColorVisualsComponent, GetInhandVisualsEvent>(OnGetVisuals);
SubscribeLocalEvent<PipeColorVisualsComponent, BeforeRenderInGridEvent>(OnDrawInGrid);
}
/// <summary>
/// This method is used to display the color changes of the pipe on the screen..
/// </summary>
private void OnGetVisuals(Entity<PipeColorVisualsComponent> item, ref GetInhandVisualsEvent args)
{
foreach (var (_, layerData) in args.Layers)
{
if (TryComp(item.Owner, out AtmosPipeColorComponent? pipeColor))
layerData.Color = pipeColor.Color;
}
}
/// <summary>
/// This method is used to change the pipe's color in a container grid.
/// </summary>
private void OnDrawInGrid(Entity<PipeColorVisualsComponent> item, ref BeforeRenderInGridEvent args)
{
if (TryComp(item.Owner, out AtmosPipeColorComponent? pipeColor))
args.Color = pipeColor.Color;
}
protected override void OnAppearanceChange(EntityUid uid, PipeColorVisualsComponent component, ref AppearanceChangeEvent args)
{
if (TryComp<SpriteComponent>(uid, out var sprite)
@@ -15,6 +50,8 @@ public sealed class PipeColorVisualizerSystem : VisualizerSystem<PipeColorVisual
var layer = sprite[PipeVisualLayers.Pipe];
layer.Color = color.WithAlpha(layer.Color.A);
}
_itemSystem.VisualsChanged(uid);
}
}

View File

@@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2007/xaml"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
MinSize="500 500" Resizable="True" Title="Air Alarm">
MinSize="500 500" Resizable="True" Title="{Loc air-alarm-ui-title}">
<BoxContainer Orientation="Vertical" Margin="5 5 5 5">
<!-- Status (pressure, temperature, alarm state, device total, address, etc) -->
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2">

View File

@@ -19,6 +19,8 @@ namespace Content.Client.Atmos.Overlays
{
public sealed class GasTileOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
private readonly SharedMapSystem _mapSystem;
@@ -54,7 +56,7 @@ namespace Content.Client.Atmos.Overlays
_mapManager = IoCManager.Resolve<IMapManager>();
_mapSystem = entManager.System<SharedMapSystem>();
_xformSys = xformSys;
_shader = protoMan.Index<ShaderPrototype>("unshaded").Instance();
_shader = protoMan.Index(UnshadedShader).Instance();
ZIndex = GasOverlayZIndex;
_gasCount = system.VisibleGasId.Length;

View File

@@ -1,21 +0,0 @@
using Content.Shared.Bed;
using Robust.Client.GameObjects;
namespace Content.Client.Bed;
public sealed class StasisBedSystem : VisualizerSystem<StasisBedVisualsComponent>
{
protected override void OnAppearanceChange(EntityUid uid, StasisBedVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite != null
&& AppearanceSystem.TryGetData<bool>(uid, StasisBedVisuals.IsOn, out var isOn, args.Component))
{
SpriteSystem.LayerSetVisible((uid, args.Sprite), StasisBedVisualLayers.IsOn, isOn);
}
}
}
public enum StasisBedVisualLayers : byte
{
IsOn,
}

View File

@@ -0,0 +1,6 @@
using Content.Shared.Body.Systems;
namespace Content.Client.Body.Systems;
/// <inheritdoc/>
public sealed class MetabolizerSystem : SharedMetabolizerSystem;

View File

@@ -18,14 +18,10 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
{
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[ValidatePrototypeId<MixingCategoryPrototype>]
private const string DefaultMixingCategory = "DummyMix";
[ValidatePrototypeId<MixingCategoryPrototype>]
private const string DefaultGrindCategory = "DummyGrind";
[ValidatePrototypeId<MixingCategoryPrototype>]
private const string DefaultJuiceCategory = "DummyJuice";
[ValidatePrototypeId<MixingCategoryPrototype>]
private const string DefaultCondenseCategory = "DummyCondense";
private static readonly ProtoId<MixingCategoryPrototype> DefaultMixingCategory = "DummyMix";
private static readonly ProtoId<MixingCategoryPrototype> DefaultGrindCategory = "DummyGrind";
private static readonly ProtoId<MixingCategoryPrototype> DefaultJuiceCategory = "DummyJuice";
private static readonly ProtoId<MixingCategoryPrototype> DefaultCondenseCategory = "DummyCondense";
private readonly Dictionary<string, List<ReagentSourceData>> _reagentSources = new();

View File

@@ -8,6 +8,8 @@ namespace Content.Client.CombatMode
{
public sealed class ColoredScreenBorderOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> Shader = "ColoredScreenBorder";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
@@ -16,7 +18,7 @@ namespace Content.Client.CombatMode
public ColoredScreenBorderOverlay()
{
IoCManager.InjectDependencies(this);
_shader = _prototypeManager.Index<ShaderPrototype>("ColoredScreenBorder").Instance();
_shader = _prototypeManager.Index(Shader).Instance();
}
protected override void Draw(in OverlayDrawArgs args)

View File

@@ -27,8 +27,7 @@ public sealed partial class FlatpackCreatorMenu : FancyWindow
private EntityUid _owner;
[ValidatePrototypeId<EntityPrototype>]
public const string NoBoardEffectId = "FlatpackerNoBoardEffect";
public static readonly EntProtoId NoBoardEffectId = "FlatpackerNoBoardEffect";
private EntityUid? _currentBoard = EntityUid.Invalid;

View File

@@ -7,6 +7,8 @@ namespace Content.Client.Cooldown
{
public sealed class CooldownGraphic : Control
{
private static readonly ProtoId<ShaderPrototype> Shader = "CooldownAnimation";
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
@@ -15,7 +17,7 @@ namespace Content.Client.Cooldown
public CooldownGraphic()
{
IoCManager.InjectDependencies(this);
_shader = _protoMan.Index<ShaderPrototype>("CooldownAnimation").InstanceUnique();
_shader = _protoMan.Index(Shader).InstanceUnique();
}
/// <summary>

View File

@@ -33,8 +33,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
public readonly EntityUid Console;
[ValidatePrototypeId<LocalizedDatasetPrototype>]
private const string ReasonPlaceholders = "CriminalRecordsWantedReasonPlaceholders";
private static readonly ProtoId<LocalizedDatasetPrototype> ReasonPlaceholders = "CriminalRecordsWantedReasonPlaceholders";
public Action<uint?>? OnKeySelected;
public Action<StationRecordFilterType, string>? OnFiltersChanged;
@@ -296,7 +295,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
var field = "reason";
var title = Loc.GetString("criminal-records-status-" + status.ToString().ToLower());
var placeholders = _proto.Index<LocalizedDatasetPrototype>(ReasonPlaceholders);
var placeholders = _proto.Index(ReasonPlaceholders);
var placeholder = Loc.GetString("criminal-records-console-reason-placeholder", ("placeholder", _random.Pick(placeholders))); // just funny it doesn't actually get used
var prompt = Loc.GetString("criminal-records-console-reason");
var entry = new QuickDialogEntry(field, QuickDialogEntryType.LongText, prompt, placeholder);

View File

@@ -1,7 +1,126 @@
using Content.Shared.Damage.Systems;
using Content.Client.Stunnable;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Damage.Systems;
public sealed partial class StaminaSystem : SharedStaminaSystem
{
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly StunSystem _stun = default!; // Clientside Stun System
private const string StaminaAnimationKey = "stamina";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StaminaComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<ActiveStaminaComponent, ComponentShutdown>(OnActiveStaminaShutdown);
SubscribeLocalEvent<StaminaComponent, MobStateChangedEvent>(OnMobStateChanged);
}
protected override void OnStamHandleState(Entity<StaminaComponent> entity, ref AfterAutoHandleStateEvent args)
{
base.OnStamHandleState(entity, ref args);
TryStartAnimation(entity);
}
private void OnActiveStaminaShutdown(Entity<ActiveStaminaComponent> entity, ref ComponentShutdown args)
{
// If we don't have active stamina, we shouldn't have stamina damage. If the update loop can trust it we can trust it.
if (!TryComp<StaminaComponent>(entity, out var stamina))
return;
StopAnimation((entity, stamina));
}
protected override void OnShutdown(Entity<StaminaComponent> entity, ref ComponentShutdown args)
{
base.OnShutdown(entity, ref args);
StopAnimation(entity);
}
private void OnMobStateChanged(Entity<StaminaComponent> ent, ref MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Dead)
StopAnimation(ent);
}
private void TryStartAnimation(Entity<StaminaComponent> entity)
{
if (!TryComp<SpriteComponent>(entity, out var sprite))
return;
// If the animation is running, the system should update it accordingly
// If we're below the threshold to animate, don't try to animate
// If we're in stamcrit don't override it
if (entity.Comp.AnimationThreshold > entity.Comp.StaminaDamage || _animation.HasRunningAnimation(entity, StaminaAnimationKey))
return;
// Don't animate if we're dead
if (_mobState.IsDead(entity))
return;
entity.Comp.StartOffset = sprite.Offset;
PlayAnimation((entity, entity.Comp, sprite));
}
private void StopAnimation(Entity<StaminaComponent, SpriteComponent?> entity)
{
if(!Resolve(entity, ref entity.Comp2))
return;
_animation.Stop(entity.Owner, StaminaAnimationKey);
entity.Comp1.StartOffset = entity.Comp2.Offset;
}
private void OnAnimationCompleted(Entity<StaminaComponent> entity, ref AnimationCompletedEvent args)
{
if (args.Key != StaminaAnimationKey || !args.Finished || !TryComp<SpriteComponent>(entity, out var sprite))
return;
// stop looping if we're below the threshold
if (entity.Comp.AnimationThreshold > entity.Comp.StaminaDamage)
{
_animation.Stop(entity.Owner, StaminaAnimationKey);
_sprite.SetOffset((entity, sprite), entity.Comp.StartOffset);
return;
}
if (!HasComp<AnimationPlayerComponent>(entity))
return;
PlayAnimation((entity, entity.Comp, sprite));
}
private void PlayAnimation(Entity<StaminaComponent, SpriteComponent> entity)
{
var step = Math.Clamp((entity.Comp1.StaminaDamage - entity.Comp1.AnimationThreshold) /
(entity.Comp1.CritThreshold - entity.Comp1.AnimationThreshold),
0f,
1f); // The things I do for project 0 warnings
var frequency = entity.Comp1.FrequencyMin + step * entity.Comp1.FrequencyMod;
var jitter = entity.Comp1.JitterAmplitudeMin + step * entity.Comp1.JitterAmplitudeMod;
var breathing = entity.Comp1.BreathingAmplitudeMin + step * entity.Comp1.BreathingAmplitudeMod;
_animation.Play(entity.Owner,
_stun.GetFatigueAnimation(entity.Comp2,
frequency,
entity.Comp1.Jitters,
jitter * entity.Comp1.JitterMin,
jitter * entity.Comp1.JitterMax,
breathing,
entity.Comp1.StartOffset,
ref entity.Comp1.LastJitter),
StaminaAnimationKey);
}
}

View File

@@ -24,7 +24,7 @@ public sealed class DeliveryVisualizerSystem : VisualizerSystem<DeliveryComponen
if (!_prototype.TryIndex<JobIconPrototype>(job, out var icon))
{
SpriteSystem.LayerSetTexture((uid, args.Sprite), DeliveryVisualLayers.JobStamp, SpriteSystem.Frame0(_prototype.Index("JobIconUnknown")));
SpriteSystem.LayerSetTexture((uid, args.Sprite), DeliveryVisualLayers.JobStamp, SpriteSystem.Frame0(_prototype.Index(UnknownIcon).Icon));
return;
}

View File

@@ -1,6 +0,0 @@
using Content.Shared.Devour;
namespace Content.Client.Devour;
public sealed class DevourSystem : SharedDevourSystem
{
}

View File

@@ -14,6 +14,8 @@ namespace Content.Client.DoAfter;
public sealed class DoAfterOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private readonly IEntityManager _entManager;
private readonly IGameTiming _timing;
private readonly IPlayerManager _player;
@@ -50,7 +52,7 @@ public sealed class DoAfterOverlay : Overlay
var sprite = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/progress_bar.rsi"), "icon");
_barTexture = _entManager.EntitySysManager.GetEntitySystem<SpriteSystem>().Frame0(sprite);
_unshadedShader = protoManager.Index<ShaderPrototype>("unshaded").Instance();
_unshadedShader = protoManager.Index(UnshadedShader).Instance();
}
protected override void Draw(in OverlayDrawArgs args)

View File

@@ -1,16 +1,17 @@
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Prototypes;
namespace Content.Client.Doors;
public sealed class DoorSystem : SharedDoorSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
@@ -85,8 +86,8 @@ public sealed class DoorSystem : SharedDoorSystem
if (!AppearanceSystem.TryGetData<DoorState>(entity, DoorVisuals.State, out var state, args.Component))
state = DoorState.Closed;
if (AppearanceSystem.TryGetData<string>(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
UpdateSpriteLayers((entity.Owner, args.Sprite), baseRsi);
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);
@@ -139,14 +140,14 @@ public sealed class DoorSystem : SharedDoorSystem
}
}
private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string baseRsi)
private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string targetProto)
{
if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
{
Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
if (!_prototypeManager.TryIndex(targetProto, out var target))
return;
}
_sprite.SetBaseRsi(sprite.AsNullable(), res.RSI);
if (!target.TryGetComponent(out SpriteComponent? targetSprite, _componentFactory))
return;
_sprite.SetBaseRsi(sprite.AsNullable(), targetSprite.BaseRSI);
}
}

View File

@@ -10,6 +10,8 @@ namespace Content.Client.Drowsiness;
public sealed class DrowsinessOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> Shader = "Drowsiness";
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -33,7 +35,7 @@ public sealed class DrowsinessOverlay : Overlay
_statusEffects = _sysMan.GetEntitySystem<StatusEffectsSystem>();
_drowsinessShader = _prototypeManager.Index<ShaderPrototype>("Drowsiness").InstanceUnique();
_drowsinessShader = _prototypeManager.Index(Shader).InstanceUnique();
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -12,6 +12,8 @@ namespace Content.Client.Drugs;
public sealed class RainbowOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> Shader = "Rainbow";
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -41,7 +43,7 @@ public sealed class RainbowOverlay : Overlay
_statusEffects = _sysMan.GetEntitySystem<StatusEffectsSystem>();
_rainbowShader = _prototypeManager.Index<ShaderPrototype>("Rainbow").InstanceUnique();
_rainbowShader = _prototypeManager.Index(Shader).InstanceUnique();
_config.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true);
}

View File

@@ -10,6 +10,8 @@ namespace Content.Client.Drunk;
public sealed class DrunkOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> Shader = "Drunk";
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -30,7 +32,7 @@ public sealed class DrunkOverlay : Overlay
public DrunkOverlay()
{
IoCManager.InjectDependencies(this);
_drunkShader = _prototypeManager.Index<ShaderPrototype>("Drunk").InstanceUnique();
_drunkShader = _prototypeManager.Index(Shader).InstanceUnique();
}
protected override void FrameUpdate(FrameEventArgs args)

View File

@@ -13,6 +13,8 @@ namespace Content.Client.Explosion;
[UsedImplicitly]
public sealed class ExplosionOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
@@ -26,7 +28,7 @@ public sealed class ExplosionOverlay : Overlay
public ExplosionOverlay(SharedAppearanceSystem appearanceSystem)
{
IoCManager.InjectDependencies(this);
_shader = _proto.Index<ShaderPrototype>("unshaded").Instance();
_shader = _proto.Index(UnshadedShader).Instance();
_transformSystem = _entMan.System<SharedTransformSystem>();
_appearance = appearanceSystem;
}

View File

@@ -12,6 +12,9 @@ namespace Content.Client.Eye.Blinding
{
public sealed class BlindOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> GreyscaleShader = "GreyscaleFullscreen";
private static readonly ProtoId<ShaderPrototype> CircleShader = "CircleMask";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
@@ -27,8 +30,8 @@ namespace Content.Client.Eye.Blinding
public BlindOverlay()
{
IoCManager.InjectDependencies(this);
_greyscaleShader = _prototypeManager.Index<ShaderPrototype>("GreyscaleFullscreen").InstanceUnique();
_circleMaskShader = _prototypeManager.Index<ShaderPrototype>("CircleMask").InstanceUnique();
_greyscaleShader = _prototypeManager.Index(GreyscaleShader).InstanceUnique();
_circleMaskShader = _prototypeManager.Index(CircleShader).InstanceUnique();
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{

View File

@@ -10,6 +10,9 @@ namespace Content.Client.Eye.Blinding
{
public sealed class BlurryVisionOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> CataractsShader = "Cataracts";
private static readonly ProtoId<ShaderPrototype> CircleShader = "CircleMask";
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -33,8 +36,8 @@ namespace Content.Client.Eye.Blinding
public BlurryVisionOverlay()
{
IoCManager.InjectDependencies(this);
_cataractsShader = _prototypeManager.Index<ShaderPrototype>("Cataracts").InstanceUnique();
_circleMaskShader = _prototypeManager.Index<ShaderPrototype>("CircleMask").InstanceUnique();
_cataractsShader = _prototypeManager.Index(CataractsShader).InstanceUnique();
_circleMaskShader = _prototypeManager.Index(CircleShader).InstanceUnique();
_circleMaskShader.SetParameter("CircleMinDist", 0.0f);
_circleMaskShader.SetParameter("CirclePow", NoMotion_Pow);

View File

@@ -1,8 +1,10 @@
using Content.Shared.CCVar;
using Content.Shared.Flash;
using Content.Shared.Flash.Components;
using Content.Shared.StatusEffect;
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,25 +13,31 @@ namespace Content.Client.Flash
{
public sealed class FlashOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> FlashedEffectShader = "FlashedEffect";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IConfigurationManager _configManager = default!;
private readonly SharedFlashSystem _flash;
private readonly StatusEffectsSystem _statusSys;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _shader;
public float PercentComplete = 0.0f;
private bool _reducedMotion;
public float PercentComplete;
public Texture? ScreenshotTexture;
public FlashOverlay()
{
IoCManager.InjectDependencies(this);
_shader = _prototypeManager.Index<ShaderPrototype>("FlashedEffect").InstanceUnique();
_shader = _prototypeManager.Index(FlashedEffectShader).InstanceUnique();
_flash = _entityManager.System<SharedFlashSystem>();
_statusSys = _entityManager.System<StatusEffectsSystem>();
_configManager.OnValueChanged(CCVars.ReducedMotion, (b) => { _reducedMotion = b; }, invokeImmediately: true);
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -47,8 +55,8 @@ namespace Content.Client.Flash
return;
var curTime = _timing.CurTime;
var lastsFor = (float) (time.Value.Item2 - time.Value.Item1).TotalSeconds;
var timeDone = (float) (curTime - time.Value.Item1).TotalSeconds;
var lastsFor = (float)(time.Value.Item2 - time.Value.Item1).TotalSeconds;
var timeDone = (float)(curTime - time.Value.Item1).TotalSeconds;
PercentComplete = timeDone / lastsFor;
}
@@ -74,10 +82,22 @@ namespace Content.Client.Flash
return;
var worldHandle = args.WorldHandle;
_shader.SetParameter("percentComplete", PercentComplete);
worldHandle.UseShader(_shader);
worldHandle.DrawTextureRectRegion(ScreenshotTexture, args.WorldBounds);
worldHandle.UseShader(null);
if (_reducedMotion)
{
// TODO: This is a very simple placeholder.
// Replace it with a proper shader once we come up with something good.
// Turns out making an effect that is supposed to be a bright, sudden, and disorienting flash
// not do any of that while also being equivalent in terms of game balance is hard.
var alpha = 1 - MathF.Pow(PercentComplete, 8f); // similar falloff curve to the flash shader
worldHandle.DrawRect(args.WorldBounds, new Color(0f, 0f, 0f, alpha));
}
else
{
_shader.SetParameter("percentComplete", PercentComplete);
worldHandle.UseShader(_shader);
worldHandle.DrawTextureRectRegion(ScreenshotTexture, args.WorldBounds);
worldHandle.UseShader(null);
}
}
protected override void DisposeBehavior()

View File

@@ -21,8 +21,7 @@ namespace Content.Client.Guidebook.Controls;
[UsedImplicitly, GenerateTypedNameReferences]
public sealed partial class GuideReagentReaction : BoxContainer, ISearchableControl
{
[ValidatePrototypeId<MixingCategoryPrototype>]
private const string DefaultMixingCategory = "DummyMix";
private static readonly ProtoId<MixingCategoryPrototype> DefaultMixingCategory = "DummyMix";
private readonly IPrototypeManager _protoMan;
@@ -55,7 +54,7 @@ public sealed partial class GuideReagentReaction : BoxContainer, ISearchableCont
}
else
{
mixingCategories.Add(protoMan.Index<MixingCategoryPrototype>(DefaultMixingCategory));
mixingCategories.Add(protoMan.Index(DefaultMixingCategory));
}
SetMixingCategory(mixingCategories, prototype, sysMan);
}

View File

@@ -55,6 +55,7 @@ namespace Content.Client.Input
human.AddFunction(EngineKeyFunctions.MoveLeft);
human.AddFunction(EngineKeyFunctions.MoveRight);
human.AddFunction(EngineKeyFunctions.Walk);
human.AddFunction(ContentKeyFunctions.ToggleKnockdown);
human.AddFunction(ContentKeyFunctions.SwapHands);
human.AddFunction(ContentKeyFunctions.SwapHandsReverse);
human.AddFunction(ContentKeyFunctions.Drop);

View File

@@ -7,17 +7,14 @@ namespace Content.Client.Interactable.Components
[RegisterComponent]
public sealed partial class InteractionOutlineComponent : Component
{
private static readonly ProtoId<ShaderPrototype> ShaderInRange = "SelectionOutlineInrange";
private static readonly ProtoId<ShaderPrototype> ShaderOutOfRange = "SelectionOutline";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
private const float DefaultWidth = 1;
[ValidatePrototypeId<ShaderPrototype>]
private const string ShaderInRange = "SelectionOutlineInrange";
[ValidatePrototypeId<ShaderPrototype>]
private const string ShaderOutOfRange = "SelectionOutline";
private bool _inRange;
private ShaderInstance? _shader;
private int _lastRenderScale;
@@ -65,7 +62,7 @@ namespace Content.Client.Interactable.Components
{
var shaderName = inRange ? ShaderInRange : ShaderOutOfRange;
var instance = _prototypeManager.Index<ShaderPrototype>(shaderName).InstanceUnique();
var instance = _prototypeManager.Index(shaderName).InstanceUnique();
instance.SetParameter("outline_width", DefaultWidth * renderScale);
return instance;
}

View File

@@ -29,6 +29,10 @@ namespace Content.Client.Interaction;
/// </summary>
public sealed class DragDropSystem : SharedDragDropSystem
{
private static readonly ProtoId<ShaderPrototype> ShaderDropTargetInRange = "SelectionOutlineInrange";
private static readonly ProtoId<ShaderPrototype> ShaderDropTargetOutOfRange = "SelectionOutline";
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
@@ -54,12 +58,6 @@ public sealed class DragDropSystem : SharedDragDropSystem
// mousedown event so it can be treated like a regular click
private const float MaxMouseDownTimeForReplayingClick = 0.85f;
[ValidatePrototypeId<ShaderPrototype>]
private const string ShaderDropTargetInRange = "SelectionOutlineInrange";
[ValidatePrototypeId<ShaderPrototype>]
private const string ShaderDropTargetOutOfRange = "SelectionOutline";
/// <summary>
/// Current entity being dragged around.
/// </summary>
@@ -113,8 +111,8 @@ public sealed class DragDropSystem : SharedDragDropSystem
Subs.CVar(_cfgMan, CCVars.DragDropDeadZone, SetDeadZone, true);
_dropTargetInRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetInRange).Instance();
_dropTargetOutOfRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetOutOfRange).Instance();
_dropTargetInRangeShader = _prototypeManager.Index(ShaderDropTargetInRange).Instance();
_dropTargetOutOfRangeShader = _prototypeManager.Index(ShaderDropTargetOutOfRange).Instance();
// needs to fire on mouseup and mousedown so we can detect a drag / drop
CommandBinds.Builder
.BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(OnUse, false, true), new[] { typeof(SharedInteractionSystem) })

View File

@@ -15,6 +15,10 @@ namespace Content.Client.Light;
/// </summary>
public sealed class AmbientOcclusionOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private static readonly ProtoId<ShaderPrototype> StencilMaskShader = "StencilMask";
private static readonly ProtoId<ShaderPrototype> StencilEqualDrawShader = "StencilEqualDraw";
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
@@ -87,7 +91,7 @@ public sealed class AmbientOcclusionOverlay : Overlay
args.WorldHandle.RenderInRenderTarget(_aoTarget,
() =>
{
worldHandle.UseShader(_proto.Index<ShaderPrototype>("unshaded").Instance());
worldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
var invMatrix = _aoTarget.GetWorldToLocalMatrix(viewport.Eye!, scale);
foreach (var entry in query.QueryAabb(mapId, worldBounds))
@@ -110,7 +114,7 @@ public sealed class AmbientOcclusionOverlay : Overlay
() =>
{
// Don't want lighting affecting it.
worldHandle.UseShader(_proto.Index<ShaderPrototype>("unshaded").Instance());
worldHandle.UseShader(_proto.Index(UnshadedShader).Instance());
foreach (var grid in _mapManager.FindGridsIntersecting(mapId, worldBounds))
{
@@ -131,11 +135,11 @@ public sealed class AmbientOcclusionOverlay : Overlay
}, Color.Transparent);
// Draw the stencil texture to depth buffer.
worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilMask").Instance());
worldHandle.UseShader(_proto.Index(StencilMaskShader).Instance());
worldHandle.DrawTextureRect(_aoStencilTarget!.Texture, worldBounds);
// Draw the Blurred AO texture finally.
worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilEqualDraw").Instance());
worldHandle.UseShader(_proto.Index(StencilEqualDrawShader).Instance());
worldHandle.DrawTextureRect(_aoTarget!.Texture, worldBounds, color);
args.WorldHandle.SetTransform(Matrix3x2.Identity);

View File

@@ -238,6 +238,9 @@ namespace Content.Client.Light.Components
public override void OnInitialize()
{
// This is very janky. This could easily result in no visible animation at all if the random values happen
// to all be close to each other.
// TODO ANIMATIONS
_randomValue1 = (float)InterpolateLinear(StartValue, EndValue, (float)_random.NextDouble());
_randomValue2 = (float)InterpolateLinear(StartValue, EndValue, (float)_random.NextDouble());
_randomValue3 = (float)InterpolateLinear(StartValue, EndValue, (float)_random.NextDouble());

View File

@@ -11,6 +11,8 @@ namespace Content.Client.Light;
public sealed class SunShadowOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> MixShader = "Mix";
public override OverlaySpace Space => OverlaySpace.BeforeLighting;
[Dependency] private readonly IClyde _clyde = default!;
@@ -150,7 +152,7 @@ public sealed class SunShadowOverlay : Overlay
viewport.LightRenderTarget.GetWorldToLocalMatrix(eye, scale);
worldHandle.SetTransform(invMatrix);
var maskShader = _protoManager.Index<ShaderPrototype>("Mix").Instance();
var maskShader = _protoManager.Index(MixShader).Instance();
worldHandle.UseShader(maskShader);
worldHandle.DrawTextureRect(_target.Texture, worldBounds, Color.Black.WithAlpha(alpha));

View File

@@ -104,8 +104,7 @@ namespace Content.Client.Lobby.UI
private bool _isDirty;
[ValidatePrototypeId<GuideEntryPrototype>]
private const string DefaultSpeciesGuidebook = "Species";
private static readonly ProtoId<GuideEntryPrototype> DefaultSpeciesGuidebook = "Species";
public event Action<List<ProtoId<GuideEntryPrototype>>>? OnOpenGuidebook;
@@ -823,9 +822,9 @@ namespace Content.Client.Lobby.UI
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var page = DefaultSpeciesGuidebook;
if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
page = species;
page = new ProtoId<GuideEntryPrototype>(species.Id); // Gross. See above todo comment.
if (_prototypeManager.TryIndex<GuideEntryPrototype>(DefaultSpeciesGuidebook, out var guideRoot))
if (_prototypeManager.TryIndex(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry>();
dict.Add(DefaultSpeciesGuidebook, guideRoot);
@@ -1457,17 +1456,13 @@ namespace Content.Client.Lobby.UI
{
return;
}
var hairMarking = Profile.Appearance.HairStyleId switch
{
HairStyles.DefaultHairStyle => new List<Marking>(),
_ => new() { new(Profile.Appearance.HairStyleId, new List<Color>() { Profile.Appearance.HairColor }) },
};
var hairMarking = Profile.Appearance.HairStyleId == HairStyles.DefaultHairStyle
? new List<Marking>()
: new() { new(Profile.Appearance.HairStyleId, new List<Color>() { Profile.Appearance.HairColor }) };
var facialHairMarking = Profile.Appearance.FacialHairStyleId switch
{
HairStyles.DefaultFacialHairStyle => new List<Marking>(),
_ => new() { new(Profile.Appearance.FacialHairStyleId, new List<Color>() { Profile.Appearance.FacialHairColor }) },
};
var facialHairMarking = Profile.Appearance.FacialHairStyleId == HairStyles.DefaultFacialHairStyle
? new List<Marking>()
: new() { new(Profile.Appearance.FacialHairStyleId, new List<Color>() { Profile.Appearance.FacialHairColor }) };
HairStylePicker.UpdateData(
hairMarking,

View File

@@ -11,6 +11,8 @@ namespace Content.Client.Mapping;
public sealed class MappingOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
@@ -35,7 +37,7 @@ public sealed class MappingOverlay : Overlay
_sprite = _entities.System<SpriteSystem>();
_state = state;
_shader = _prototypes.Index<ShaderPrototype>("unshaded").Instance();
_shader = _prototypes.Index(UnshadedShader).Instance();
}
protected override void Draw(in OverlayDrawArgs args)

View File

@@ -57,7 +57,8 @@ public sealed class MouseRotatorSystem : SharedMouseRotatorSystem
rotation += 2 * Math.PI;
RaisePredictiveEvent(new RequestMouseRotatorRotationEvent
{
Rotation = rotation
Rotation = rotation,
User = GetNetEntity(player)
});
return;
@@ -77,7 +78,8 @@ public sealed class MouseRotatorSystem : SharedMouseRotatorSystem
RaisePredictiveEvent(new RequestMouseRotatorRotationEvent
{
Rotation = angle
Rotation = angle,
User = GetNetEntity(player)
});
}
}

View File

@@ -8,6 +8,8 @@ namespace Content.Client.Movement.Systems;
public sealed class FloorOcclusionSystem : SharedFloorOcclusionSystem
{
private static readonly ProtoId<ShaderPrototype> HorizontalCut = "HorizontalCut";
[Dependency] private readonly IPrototypeManager _proto = default!;
private EntityQuery<SpriteComponent> _spriteQuery;
@@ -48,7 +50,7 @@ public sealed class FloorOcclusionSystem : SharedFloorOcclusionSystem
if (!_spriteQuery.Resolve(sprite.Owner, ref sprite.Comp, false))
return;
var shader = _proto.Index<ShaderPrototype>("HorizontalCut").Instance();
var shader = _proto.Index(HorizontalCut).Instance();
if (sprite.Comp.PostShader is not null && sprite.Comp.PostShader != shader)
return;

View File

@@ -23,8 +23,7 @@ public sealed class NetworkConfiguratorSystem : SharedNetworkConfiguratorSystem
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string Action = "ActionClearNetworkLinkOverlays";
private static readonly EntProtoId Action = "ActionClearNetworkLinkOverlays";
public override void Initialize()
{

View File

@@ -6,13 +6,16 @@
Resizable="False">
<BoxContainer Orientation="Vertical" SeparationOverride="4" MinWidth="150">
<changelog:ChangelogButton Access="Public" Name="ChangelogButton"/>
<ui:VoteCallMenuButton />
<Button Access="Public" Name="OptionsButton" Text="{Loc 'ui-escape-options'}" />
<PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
<Button Access="Public" Name="RulesButton" Text="{Loc 'ui-escape-rules'}" />
<Button Access="Public" Name="GuidebookButton" Text="{Loc 'ui-escape-guidebook'}" />
<Button Access="Public" Name="WikiButton" Text="{Loc 'ui-escape-wiki'}" />
<changelog:ChangelogButton Access="Public" Name="ChangelogButton" />
<PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
<Button Access="Public" Name="OptionsButton" Text="{Loc 'ui-escape-options'}" />
<PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
<Button Access="Public" Name="DisconnectButton" Text="{Loc 'ui-escape-disconnect'}" />
<Button Access="Public" Name="QuitButton" Text="{Loc 'ui-escape-quit'}" />
<Button Access="Public" Name="QuitButton" Text="{Loc 'ui-escape-quit'}" StyleClasses="ButtonColorRed" />
</BoxContainer>
</ui1:EscapeMenu>

View File

@@ -162,6 +162,7 @@ namespace Content.Client.Options.UI.Tabs
AddButton(EngineKeyFunctions.Walk);
AddCheckBox("ui-options-hotkey-toggle-walk", _cfg.GetCVar(CCVars.ToggleWalk), HandleToggleWalk);
InitToggleWalk();
AddButton(ContentKeyFunctions.ToggleKnockdown);
AddHeader("ui-options-header-camera");
AddButton(EngineKeyFunctions.CameraRotateLeft);

View File

@@ -15,6 +15,9 @@ namespace Content.Client.Outline;
/// </summary>
public sealed class TargetOutlineSystem : EntitySystem
{
private static readonly ProtoId<ShaderPrototype> ShaderTargetValid = "SelectionOutlineInrange";
private static readonly ProtoId<ShaderPrototype> ShaderTargetInvalid = "SelectionOutline";
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
@@ -70,12 +73,6 @@ public sealed class TargetOutlineSystem : EntitySystem
private Vector2 LookupVector => new(LookupSize, LookupSize);
[ValidatePrototypeId<ShaderPrototype>]
private const string ShaderTargetValid = "SelectionOutlineInrange";
[ValidatePrototypeId<ShaderPrototype>]
private const string ShaderTargetInvalid = "SelectionOutline";
private ShaderInstance? _shaderTargetValid;
private ShaderInstance? _shaderTargetInvalid;
@@ -85,8 +82,8 @@ public sealed class TargetOutlineSystem : EntitySystem
{
base.Initialize();
_shaderTargetValid = _prototypeManager.Index<ShaderPrototype>(ShaderTargetValid).InstanceUnique();
_shaderTargetInvalid = _prototypeManager.Index<ShaderPrototype>(ShaderTargetInvalid).InstanceUnique();
_shaderTargetValid = _prototypeManager.Index(ShaderTargetValid).InstanceUnique();
_shaderTargetInvalid = _prototypeManager.Index(ShaderTargetInvalid).InstanceUnique();
}
public void Disable()

View File

@@ -6,6 +6,8 @@ namespace Content.Client.Overlays;
public sealed partial class BlackAndWhiteOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> Shader = "GreyscaleFullscreen";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
@@ -15,7 +17,7 @@ public sealed partial class BlackAndWhiteOverlay : Overlay
public BlackAndWhiteOverlay()
{
IoCManager.InjectDependencies(this);
_greyscaleShader = _prototypeManager.Index<ShaderPrototype>("GreyscaleFullscreen").InstanceUnique();
_greyscaleShader = _prototypeManager.Index(Shader).InstanceUnique();
ZIndex = 10; // draw this over the DamageOverlay, RainbowOverlay etc.
}

View File

@@ -6,6 +6,8 @@ namespace Content.Client.Overlays;
public sealed partial class NoirOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> Shader = "Noir";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
@@ -15,7 +17,7 @@ public sealed partial class NoirOverlay : Overlay
public NoirOverlay()
{
IoCManager.InjectDependencies(this);
_noirShader = _prototypeManager.Index<ShaderPrototype>("Noir").InstanceUnique();
_noirShader = _prototypeManager.Index(Shader).InstanceUnique();
ZIndex = 9; // draw this over the DamageOverlay, RainbowOverlay etc, but before the black and white shader
}

View File

@@ -13,8 +13,7 @@ public sealed class ShowJobIconsSystem : EquipmentHudSystem<ShowJobIconsComponen
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[ValidatePrototypeId<JobIconPrototype>]
private const string JobIconForNoId = "JobIconNoId";
private static readonly ProtoId<JobIconPrototype> JobIconForNoId = "JobIconNoId";
public override void Initialize()
{
@@ -52,7 +51,7 @@ public sealed class ShowJobIconsSystem : EquipmentHudSystem<ShowJobIconsComponen
}
}
if (_prototype.TryIndex<JobIconPrototype>(iconId, out var iconPrototype))
if (_prototype.TryIndex(iconId, out var iconPrototype))
ev.StatusIcons.Add(iconPrototype);
else
Log.Error($"Invalid job icon prototype: {iconPrototype}");

View File

@@ -45,13 +45,13 @@ public sealed partial class StencilOverlay
}, Color.Transparent);
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilMask").Instance());
worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
var curTime = _timing.RealTime;
var sprite = _sprite.GetFrame(new SpriteSpecifier.Texture(new ResPath("/Textures/Parallaxes/noise.png")), curTime);
// Draw the rain
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilDraw").Instance());
worldHandle.UseShader(_protoManager.Index(StencilDraw).Instance());
_parallax.DrawParallax(worldHandle, worldAABB, sprite, curTime, position, new Vector2(0.5f, 0f));
}
}

View File

@@ -55,13 +55,13 @@ public sealed partial class StencilOverlay
}, Color.Transparent);
worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilMask").Instance());
worldHandle.UseShader(_protoManager.Index(StencilMask).Instance());
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
var curTime = _timing.RealTime;
var sprite = _sprite.GetFrame(weatherProto.Sprite, curTime);
// Draw the rain
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilDraw").Instance());
worldHandle.UseShader(_protoManager.Index(StencilDraw).Instance());
_parallax.DrawParallax(worldHandle, worldAABB, sprite, curTime, position, Vector2.Zero, modulate: (weatherProto.Color ?? Color.White).WithAlpha(alpha));
worldHandle.SetTransform(Matrix3x2.Identity);

View File

@@ -17,6 +17,10 @@ namespace Content.Client.Overlays;
/// </summary>
public sealed partial class StencilOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> CircleShader = "WorldGradientCircle";
private static readonly ProtoId<ShaderPrototype> StencilMask = "StencilMask";
private static readonly ProtoId<ShaderPrototype> StencilDraw = "StencilDraw";
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -43,7 +47,7 @@ public sealed partial class StencilOverlay : Overlay
_sprite = sprite;
_weather = weather;
IoCManager.InjectDependencies(this);
_shader = _protoManager.Index<ShaderPrototype>("WorldGradientCircle").InstanceUnique();
_shader = _protoManager.Index(CircleShader).InstanceUnique();
}
protected override void Draw(in OverlayDrawArgs args)

View File

@@ -10,6 +10,8 @@ namespace Content.Client.Paper.UI;
[GenerateTypedNameReferences]
public sealed partial class StampLabel : Label
{
private static readonly ProtoId<ShaderPrototype> PaperStamp = "PaperStamp";
/// A scale that's applied to the text to ensure it
/// fits in the allowed space.
private Vector2 _textScaling = Vector2.One;
@@ -26,7 +28,7 @@ public sealed partial class StampLabel : Label
RobustXamlLoader.Load(this);
var prototypes = IoCManager.Resolve<IPrototypeManager>();
_stampShader = prototypes.Index<ShaderPrototype>("PaperStamp").InstanceUnique();
_stampShader = prototypes.Index(PaperStamp).InstanceUnique();
}
protected override Vector2 MeasureOverride(Vector2 availableSize)

View File

@@ -12,6 +12,8 @@ namespace Content.Client.Paper.UI;
[GenerateTypedNameReferences]
public sealed partial class StampWidget : PanelContainer
{
private static readonly ProtoId<ShaderPrototype> PaperStamp = "PaperStamp";
private StyleBoxTexture _borderTexture;
private ShaderInstance? _stampShader;
@@ -42,7 +44,7 @@ public sealed partial class StampWidget : PanelContainer
PanelOverride = _borderTexture;
var prototypes = IoCManager.Resolve<IPrototypeManager>();
_stampShader = prototypes.Index<ShaderPrototype>("PaperStamp").InstanceUnique();
_stampShader = prototypes.Index(PaperStamp).InstanceUnique();
}
protected override void Draw(DrawingHandleScreen handle)

View File

@@ -195,7 +195,6 @@ public sealed class GeneratedParallaxCache : IPostInjectInit
public required ResPath ConfigPath;
public required Task<Texture> LoadTask;
public required CancellationTokenSource CancellationSource;
public ValueList<CancellationTokenRegistration> CancelRegistrations;
public int RefCount;
}

View File

@@ -14,8 +14,7 @@ public sealed class ParallaxSystem : SharedParallaxSystem
[Dependency] private readonly IParallaxManager _parallax = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[ValidatePrototypeId<ParallaxPrototype>]
private const string Fallback = "Default";
private static readonly ProtoId<ParallaxPrototype> Fallback = "Default";
public const int ParallaxZIndex = 0;

View File

@@ -262,6 +262,7 @@ public sealed partial class ParticleAcceleratorControlMenu : FancyWindow
public sealed class PASegmentControl : Control
{
private static readonly ProtoId<ShaderPrototype> GreyscaleShaderId = "Greyscale";
private readonly ShaderInstance _greyScaleShader;
private readonly TextureRect _base;
private readonly TextureRect _unlit;
@@ -272,7 +273,7 @@ public sealed class PASegmentControl : Control
public PASegmentControl()
{
_greyScaleShader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>("Greyscale").Instance();
_greyScaleShader = IoCManager.Resolve<IPrototypeManager>().Index(GreyscaleShaderId).Instance();
AddChild(_base = new TextureRect());
AddChild(_unlit = new TextureRect());

View File

@@ -16,6 +16,8 @@ namespace Content.Client.Popups;
/// </summary>
public sealed class PopupOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
private readonly IConfigurationManager _configManager;
private readonly IEntityManager _entManager;
private readonly IPlayerManager _playerMgr;
@@ -48,7 +50,7 @@ public sealed class PopupOverlay : Overlay
_popup = popup;
_controller = controller;
_shader = protoManager.Index<ShaderPrototype>("unshaded").Instance();
_shader = protoManager.Index(UnshadedShader).Instance();
}
protected override void Draw(in OverlayDrawArgs args)

View File

@@ -9,7 +9,7 @@ public static class StaticPowerSystem
public static bool IsPowered(this EntitySystem system, EntityUid uid, IEntityManager entManager, ApcPowerReceiverComponent? receiver = null)
{
if (receiver == null && !entManager.TryGetComponent(uid, out receiver))
return false;
return true;
return receiver.Powered;
}

View File

@@ -15,8 +15,7 @@ namespace Content.Client.Power.Generation.Teg;
/// <seealso cref="TegCirculatorComponent"/>
public sealed class TegSystem : EntitySystem
{
[ValidatePrototypeId<EntityPrototype>]
private const string ArrowPrototype = "TegCirculatorArrow";
private static readonly EntProtoId ArrowPrototype = "TegCirculatorArrow";
public override void Initialize()
{

View File

@@ -48,8 +48,9 @@ public sealed class PowerCellSystem : SharedPowerCellSystem
if (!_sprite.LayerExists((uid, args.Sprite), PowerCellVisualLayers.Unshaded))
return;
// If no appearance data is set, rely on whatever existing sprite state is set being correct.
if (!_appearance.TryGetData<byte>(uid, PowerCellVisuals.ChargeLevel, out var level, args.Component))
level = 0;
return;
var positiveCharge = level > 0;
_sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, positiveCharge);

View File

@@ -13,6 +13,8 @@ namespace Content.Client.Radiation.Overlays
{
public sealed class RadiationPulseOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> RadiationShader = "Radiation";
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
@@ -29,7 +31,7 @@ namespace Content.Client.Radiation.Overlays
public RadiationPulseOverlay()
{
IoCManager.InjectDependencies(this);
_baseShader = _prototypeManager.Index<ShaderPrototype>("Radiation").Instance().Duplicate();
_baseShader = _prototypeManager.Index(RadiationShader).Instance().Duplicate();
}
protected override bool BeforeDraw(in OverlayDrawArgs args)

View File

@@ -14,6 +14,8 @@ namespace Content.Client.Shuttles;
/// </summary>
public sealed class FtlArrivalOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
private EntityLookupSystem _lookups;
@@ -36,7 +38,7 @@ public sealed class FtlArrivalOverlay : Overlay
_maps = _entManager.System<SharedMapSystem>();
_sprites = _entManager.System<SpriteSystem>();
_shader = _protos.Index<ShaderPrototype>("unshaded").Instance();
_shader = _protos.Index(UnshadedShader).Instance();
}
protected override bool BeforeDraw(in OverlayDrawArgs args)

View File

@@ -220,9 +220,9 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
var gridCentre = Vector2.Transform(gridBody.LocalCenter, curGridToView);
var distance = gridCentre.Length();
var gridDistance = (gridBody.LocalCenter - xform.LocalPosition).Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
("distance", $"{distance:0.0}"));
("distance", $"{gridDistance:0.0}"));
var mapCoords = _transform.GetWorldPosition(gUid);
var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})";

View File

@@ -25,7 +25,6 @@ public sealed partial class BorgSelectTypeMenu : FancyWindow
public event Action<ProtoId<BorgTypePrototype>>? ConfirmedBorgType;
[ValidatePrototypeId<GuideEntryPrototype>]
private static readonly List<ProtoId<GuideEntryPrototype>> GuidebookEntries = new() { "Cyborgs", "Robotics" };
public BorgSelectTypeMenu()

View File

@@ -84,12 +84,13 @@ public sealed partial class LawDisplay : Control
radioChannelButton.OnPressed += _ =>
{
switch (radioChannel)
if (radioChannel == SharedChatSystem.CommonChannel)
{
case SharedChatSystem.CommonChannel:
_chatManager.SendMessage($"{SharedChatSystem.RadioCommonPrefix} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Radio); break;
default:
_chatManager.SendMessage($"{SharedChatSystem.RadioChannelPrefix}{radioChannelProto.KeyCode} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Radio); break;
_chatManager.SendMessage($"{SharedChatSystem.RadioCommonPrefix} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Radio);
}
else
{
_chatManager.SendMessage($"{SharedChatSystem.RadioChannelPrefix}{radioChannelProto.KeyCode} {lawIdentifierPlaintext}: {lawDescriptionPlaintext}", ChatSelectChannel.Radio);
}
_nextAllowedPress[radioChannelButton] = _timing.CurTime + PressCooldown;
};

View File

@@ -12,6 +12,10 @@ namespace Content.Client.Silicons.StationAi;
public sealed class StationAiOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> CameraStaticShader = "CameraStatic";
private static readonly ProtoId<ShaderPrototype> StencilMaskShader = "StencilMask";
private static readonly ProtoId<ShaderPrototype> StencilDrawShader = "StencilDraw";
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -91,7 +95,7 @@ public sealed class StationAiOverlay : Overlay
() =>
{
worldHandle.SetTransform(invMatrix);
var shader = _proto.Index<ShaderPrototype>("CameraStatic").Instance();
var shader = _proto.Index(CameraStaticShader).Instance();
worldHandle.UseShader(shader);
worldHandle.DrawRect(worldBounds, Color.White);
},
@@ -114,11 +118,11 @@ public sealed class StationAiOverlay : Overlay
}
// Use the lighting as a mask
worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilMask").Instance());
worldHandle.UseShader(_proto.Index(StencilMaskShader).Instance());
worldHandle.DrawTextureRect(_stencilTexture!.Texture, worldBounds);
// Draw the static
worldHandle.UseShader(_proto.Index<ShaderPrototype>("StencilDraw").Instance());
worldHandle.UseShader(_proto.Index(StencilDrawShader).Instance());
worldHandle.DrawTextureRect(_staticTexture!.Texture, worldBounds);
worldHandle.SetTransform(Matrix3x2.Identity);

View File

@@ -9,6 +9,8 @@ namespace Content.Client.Singularity
{
public sealed class SingularityOverlay : Overlay, IEntityEventSubscriber
{
private static readonly ProtoId<ShaderPrototype> Shader = "Singularity";
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private SharedTransformSystem? _xformSystem = null;
@@ -29,7 +31,7 @@ namespace Content.Client.Singularity
public SingularityOverlay()
{
IoCManager.InjectDependencies(this);
_shader = _prototypeManager.Index<ShaderPrototype>("Singularity").Instance().Duplicate();
_shader = _prototypeManager.Index(Shader).Instance().Duplicate();
_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.

View File

@@ -1,56 +1,129 @@
using Content.Shared.SprayPainter;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Utility;
using System.Linq;
using Robust.Shared.Graphics;
using Content.Client.Items;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Decals;
using Content.Shared.SprayPainter;
using Content.Shared.SprayPainter.Components;
using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.SprayPainter;
/// <summary>
/// Client-side spray painter functions. Caches information for spray painter windows and updates the UI to reflect component state.
/// </summary>
public sealed class SprayPainterSystem : SharedSprayPainterSystem
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
public List<SprayPainterEntry> Entries { get; private set; } = new();
public List<SprayPainterDecalEntry> Decals = [];
public Dictionary<string, List<string>> PaintableGroupsByCategory = new();
public Dictionary<string, Dictionary<string, EntProtoId>> PaintableStylesByGroup = new();
protected override void CacheStyles()
public override void Initialize()
{
base.CacheStyles();
base.Initialize();
Entries.Clear();
foreach (var style in Styles)
Subs.ItemStatus<SprayPainterComponent>(ent => new StatusControl(ent));
SubscribeLocalEvent<SprayPainterComponent, AfterAutoHandleStateEvent>(OnStateUpdate);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
CachePrototypes();
}
private void OnStateUpdate(Entity<SprayPainterComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateUi(ent);
}
protected override void UpdateUi(Entity<SprayPainterComponent> ent)
{
if (_ui.TryGetOpenUi(ent.Owner, SprayPainterUiKey.Key, out var bui))
bui.Update();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.WasModified<PaintableGroupCategoryPrototype>() || !args.WasModified<PaintableGroupPrototype>() || !args.WasModified<DecalPrototype>())
return;
CachePrototypes();
}
private void CachePrototypes()
{
PaintableGroupsByCategory.Clear();
PaintableStylesByGroup.Clear();
foreach (var category in Proto.EnumeratePrototypes<PaintableGroupCategoryPrototype>().OrderBy(x => x.ID))
{
var name = style.Name;
string? iconPath = Groups
.FindAll(x => x.StylePaths.ContainsKey(name))?
.MaxBy(x => x.IconPriority)?.StylePaths[name];
if (iconPath == null)
var groupList = new List<string>();
foreach (var groupId in category.Groups)
{
Entries.Add(new SprayPainterEntry(name, null));
continue;
if (!Proto.TryIndex(groupId, out var group))
continue;
groupList.Add(groupId);
PaintableStylesByGroup[groupId] = group.Styles;
}
RSIResource doorRsi = _resourceCache.GetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / new ResPath(iconPath));
if (!doorRsi.RSI.TryGetState("closed", out var icon))
{
Entries.Add(new SprayPainterEntry(name, null));
continue;
}
if (groupList.Count > 0)
PaintableGroupsByCategory[category.ID] = groupList;
}
Entries.Add(new SprayPainterEntry(name, icon.Frame0));
Decals.Clear();
foreach (var decalPrototype in Proto.EnumeratePrototypes<DecalPrototype>().OrderBy(x => x.ID))
{
if (!decalPrototype.Tags.Contains("station")
&& !decalPrototype.Tags.Contains("markings")
|| decalPrototype.Tags.Contains("dirty"))
continue;
Decals.Add(new SprayPainterDecalEntry(decalPrototype.ID, decalPrototype.Sprite));
}
}
private sealed class StatusControl : Control
{
private readonly RichTextLabel _label;
private readonly Entity<SprayPainterComponent> _entity;
private DecalPaintMode? _lastPaintingDecals = null;
public StatusControl(Entity<SprayPainterComponent> ent)
{
_entity = ent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_entity.Comp.DecalMode == _lastPaintingDecals)
return;
_lastPaintingDecals = _entity.Comp.DecalMode;
string modeLocString = _entity.Comp.DecalMode switch
{
DecalPaintMode.Add => "spray-painter-item-status-add",
DecalPaintMode.Remove => "spray-painter-item-status-remove",
_ => "spray-painter-item-status-off"
};
_label.SetMarkupPermissive(Robust.Shared.Localization.Loc.GetString("spray-painter-item-status-label",
("mode", Robust.Shared.Localization.Loc.GetString(modeLocString))));
}
}
}
public sealed class SprayPainterEntry
{
public string Name;
public Texture? Icon;
public SprayPainterEntry(string name, Texture? icon)
{
Name = name;
Icon = icon;
}
}
/// <summary>
/// A spray paintable decal, mapped by ID.
/// </summary>
public sealed record SprayPainterDecalEntry(string Name, SpriteSpecifier Sprite);

View File

@@ -1,42 +1,96 @@
using Content.Shared.Decals;
using Content.Shared.SprayPainter;
using Content.Shared.SprayPainter.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
namespace Content.Client.SprayPainter.UI;
public sealed class SprayPainterBoundUserInterface : BoundUserInterface
/// <summary>
/// A BUI for a spray painter. Allows selecting pipe colours, decals, and paintable object types sorted by category.
/// </summary>
public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private SprayPainterWindow? _window;
public SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<SprayPainterWindow>();
_window.OnSpritePicked = OnSpritePicked;
_window.OnColorPicked = OnColorPicked;
if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? comp))
if (_window == null)
{
_window.Populate(EntMan.System<SprayPainterSystem>().Entries, comp.Index, comp.PickedColor, comp.ColorPalette);
_window = this.CreateWindow<SprayPainterWindow>();
_window.OnSpritePicked += OnSpritePicked;
_window.OnSetPipeColor += OnSetPipeColor;
_window.OnTabChanged += OnTabChanged;
_window.OnDecalChanged += OnDecalChanged;
_window.OnDecalColorChanged += OnDecalColorChanged;
_window.OnDecalAngleChanged += OnDecalAngleChanged;
_window.OnDecalSnapChanged += OnDecalSnapChanged;
}
var sprayPainter = EntMan.System<SprayPainterSystem>();
_window.PopulateCategories(sprayPainter.PaintableStylesByGroup, sprayPainter.PaintableGroupsByCategory, sprayPainter.Decals);
Update();
if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainterComp))
_window.SetSelectedTab(sprayPainterComp.SelectedTab);
}
private void OnSpritePicked(ItemList.ItemListSelectedEventArgs args)
public override void Update()
{
SendMessage(new SprayPainterSpritePickedMessage(args.ItemIndex));
if (_window == null)
return;
if (!EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainter))
return;
_window.PopulateColors(sprayPainter.ColorPalette);
if (sprayPainter.PickedColor != null)
_window.SelectColor(sprayPainter.PickedColor);
_window.SetSelectedStyles(sprayPainter.StylesByGroup);
_window.SetSelectedDecal(sprayPainter.SelectedDecal);
_window.SetDecalAngle(sprayPainter.SelectedDecalAngle);
_window.SetDecalColor(sprayPainter.SelectedDecalColor);
_window.SetDecalSnap(sprayPainter.SnapDecals);
}
private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
private void OnDecalSnapChanged(bool snap)
{
SendPredictedMessage(new SprayPainterSetDecalSnapMessage(snap));
}
private void OnDecalAngleChanged(int angle)
{
SendPredictedMessage(new SprayPainterSetDecalAngleMessage(angle));
}
private void OnDecalColorChanged(Color? color)
{
SendPredictedMessage(new SprayPainterSetDecalColorMessage(color));
}
private void OnDecalChanged(ProtoId<DecalPrototype> protoId)
{
SendPredictedMessage(new SprayPainterSetDecalMessage(protoId));
}
private void OnTabChanged(int index, bool isSelectedTabWithDecals)
{
SendPredictedMessage(new SprayPainterTabChangedMessage(index, isSelectedTabWithDecals));
}
private void OnSpritePicked(string group, string style)
{
SendPredictedMessage(new SprayPainterSetPaintableStyleMessage(group, style));
}
private void OnSetPipeColor(ItemList.ItemListSelectedEventArgs args)
{
var key = _window?.IndexToColorKey(args.ItemIndex);
SendMessage(new SprayPainterColorPickedMessage(key));
SendPredictedMessage(new SprayPainterSetPipeColorMessage(key));
}
}

View File

@@ -0,0 +1,26 @@
<controls:SprayPainterDecals
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.SprayPainter.UI">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'spray-painter-selected-decals'}" />
<ScrollContainer VerticalExpand="True">
<GridContainer Columns="7" Name="DecalsGrid">
<!-- populated by code -->
</GridContainer>
</ScrollContainer>
<BoxContainer Orientation="Vertical">
<ColorSelectorSliders Name="ColorSelector" IsAlphaVisible="True" />
<CheckBox Name="UseCustomColorCheckBox" Text="{Loc 'spray-painter-use-custom-color'}" />
<CheckBox Name="SnapToTileCheckBox" Text="{Loc 'spray-painter-use-snap-to-tile'}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'spray-painter-angle-rotation'}" />
<SpinBox Name="AngleSpinBox" HorizontalExpand="True" />
<Button Text="{Loc 'spray-painter-angle-rotation-90-sub'}" Name="SubAngleButton" />
<Button Text="{Loc 'spray-painter-angle-rotation-reset'}" Name="SetZeroAngleButton" />
<Button Text="{Loc 'spray-painter-angle-rotation-90-add'}" Name="AddAngleButton" />
</BoxContainer>
</BoxContainer>
</controls:SprayPainterDecals>

View File

@@ -0,0 +1,174 @@
using System.Numerics;
using Content.Client.Stylesheets;
using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.SprayPainter.UI;
/// <summary>
/// Used to control decal painting parameters for the spray painter.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class SprayPainterDecals : Control
{
public Action<ProtoId<DecalPrototype>>? OnDecalSelected;
public Action<Color?>? OnColorChanged;
public Action<int>? OnAngleChanged;
public Action<bool>? OnSnapChanged;
private SpriteSystem? _sprite;
private string _selectedDecal = string.Empty;
private List<SprayPainterDecalEntry> _decals = [];
public SprayPainterDecals()
{
RobustXamlLoader.Load(this);
AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value += 90;
SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value -= 90;
SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0;
AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value);
UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed;
SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed;
ColorSelector.OnColorChanged += OnColorSelected;
}
private void UseCustomColorCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
{
OnColorChanged?.Invoke(UseCustomColorCheckBox.Pressed ? ColorSelector.Color : null);
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
}
private void SnapToTileCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
{
OnSnapChanged?.Invoke(SnapToTileCheckBox.Pressed);
}
/// <summary>
/// Updates the decal list.
/// </summary>
public void PopulateDecals(List<SprayPainterDecalEntry> decals, SpriteSystem sprite)
{
_sprite ??= sprite;
_decals = decals;
DecalsGrid.Children.Clear();
foreach (var decal in decals)
{
var button = new TextureButton()
{
TextureNormal = sprite.Frame0(decal.Sprite),
Name = decal.Name,
ToolTip = decal.Name,
Scale = new Vector2(2, 2),
};
button.OnPressed += DecalButtonOnPressed;
if (UseCustomColorCheckBox.Pressed)
{
button.Modulate = ColorSelector.Color;
}
if (_selectedDecal == decal.Name)
{
var panelContainer = new PanelContainer()
{
PanelOverride = new StyleBoxFlat()
{
BackgroundColor = StyleNano.ButtonColorDefault,
},
Children =
{
button,
},
};
DecalsGrid.AddChild(panelContainer);
}
else
{
DecalsGrid.AddChild(button);
}
}
}
private void OnColorSelected(Color color)
{
if (!UseCustomColorCheckBox.Pressed)
return;
OnColorChanged?.Invoke(color);
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
}
private void UpdateColorButtons(bool apply)
{
Color modulateColor = apply ? ColorSelector.Color : Color.White;
foreach (var button in DecalsGrid.Children)
{
switch (button)
{
case TextureButton:
button.Modulate = modulateColor;
break;
case PanelContainer panelContainer:
{
foreach (TextureButton textureButton in panelContainer.Children)
textureButton.Modulate = modulateColor;
break;
}
}
}
}
private void DecalButtonOnPressed(BaseButton.ButtonEventArgs obj)
{
if (obj.Button.Name is not { } name)
return;
_selectedDecal = name;
OnDecalSelected?.Invoke(_selectedDecal);
if (_sprite is null)
return;
PopulateDecals(_decals, _sprite);
}
public void SetSelectedDecal(string name)
{
_selectedDecal = name;
if (_sprite is null)
return;
PopulateDecals(_decals, _sprite);
}
public void SetAngle(int degrees)
{
AngleSpinBox.OverrideValue(degrees);
}
public void SetColor(Color? color)
{
UseCustomColorCheckBox.Pressed = color != null;
if (color != null)
ColorSelector.Color = color.Value;
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
}
public void SetSnap(bool snap)
{
SnapToTileCheckBox.Pressed = snap;
}
}

View File

@@ -0,0 +1,12 @@
<BoxContainer
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Orientation="Vertical">
<Label Text="{Loc 'spray-painter-selected-style'}" />
<controls:ListContainer
Name="StyleList"
Toggle="True"
Group="True">
<!-- populated by code -->
</controls:ListContainer>
</BoxContainer>

View File

@@ -0,0 +1,66 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.SprayPainter.UI;
/// <summary>
/// Used to display a group of paintable styles in the spray painter menu.
/// (e.g. each type of paintable locker or plastic crate)
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class SprayPainterGroup : BoxContainer
{
public event Action<SpriteListData>? OnButtonPressed;
public SprayPainterGroup()
{
RobustXamlLoader.Load(this);
StyleList.GenerateItem = GenerateItems;
}
public void PopulateList(List<SpriteListData> spriteList)
{
StyleList.PopulateList(spriteList);
}
public void SelectItemByStyle(string key)
{
foreach (var elem in StyleList.Data)
{
if (elem is not SpriteListData spriteElem)
continue;
if (spriteElem.Style == key)
{
StyleList.Select(spriteElem);
break;
}
}
}
private void GenerateItems(ListData data, ListContainerButton button)
{
if (data is not SpriteListData spriteListData)
return;
var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal };
var protoView = new EntityPrototypeView();
protoView.SetPrototype(spriteListData.Prototype);
var label = new Label()
{
Text = Loc.GetString($"spray-painter-style-{spriteListData.Group.ToLower()}-{spriteListData.Style.ToLower()}")
};
box.AddChild(protoView);
box.AddChild(label);
button.AddChild(box);
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
button.OnPressed += _ => OnButtonPressed?.Invoke(spriteListData);
if (spriteListData.SelectedIndex == button.Index)
button.Pressed = true;
}
}

View File

@@ -1,34 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io"
MinSize="500 300"
SetSize="500 500"
Title="{Loc 'spray-painter-window-title'}">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
VerticalExpand="True"
SeparationOverride="4"
MinWidth="450">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
SeparationOverride="4"
MinWidth="200">
<Label Name="SelectedSpriteLabel"
Text="{Loc 'spray-painter-selected-style'}">
</Label>
<ItemList Name="SpriteList"
SizeFlagsStretchRatio="8"
VerticalExpand="True"/>
</BoxContainer>
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
SeparationOverride="4"
MinWidth="200">
<Label Name="SelectedColorLabel"
Text="{Loc 'spray-painter-selected-color'}"/>
<ItemList Name="ColorList"
SizeFlagsStretchRatio="8"
VerticalExpand="True"/>
</BoxContainer>
</BoxContainer>
MinSize="520 300"
SetSize="520 700"
Title="{Loc 'spray-painter-window-title'}">
<TabContainer Name="Tabs"/>
</DefaultWindow>

View File

@@ -1,12 +1,19 @@
using System.Linq;
using Content.Client.UserInterface.Controls;
using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.SprayPainter.UI;
/// <summary>
/// A window to select spray painter settings by object type, as well as pipe colours and decals.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class SprayPainterWindow : DefaultWindow
{
@@ -15,13 +22,33 @@ public sealed partial class SprayPainterWindow : DefaultWindow
private readonly SpriteSystem _spriteSystem;
public Action<ItemList.ItemListSelectedEventArgs>? OnSpritePicked;
public Action<ItemList.ItemListSelectedEventArgs>? OnColorPicked;
// Events
public event Action<string, string>? OnSpritePicked;
public event Action<int, bool>? OnTabChanged;
public event Action<ProtoId<DecalPrototype>>? OnDecalChanged;
public event Action<ItemList.ItemListSelectedEventArgs>? OnSetPipeColor;
public event Action<Color?>? OnDecalColorChanged;
public event Action<int>? OnDecalAngleChanged;
public event Action<bool>? OnDecalSnapChanged;
// Pipe color data
private ItemList _colorList = default!;
public Dictionary<string, int> ItemColorIndex = new();
private Dictionary<string, Color> currentPalette = new();
private const string colorLocKeyPrefix = "pipe-painter-color-";
private List<SprayPainterEntry> CurrentEntries = new List<SprayPainterEntry>();
private Dictionary<string, Color> _currentPalette = new();
private const string ColorLocKeyPrefix = "pipe-painter-color-";
// Paintable objects
private Dictionary<string, Dictionary<string, EntProtoId>> _currentStylesByGroup = new();
private Dictionary<string, List<string>> _currentGroupsByCategory = new();
// Tab controls
private Dictionary<string, SprayPainterGroup> _paintableControls = new();
private BoxContainer? _pipeControl;
// Decals
private List<SprayPainterDecalEntry> _currentDecals = [];
private SprayPainterDecals? _sprayPainterDecals;
private readonly SpriteSpecifier _colorEntryIconTexture = new SpriteSpecifier.Rsi(
new ResPath("Structures/Piping/Atmospherics/pipe.rsi"),
@@ -32,13 +59,14 @@ public sealed partial class SprayPainterWindow : DefaultWindow
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_spriteSystem = _sysMan.GetEntitySystem<SpriteSystem>();
Tabs.OnTabChanged += (index) => OnTabChanged?.Invoke(index, _sprayPainterDecals?.GetPositionInParent() == index);
}
private string GetColorLocString(string? colorKey)
{
if (string.IsNullOrEmpty(colorKey))
return Loc.GetString("pipe-painter-no-color-selected");
var locKey = colorLocKeyPrefix + colorKey;
var locKey = ColorLocKeyPrefix + colorKey;
if (!_loc.TryGetString(locKey, out var locString))
locString = colorKey;
@@ -48,51 +76,229 @@ public sealed partial class SprayPainterWindow : DefaultWindow
public string? IndexToColorKey(int index)
{
return (string?) ColorList[index].Metadata;
return _colorList[index].Text;
}
public void Populate(List<SprayPainterEntry> entries, int selectedStyle, string? selectedColorKey, Dictionary<string, Color> palette)
private void OnStyleSelected(ListData data)
{
// Only clear if the entries change. Otherwise the list would "jump" after selecting an item
if (!CurrentEntries.Equals(entries))
if (data is SpriteListData listData)
OnSpritePicked?.Invoke(listData.Group, listData.Style);
}
/// <summary>
/// Wrapper to allow for selecting/deselecting the event to avoid loops
/// </summary>
private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
{
OnSetPipeColor?.Invoke(args);
}
/// <summary>
/// Setup function for the window.
/// </summary>
/// <param name="stylesByGroup">Each group, mapped by name to the set of named styles by their associated entity prototype.</param>
/// <param name="groupsByCategory">The set of categories and the groups associated with them.</param>
/// <param name="decals">A list of each decal.</param>
public void PopulateCategories(Dictionary<string, Dictionary<string, EntProtoId>> stylesByGroup, Dictionary<string, List<string>> groupsByCategory, List<SprayPainterDecalEntry> decals)
{
bool tabsCleared = false;
var lastTab = Tabs.CurrentTab;
if (!_currentGroupsByCategory.Equals(groupsByCategory))
{
CurrentEntries = entries;
SpriteList.Clear();
foreach (var entry in entries)
// Destroy all existing tabs
tabsCleared = true;
_paintableControls.Clear();
_pipeControl = null;
_sprayPainterDecals = null;
Tabs.RemoveAllChildren();
}
// Only clear if the entries change. Otherwise the list would "jump" after selecting an item
if (tabsCleared || !_currentStylesByGroup.Equals(stylesByGroup))
{
_currentStylesByGroup = stylesByGroup;
var tabIndex = 0;
foreach (var (categoryName, categoryGroups) in groupsByCategory.OrderBy(c => c.Key))
{
SpriteList.AddItem(entry.Name, entry.Icon);
if (categoryGroups.Count <= 0)
continue;
// Repopulating controls:
// ensure that categories with multiple groups have separate subtabs
// but single-group categories do not.
if (tabsCleared)
{
TabContainer? subTabs = null;
if (categoryGroups.Count > 1)
subTabs = new();
foreach (var group in categoryGroups)
{
if (!stylesByGroup.TryGetValue(group, out var styles))
continue;
var groupControl = new SprayPainterGroup();
groupControl.OnButtonPressed += OnStyleSelected;
_paintableControls[group] = groupControl;
if (categoryGroups.Count > 1)
{
if (subTabs != null)
{
subTabs?.AddChild(groupControl);
var subTabLocalization = Loc.GetString("spray-painter-tab-group-" + group.ToLower());
TabContainer.SetTabTitle(groupControl, subTabLocalization);
}
}
else
{
Tabs.AddChild(groupControl);
}
}
if (subTabs != null)
Tabs.AddChild(subTabs);
var tabLocalization = Loc.GetString("spray-painter-tab-category-" + categoryName.ToLower());
Tabs.SetTabTitle(tabIndex, tabLocalization);
tabIndex++;
}
// Finally, populate all groups with new data.
foreach (var group in categoryGroups)
{
if (!stylesByGroup.TryGetValue(group, out var styles) ||
!_paintableControls.TryGetValue(group, out var control))
continue;
var dataList = styles
.Select(e => new SpriteListData(group, e.Key, e.Value, 0))
.OrderBy(d => Loc.GetString($"spray-painter-style-{group.ToLower()}-{d.Style.ToLower()}"))
.ToList();
control.PopulateList(dataList);
}
}
}
if (!currentPalette.Equals(palette))
{
currentPalette = palette;
ItemColorIndex.Clear();
ColorList.Clear();
PopulateColors(_currentPalette);
if (!_currentDecals.Equals(decals))
{
_currentDecals = decals;
if (_sprayPainterDecals is null)
{
_sprayPainterDecals = new SprayPainterDecals();
_sprayPainterDecals.OnDecalSelected += id => OnDecalChanged?.Invoke(id);
_sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color);
_sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle);
_sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap);
Tabs.AddChild(_sprayPainterDecals);
TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals"));
}
_sprayPainterDecals.PopulateDecals(decals, _spriteSystem);
}
if (tabsCleared)
SetSelectedTab(lastTab);
}
public void PopulateColors(Dictionary<string, Color> palette)
{
// Create pipe tab controls if they don't exist
bool tabCreated = false;
if (_pipeControl == null)
{
_pipeControl = new BoxContainer() { Orientation = BoxContainer.LayoutOrientation.Vertical };
var label = new Label() { Text = Loc.GetString("spray-painter-selected-color") };
_colorList = new ItemList() { VerticalExpand = true };
_colorList.OnItemSelected += OnColorPicked;
_pipeControl.AddChild(label);
_pipeControl.AddChild(_colorList);
Tabs.AddChild(_pipeControl);
TabContainer.SetTabTitle(_pipeControl, Loc.GetString("spray-painter-tab-category-pipes"));
tabCreated = true;
}
// Populate the tab if needed (new tab/new data)
if (tabCreated || !_currentPalette.Equals(palette))
{
_currentPalette = palette;
ItemColorIndex.Clear();
_colorList.Clear();
int index = 0;
foreach (var color in palette)
{
var locString = GetColorLocString(color.Key);
var item = ColorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture));
var item = _colorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture), metadata: color.Key);
item.IconModulate = color.Value;
item.Metadata = color.Key;
ItemColorIndex.Add(color.Key, ColorList.IndexOf(item));
ItemColorIndex.Add(color.Key, index);
index++;
}
}
// Disable event so we don't send a new event for pre-selectedStyle entry and end up in a loop
if (selectedColorKey != null)
{
var index = ItemColorIndex[selectedColorKey];
ColorList.OnItemSelected -= OnColorPicked;
ColorList[index].Selected = true;
ColorList.OnItemSelected += OnColorPicked;
}
SpriteList.OnItemSelected -= OnSpritePicked;
SpriteList[selectedStyle].Selected = true;
SpriteList.OnItemSelected += OnSpritePicked;
}
# region Setters
public void SetSelectedStyles(Dictionary<string, string> selectedStyles)
{
foreach (var (group, style) in selectedStyles)
{
if (!_paintableControls.TryGetValue(group, out var control))
continue;
control.SelectItemByStyle(style);
}
}
public void SelectColor(string color)
{
if (_colorList != null && ItemColorIndex.TryGetValue(color, out var colorIdx))
{
_colorList.OnItemSelected -= OnColorPicked;
_colorList[colorIdx].Selected = true;
_colorList.OnItemSelected += OnColorPicked;
}
}
public void SetSelectedTab(int tab)
{
Tabs.CurrentTab = int.Min(tab, Tabs.ChildCount - 1);
}
public void SetSelectedDecal(string decal)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetSelectedDecal(decal);
}
public void SetDecalAngle(int angle)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetAngle(angle);
}
public void SetDecalColor(Color? color)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetColor(color);
}
public void SetDecalSnap(bool snap)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetSnap(snap);
}
# endregion
}
public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData;

View File

@@ -11,6 +11,8 @@ namespace Content.Client.StatusIcon;
public sealed class StatusIconOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> UnshadedShader = "unshaded";
[Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IGameTiming _timing = default!;
@@ -29,7 +31,7 @@ public sealed class StatusIconOverlay : Overlay
_sprite = _entity.System<SpriteSystem>();
_transform = _entity.System<TransformSystem>();
_statusIcon = _entity.System<StatusIconSystem>();
_unshadedShader = _prototype.Index<ShaderPrototype>("unshaded").Instance();
_unshadedShader = _prototype.Index(UnshadedShader).Instance();
}
protected override void Draw(in OverlayDrawArgs args)

View File

@@ -10,6 +10,8 @@ namespace Content.Client.Stealth;
public sealed class StealthSystem : SharedStealthSystem
{
private static readonly ProtoId<ShaderPrototype> Shader = "Stealth";
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
@@ -20,7 +22,7 @@ public sealed class StealthSystem : SharedStealthSystem
{
base.Initialize();
_shader = _protoMan.Index<ShaderPrototype>("Stealth").InstanceUnique();
_shader = _protoMan.Index(Shader).InstanceUnique();
SubscribeLocalEvent<StealthComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<StealthComponent, ComponentStartup>(OnStartup);

View File

@@ -1,10 +1,15 @@
using Content.Shared.SprayPainter.Prototypes;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Storage.Visualizers;
public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStorageVisualsComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
public override void Initialize()
{
base.Initialize();
@@ -26,12 +31,34 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
SpriteSystem.LayerSetRsiState((uid, sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
}
protected override void OnAppearanceChange(EntityUid uid, EntityStorageVisualsComponent comp, ref AppearanceChangeEvent args)
protected override void OnAppearanceChange(EntityUid uid,
EntityStorageVisualsComponent comp,
ref AppearanceChangeEvent args)
{
if (args.Sprite == null
|| !AppearanceSystem.TryGetData<bool>(uid, StorageVisuals.Open, out var open, args.Component))
|| !AppearanceSystem.TryGetData<bool>(uid, StorageVisuals.Open, out var open, args.Component))
return;
var forceRedrawBase = false;
if (AppearanceSystem.TryGetData<string>(uid, PaintableVisuals.Prototype, out var prototype, args.Component))
{
if (_prototypeManager.TryIndex(prototype, out var proto))
{
if (proto.TryGetComponent(out SpriteComponent? sprite, _componentFactory))
{
SpriteSystem.SetBaseRsi((uid, args.Sprite), sprite.BaseRSI);
}
if (proto.TryGetComponent(out EntityStorageVisualsComponent? visuals, _componentFactory))
{
comp.StateBaseOpen = visuals.StateBaseOpen;
comp.StateBaseClosed = visuals.StateBaseClosed;
comp.StateDoorOpen = visuals.StateDoorOpen;
comp.StateDoorClosed = visuals.StateDoorClosed;
forceRedrawBase = true;
}
}
}
// Open/Closed state for the storage entity.
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), StorageVisualLayers.Door, out _, false))
{
@@ -52,6 +79,8 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
if (comp.StateBaseOpen != null)
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseOpen);
else if (forceRedrawBase && comp.StateBaseClosed != null)
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
}
else
{
@@ -68,6 +97,8 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
if (comp.StateBaseClosed != null)
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
else if (forceRedrawBase && comp.StateBaseOpen != null)
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseOpen);
}
}
}

View File

@@ -1,24 +1,28 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Margin="8,8,8,8" Orientation="Vertical">
<BoxContainer Margin="8,0,8,8" Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Name="StoreItemName" HorizontalExpand="True" />
<Label Name="DiscountSubText"
HorizontalAlignment="Right"/>
<Button
Name="StoreItemBuyButton"
MinWidth="64"
HorizontalAlignment="Right"
Access="Public" />
</BoxContainer>
<PanelContainer StyleClasses="HighDivider" />
<PanelContainer StyleClasses="HighDivider" Margin="0,1,0,2"/>
<BoxContainer HorizontalExpand="True" Orientation="Horizontal">
<TextureRect
Name="StoreItemTexture"
Margin="0,0,4,0"
MinSize="48 48"
Stretch="KeepAspectCentered" />
<Control MinWidth="5"/>
<RichTextLabel Name="StoreItemDescription" />
<BoxContainer Orientation="Vertical" VerticalAlignment="Center" Margin="0,0,4,0">
<TextureRect
Name="StoreItemTexture"
Margin="0,0,4,0"
MinSize="48 48"
Stretch="KeepAspectCentered" />
<Button
Name="StoreItemBuyButton"
MinWidth="48"
MaxHeight="48"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Margin="0,4,0,0"
Access="Public" />
<Label Name="DiscountSubText"
HorizontalAlignment="Center"/>
</BoxContainer>
<RichTextLabel Name="StoreItemDescription" VerticalAlignment="Top"/>
</BoxContainer>
</BoxContainer>
</Control>

View File

@@ -1,9 +1,182 @@
using System.Numerics;
using Content.Shared.CombatMode;
using Content.Shared.Interaction;
using Content.Shared.Stunnable;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Random;
namespace Content.Client.Stunnable
namespace Content.Client.Stunnable;
public sealed class StunSystem : SharedStunSystem
{
public sealed class StunSystem : SharedStunSystem
{
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
private readonly int[] _sign = [-1, 1];
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<StunVisualsComponent, AppearanceChangeEvent>(OnAppearanceChanged);
CommandBinds.Builder
.BindAfter(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnUseSecondary, true, true), typeof(SharedInteractionSystem))
.Register<StunSystem>();
}
private bool OnUseSecondary(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (args.Session?.AttachedEntity is not {Valid: true} uid)
return false;
if (args.EntityUid != uid || !HasComp<KnockedDownComponent>(uid) || !_combat.IsInCombatMode(uid))
return false;
RaisePredictiveEvent(new ForceStandUpEvent());
return true;
}
/// <summary>
/// Add stun visual layers
/// </summary>
private void OnComponentInit(Entity<StunVisualsComponent> entity, ref ComponentInit args)
{
if (!TryComp<SpriteComponent>(entity, out var sprite))
return;
var spriteEntity = (entity.Owner, sprite);
_spriteSystem.LayerMapReserve(spriteEntity, StunVisualLayers.StamCrit);
_spriteSystem.LayerSetVisible(spriteEntity, StunVisualLayers.StamCrit, false);
_spriteSystem.LayerSetOffset(spriteEntity, StunVisualLayers.StamCrit, new Vector2(0, 0.3125f));
_spriteSystem.LayerSetRsi(spriteEntity, StunVisualLayers.StamCrit, entity.Comp.StarsPath);
UpdateAppearance((entity, sprite), entity.Comp.State);
}
private void OnAppearanceChanged(Entity<StunVisualsComponent> entity, ref AppearanceChangeEvent args)
{
if (args.Sprite != null)
UpdateAppearance((entity, args.Sprite), entity.Comp.State);
}
private void UpdateAppearance(Entity<SpriteComponent?> entity, string state)
{
if (!Resolve(entity, ref entity.Comp))
return;
if (!_spriteSystem.LayerMapTryGet((entity, entity.Comp), StunVisualLayers.StamCrit, out var index, false))
return;
var visible = Appearance.TryGetData<bool>(entity, StunVisuals.SeeingStars, out var stars) && stars;
_spriteSystem.LayerSetVisible((entity, entity.Comp), index, visible);
_spriteSystem.LayerSetRsiState((entity, entity.Comp), index, state);
}
/// <summary>
/// A simple fatigue animation, a mild modification of the jittering animation. The animation constructor is
/// quite complex, but that's because the AnimationSystem doesn't have proper adjustment layers. In a potential
/// future where proper adjustment layers are added feel free to clean this up to be an animation with two adjustment
/// layers rather than one mega layer.
/// </summary>
/// <param name="sprite">The spriteComponent we're adjusting the offset of</param>
/// <param name="frequency">How many times per second does the animation run?</param>
/// <param name="jitters">How many times should we jitter during the animation? Also determines breathing frequency</param>
/// <param name="minJitter">Mininum jitter offset multiplier for X and Y directions</param>
/// <param name="maxJitter">Maximum jitter offset multiplier for X and Y directions</param>
/// <param name="breathing">Maximum breathing offset, this is in the Y direction</param>
/// <param name="startOffset">Starting offset because we don't have adjustment layers</param>
/// <param name="lastJitter">Last jitter so we don't jitter to the same quadrant</param>
/// <returns></returns>
public Animation GetFatigueAnimation(SpriteComponent sprite,
float frequency,
int jitters,
Vector2 minJitter,
Vector2 maxJitter,
float breathing,
Vector2 startOffset,
ref Vector2 lastJitter)
{
// avoid animations with negative length or infinite length
if (frequency <= 0)
return new Animation();
var breaths = new Vector2(0, breathing * 2) / jitters;
var length = 1 / frequency;
var frames = length / jitters;
var keyFrames = new List<AnimationTrackProperty.KeyFrame> { new(sprite.Offset, 0f) };
// Spits out a list of keyframes to feed to the AnimationPlayer based on the variables we've inputted
for (var i = 1; i <= jitters; i++)
{
var offset = new Vector2(_random.NextFloat(minJitter.X, maxJitter.X),
_random.NextFloat(minJitter.Y, maxJitter.Y));
offset.X *= _random.Pick(_sign);
offset.Y *= _random.Pick(_sign);
if (i == 1 && Math.Sign(offset.X) == Math.Sign(lastJitter.X)
&& Math.Sign(offset.Y) == Math.Sign(lastJitter.Y))
{
// If the sign is the same as last time on both axis we flip one randomly
// to avoid jitter staying in one quadrant too much.
if (_random.Prob(0.5f))
offset.X *= -1;
else
offset.Y *= -1;
}
lastJitter = offset;
// For the first half of the jitter, we vertically displace the sprite upwards to simulate breathing in
if (i <= jitters / 2)
{
keyFrames.Add(new AnimationTrackProperty.KeyFrame(startOffset + breaths * i + offset, frames));
}
// For the next quarter we displace the sprite down, to about 12.5% breathing offset below our starting position
// Simulates breathing out
else if (i < jitters * 3 / 4)
{
keyFrames.Add(
new AnimationTrackProperty.KeyFrame(startOffset + breaths * ( jitters - i * 1.5f ) + offset, frames));
}
// Return to our starting position for breathing, jitter reaches its final position
else
{
keyFrames.Add(
new AnimationTrackProperty.KeyFrame(startOffset + breaths * ( i - jitters ) + offset, frames));
}
}
return new Animation
{
Length = TimeSpan.FromSeconds(length),
AnimationTracks =
{
new AnimationTrackComponentProperty
{
// Heavy Breathing
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Offset),
InterpolationMode = AnimationInterpolationMode.Cubic,
KeyFrames = keyFrames,
},
}
};
}
}
public enum StunVisualLayers : byte
{
StamCrit,
}

View File

@@ -17,6 +17,8 @@ namespace Content.Client.SurveillanceCamera.UI;
[GenerateTypedNameReferences]
public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
{
private static readonly ProtoId<ShaderPrototype> CameraStaticShader = "CameraStatic";
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
@@ -53,7 +55,7 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
// This could be done better. I don't want to deal with stylesheets at the moment.
var texture = _resourceCache.GetTexture("/Textures/Interface/Nano/square_black.png");
var shader = _prototypeManager.Index<ShaderPrototype>("CameraStatic").Instance().Duplicate();
var shader = _prototypeManager.Index(CameraStaticShader).Instance().Duplicate();
CameraView.ViewportSize = new Vector2i(500, 500);
CameraView.Eye = _defaultEye; // sure

View File

@@ -38,7 +38,7 @@ public sealed partial class SurveillanceCameraSetupWindow : DefaultWindow
}
// Pass in a list of frequency prototype IDs.
public void LoadAvailableNetworks(uint currentNetwork, List<string> networks)
public void LoadAvailableNetworks(uint currentNetwork, List<ProtoId<DeviceFrequencyPrototype>> networks)
{
NetworkSelector.Clear();

View File

@@ -12,7 +12,7 @@ namespace Content.Client.UserInterface.RichText;
/// </summary>
public sealed class MonoTag : IMarkupTag
{
[ValidatePrototypeId<FontPrototype>] public const string MonoFont = "Monospace";
public static readonly ProtoId<FontPrototype> MonoFont = "Monospace";
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;

View File

@@ -67,8 +67,7 @@ public sealed partial class ChatUIController : UIController
[UISystemDependency] private readonly MindSystem? _mindSystem = default!;
[UISystemDependency] private readonly RoleCodewordSystem? _roleCodewordSystem = default!;
[ValidatePrototypeId<ColorPalettePrototype>]
private const string ChatNamePalette = "ChatNames";
private static readonly ProtoId<ColorPalettePrototype> ChatNamePalette = "ChatNames";
private string[] _chatNameColors = default!;
private bool _chatNameColorsEnabled;
@@ -232,7 +231,7 @@ public sealed partial class ChatUIController : UIController
gameplayStateLoad.OnScreenLoad += OnScreenLoad;
gameplayStateLoad.OnScreenUnload += OnScreenUnload;
var nameColors = _prototypeManager.Index<ColorPalettePrototype>(ChatNamePalette).Colors.Values.ToArray();
var nameColors = _prototypeManager.Index(ChatNamePalette).Colors.Values.ToArray();
_chatNameColors = new string[nameColors.Length];
for (var i = 0; i < nameColors.Length; i++)
{

View File

@@ -9,6 +9,8 @@ namespace Content.Client.UserInterface.Systems.DamageOverlays.Overlays;
public sealed class DamageOverlay : Overlay
{
private static readonly ProtoId<ShaderPrototype> CircleMaskShader = "GradientCircleMask";
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
@@ -49,9 +51,9 @@ public sealed class DamageOverlay : Overlay
{
// TODO: Replace
IoCManager.InjectDependencies(this);
_oxygenShader = _prototypeManager.Index<ShaderPrototype>("GradientCircleMask").InstanceUnique();
_critShader = _prototypeManager.Index<ShaderPrototype>("GradientCircleMask").InstanceUnique();
_bruteShader = _prototypeManager.Index<ShaderPrototype>("GradientCircleMask").InstanceUnique();
_oxygenShader = _prototypeManager.Index(CircleMaskShader).InstanceUnique();
_critShader = _prototypeManager.Index(CircleMaskShader).InstanceUnique();
_bruteShader = _prototypeManager.Index(CircleMaskShader).InstanceUnique();
}
protected override void Draw(in OverlayDrawArgs args)

View File

@@ -19,8 +19,7 @@ public sealed class InfoUIController : UIController, IOnStateExited<GameplayStat
private RulesPopup? _rulesPopup;
private RulesAndInfoWindow? _infoWindow;
[ValidatePrototypeId<GuideEntryPrototype>]
private const string DefaultRuleset = "DefaultRuleset";
private static readonly ProtoId<GuideEntryPrototype> DefaultRuleset = "DefaultRuleset";
public ProtoId<GuideEntryPrototype> RulesEntryId = DefaultRuleset;
@@ -92,7 +91,7 @@ public sealed class InfoUIController : UIController, IOnStateExited<GameplayStat
{
if (!_prototype.TryIndex(RulesEntryId, out var guideEntryPrototype))
{
guideEntryPrototype = _prototype.Index<GuideEntryPrototype>(DefaultRuleset);
guideEntryPrototype = _prototype.Index(DefaultRuleset);
Log.Error($"Couldn't find the following prototype: {RulesEntryId}. Falling back to {DefaultRuleset}, please check that the server has the rules set up correctly");
return guideEntryPrototype;
}

View File

@@ -185,7 +185,12 @@ public sealed class ItemGridPiece : Control, IEntityControl
handle.SetTransform(pos, iconRotation);
var box = new UIBox2(root, root + sprite.Size * scale);
handle.DrawTextureRect(sprite, box);
var ev = new BeforeRenderInGridEvent(new Color(255, 255, 255));
_entityManager.EventBus.RaiseLocalEvent(Entity, ev);
handle.DrawTextureRect(sprite, box, ev.Color);
handle.SetTransform(GlobalPixelPosition, Angle.Zero);
}
else
@@ -298,6 +303,19 @@ public sealed class ItemGridPiece : Control, IEntityControl
public EntityUid? UiEntity => Entity;
}
/// <summary>
/// This event gets raised before a sprite gets drawn in a grid and lets to change the sprite color for several gameobjects that have special sprites to render in containers.
/// </summary>
public sealed class BeforeRenderInGridEvent : EntityEventArgs
{
public Color Color { get; set; }
public BeforeRenderInGridEvent(Color color)
{
Color = color;
}
}
public enum ItemGridPieceMarks
{
First,

View File

@@ -39,8 +39,7 @@ public sealed partial class GunSystem : SharedGunSystem
[Dependency] private readonly SharedTransformSystem _xform = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[ValidatePrototypeId<EntityPrototype>]
public const string HitscanProto = "HitscanEffect";
public static readonly EntProtoId HitscanProto = "HitscanEffect";
public bool SpreadOverlay
{

View File

@@ -7,10 +7,12 @@ namespace Content.IntegrationTests.Tests.Atmos
[TestOf(typeof(AtmosAlarmThreshold))]
public sealed class AlarmThresholdTest
{
private const string AlarmThresholdTestDummyId = "AlarmThresholdTestDummy";
[TestPrototypes]
private const string Prototypes = @"
private const string Prototypes = $@"
- type: alarmThreshold
id: AlarmThresholdTestDummy
id: {AlarmThresholdTestDummyId}
upperBound: !type:AlarmThresholdSetting
threshold: 5
lowerBound: !type:AlarmThresholdSetting
@@ -30,7 +32,7 @@ namespace Content.IntegrationTests.Tests.Atmos
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
AtmosAlarmThreshold threshold = default!;
var proto = prototypeManager.Index<AtmosAlarmThresholdPrototype>("AlarmThresholdTestDummy");
var proto = prototypeManager.Index<AtmosAlarmThresholdPrototype>(AlarmThresholdTestDummyId);
threshold = new(proto);
await server.WaitAssertion(() =>

View File

@@ -15,6 +15,8 @@ namespace Content.IntegrationTests.Tests.Commands
[TestOf(typeof(RejuvenateSystem))]
public sealed class RejuvenateTest
{
private static readonly ProtoId<DamageGroupPrototype> TestDamageGroup = "Toxin";
[TestPrototypes]
private const string Prototypes = @"
- type: entity
@@ -62,7 +64,7 @@ namespace Content.IntegrationTests.Tests.Commands
});
// Kill the entity
DamageSpecifier damage = new(prototypeManager.Index<DamageGroupPrototype>("Toxin"), FixedPoint2.New(10000000));
DamageSpecifier damage = new(prototypeManager.Index(TestDamageGroup), FixedPoint2.New(10000000));
damSystem.TryChangeDamage(human, damage, true);

View File

@@ -53,6 +53,8 @@ public sealed class SuicideCommandTests
components:
- type: MaterialReclaimer";
private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide";
private static readonly ProtoId<DamageTypePrototype> DamageType = "Slash";
/// <summary>
/// Run the suicide command in the console
/// Should successfully kill the player and ghost them
@@ -144,7 +146,7 @@ public sealed class SuicideCommandTests
mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
damageableComp = entManager.GetComponent<DamageableComponent>(player);
if (protoMan.TryIndex<DamageTypePrototype>("Slash", out var slashProto))
if (protoMan.TryIndex(DamageType, out var slashProto))
damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5)));
});

View File

@@ -8,6 +8,8 @@ namespace Content.IntegrationTests.Tests.Construction.Interaction;
public sealed class WindowRepair : InteractionTest
{
private static readonly ProtoId<DamageTypePrototype> BluntDamageType = "Blunt";
[Test]
public async Task RepairReinforcedWindow()
{
@@ -16,7 +18,7 @@ public sealed class WindowRepair : InteractionTest
// Damage the entity.
var sys = SEntMan.System<DamageableSystem>();
var comp = Comp<DamageableComponent>();
var damageType = Server.ResolveDependency<IPrototypeManager>().Index<DamageTypePrototype>("Blunt");
var damageType = Server.ProtoMan.Index(BluntDamageType);
var damage = new DamageSpecifier(damageType, FixedPoint2.New(10));
Assert.That(comp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero));
await Server.WaitPost(() => sys.TryChangeDamage(SEntMan.GetEntity(Target), damage, ignoreResistances: true));

View File

@@ -1,9 +1,7 @@
using System.Linq;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
@@ -14,66 +12,79 @@ namespace Content.IntegrationTests.Tests.Damageable
[TestOf(typeof(DamageableSystem))]
public sealed class DamageableTest
{
private const string TestDamageableEntityId = "TestDamageableEntityId";
private const string TestGroup1 = "TestGroup1";
private const string TestGroup2 = "TestGroup2";
private const string TestGroup3 = "TestGroup3";
private const string TestDamage1 = "TestDamage1";
private const string TestDamage2a = "TestDamage2a";
private const string TestDamage2b = "TestDamage2b";
private const string TestDamage3a = "TestDamage3a";
private const string TestDamage3b = "TestDamage3b";
private const string TestDamage3c = "TestDamage3c";
[TestPrototypes]
private const string Prototypes = @"
private const string Prototypes = $@"
# Define some damage groups
- type: damageType
id: TestDamage1
id: {TestDamage1}
name: damage-type-blunt
- type: damageType
id: TestDamage2a
id: {TestDamage2a}
name: damage-type-blunt
- type: damageType
id: TestDamage2b
id: {TestDamage2b}
name: damage-type-blunt
- type: damageType
id: TestDamage3a
id: {TestDamage3a}
name: damage-type-blunt
- type: damageType
id: TestDamage3b
id: {TestDamage3b}
name: damage-type-blunt
- type: damageType
id: TestDamage3c
id: {TestDamage3c}
name: damage-type-blunt
# Define damage Groups with 1,2,3 damage types
- type: damageGroup
id: TestGroup1
id: {TestGroup1}
name: damage-group-brute
damageTypes:
- TestDamage1
- {TestDamage1}
- type: damageGroup
id: TestGroup2
id: {TestGroup2}
name: damage-group-brute
damageTypes:
- TestDamage2a
- TestDamage2b
- {TestDamage2a}
- {TestDamage2b}
- type: damageGroup
id: TestGroup3
id: {TestGroup3}
name: damage-group-brute
damageTypes:
- TestDamage3a
- TestDamage3b
- TestDamage3c
- {TestDamage3a}
- {TestDamage3b}
- {TestDamage3c}
# This container should not support TestDamage1 or TestDamage2b
- type: damageContainer
id: testDamageContainer
supportedGroups:
- TestGroup3
- {TestGroup3}
supportedTypes:
- TestDamage2a
- {TestDamage2a}
- type: entity
id: TestDamageableEntityId
name: TestDamageableEntityId
id: {TestDamageableEntityId}
name: {TestDamageableEntityId}
components:
- type: Damageable
damageContainer: testDamageContainer
@@ -113,20 +124,20 @@ namespace Content.IntegrationTests.Tests.Damageable
{
var coordinates = map.MapCoords;
sDamageableEntity = sEntityManager.SpawnEntity("TestDamageableEntityId", coordinates);
sDamageableEntity = sEntityManager.SpawnEntity(TestDamageableEntityId, coordinates);
sDamageableComponent = sEntityManager.GetComponent<DamageableComponent>(sDamageableEntity);
sDamageableSystem = sEntitySystemManager.GetEntitySystem<DamageableSystem>();
group1 = sPrototypeManager.Index<DamageGroupPrototype>("TestGroup1");
group2 = sPrototypeManager.Index<DamageGroupPrototype>("TestGroup2");
group3 = sPrototypeManager.Index<DamageGroupPrototype>("TestGroup3");
group1 = sPrototypeManager.Index<DamageGroupPrototype>(TestGroup1);
group2 = sPrototypeManager.Index<DamageGroupPrototype>(TestGroup2);
group3 = sPrototypeManager.Index<DamageGroupPrototype>(TestGroup3);
type1 = sPrototypeManager.Index<DamageTypePrototype>("TestDamage1");
type2a = sPrototypeManager.Index<DamageTypePrototype>("TestDamage2a");
type2b = sPrototypeManager.Index<DamageTypePrototype>("TestDamage2b");
type3a = sPrototypeManager.Index<DamageTypePrototype>("TestDamage3a");
type3b = sPrototypeManager.Index<DamageTypePrototype>("TestDamage3b");
type3c = sPrototypeManager.Index<DamageTypePrototype>("TestDamage3c");
type1 = sPrototypeManager.Index<DamageTypePrototype>(TestDamage1);
type2a = sPrototypeManager.Index<DamageTypePrototype>(TestDamage2a);
type2b = sPrototypeManager.Index<DamageTypePrototype>(TestDamage2b);
type3a = sPrototypeManager.Index<DamageTypePrototype>(TestDamage3a);
type3b = sPrototypeManager.Index<DamageTypePrototype>(TestDamage3b);
type3c = sPrototypeManager.Index<DamageTypePrototype>(TestDamage3c);
});
await server.WaitRunTicks(5);

View File

@@ -54,8 +54,8 @@ namespace Content.IntegrationTests.Tests.Destructible
await server.WaitAssertion(() =>
{
var bruteDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBrute");
var burnDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBurn");
var bruteDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>(TestBruteDamageGroupId);
var burnDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>(TestBurnDamageGroupId);
DamageSpecifier bruteDamage = new(bruteDamageGroup, FixedPoint2.New(5));
DamageSpecifier burnDamage = new(burnDamageGroup, FixedPoint2.New(5));

View File

@@ -49,8 +49,8 @@ namespace Content.IntegrationTests.Tests.Destructible
await server.WaitAssertion(() =>
{
var bluntDamageType = protoManager.Index<DamageTypePrototype>("TestBlunt");
var slashDamageType = protoManager.Index<DamageTypePrototype>("TestSlash");
var bluntDamageType = protoManager.Index<DamageTypePrototype>(TestBluntDamageTypeId);
var slashDamageType = protoManager.Index<DamageTypePrototype>(TestSlashDamageTypeId);
var bluntDamage = new DamageSpecifier(bluntDamageType, 5);
var slashDamage = new DamageSpecifier(slashDamageType, 5);

View File

@@ -39,7 +39,7 @@ namespace Content.IntegrationTests.Tests.Destructible
await server.WaitAssertion(() =>
{
var coordinates = sEntityManager.GetComponent<TransformComponent>(sDestructibleEntity).Coordinates;
var bruteDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>("TestBrute");
var bruteDamageGroup = sPrototypeManager.Index<DamageGroupPrototype>(TestBruteDamageGroupId);
DamageSpecifier bruteDamage = new(bruteDamageGroup, 50);
#pragma warning disable NUnit2045 // Interdependent assertions.

View File

@@ -7,48 +7,56 @@ namespace Content.IntegrationTests.Tests.Destructible
public const string DestructibleDestructionEntityId = "DestructibleTestsDestructibleDestructionEntity";
public const string DestructibleDamageTypeEntityId = "DestructibleTestsDestructibleDamageTypeEntity";
public const string DestructibleDamageGroupEntityId = "DestructibleTestsDestructibleDamageGroupEntity";
public const string TestBruteDamageGroupId = "TestBrute";
public const string TestBurnDamageGroupId = "TestBurn";
public const string TestBluntDamageTypeId = "TestBlunt";
public const string TestSlashDamageTypeId = "TestSlash";
public const string TestPiercingDamageTypeId = "TestPiercing";
public const string TestHeatDamageTypeId = "TestHeat";
public const string TestShockDamageTypeId = "TestShock";
public const string TestColdDamageTypeId = "TestCold";
[TestPrototypes]
public const string DamagePrototypes = $@"
- type: damageType
id: TestBlunt
id: {TestBluntDamageTypeId}
name: damage-type-blunt
- type: damageType
id: TestSlash
id: {TestSlashDamageTypeId}
name: damage-type-slash
- type: damageType
id: TestPiercing
id: {TestPiercingDamageTypeId}
name: damage-type-piercing
- type: damageType
id: TestHeat
id: {TestHeatDamageTypeId}
name: damage-type-heat
- type: damageType
id: TestShock
id: {TestShockDamageTypeId}
name: damage-type-shock
- type: damageType
id: TestCold
id: {TestColdDamageTypeId}
name: damage-type-cold
- type: damageGroup
id: TestBrute
id: {TestBruteDamageGroupId}
name: damage-group-brute
damageTypes:
- TestBlunt
- TestSlash
- TestPiercing
- {TestBluntDamageTypeId}
- {TestSlashDamageTypeId}
- {TestPiercingDamageTypeId}
- type: damageGroup
id: TestBurn
id: {TestBurnDamageGroupId}
name: damage-group-burn
damageTypes:
- TestHeat
- TestShock
- TestCold
- {TestHeatDamageTypeId}
- {TestShockDamageTypeId}
- {TestColdDamageTypeId}
- type: entity
id: {SpawnedEntityId}
@@ -114,10 +122,10 @@ namespace Content.IntegrationTests.Tests.Destructible
!type:AndTrigger
triggers:
- !type:DamageTypeTrigger
damageType: TestBlunt
damageType: {TestBluntDamageTypeId}
damage: 10
- !type:DamageTypeTrigger
damageType: TestSlash
damageType: {TestSlashDamageTypeId}
damage: 10
- type: entity
@@ -131,10 +139,10 @@ namespace Content.IntegrationTests.Tests.Destructible
!type:AndTrigger
triggers:
- !type:DamageGroupTrigger
damageGroup: TestBrute
damageGroup: {TestBruteDamageGroupId}
damage: 10
- !type:DamageGroupTrigger
damageGroup: TestBurn
damageGroup: {TestBurnDamageGroupId}
damage: 10";
}
}

View File

@@ -61,7 +61,7 @@ namespace Content.IntegrationTests.Tests.Destructible
await server.WaitAssertion(() =>
{
var bluntDamage = new DamageSpecifier(sPrototypeManager.Index<DamageTypePrototype>("TestBlunt"), 10);
var bluntDamage = new DamageSpecifier(sPrototypeManager.Index<DamageTypePrototype>(TestBluntDamageTypeId), 10);
sDamageableSystem.TryChangeDamage(sDestructibleEntity, bluntDamage, true);

View File

@@ -110,7 +110,7 @@ public sealed class AbsorbentTest
solutionContainerSystem.AddSolution(refillableSoln.Value, new Solution(NonEvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfNonEvaporable));
// Act
absorbentSystem.Mop(user, refillable, absorbent, component);
absorbentSystem.Mop((absorbent, component), user, refillable);
// Assert
var absorbentComposition = absorbentSolution.GetReagentPrototypes(prototypeManager).ToDictionary(r => r.Key.ID, r => r.Value);
@@ -167,7 +167,7 @@ public sealed class AbsorbentTest
solutionContainerSystem.AddSolution(refillableSoln.Value, new Solution(NonEvaporablePrototypeId, testCase.InitialRefillableSolution.VolumeOfNonEvaporable));
// Act
absorbentSystem.Mop(user, refillable, absorbent, component);
absorbentSystem.Mop((absorbent, component), user, refillable);
// Assert
var absorbentComposition = absorbentSolution.GetReagentPrototypes(prototypeManager).ToDictionary(r => r.Key.ID, r => r.Value);

View File

@@ -4,7 +4,6 @@ using Content.Shared.Tag;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.IntegrationTests.Tests.Linter;
@@ -66,25 +65,25 @@ public sealed class StaticFieldValidationTest
[Reflect(false)]
private sealed class StringValid
{
[ValidatePrototypeId<TagPrototype>] public static string Tag = "StaticFieldTestTag";
public static readonly ProtoId<TagPrototype> Tag = "StaticFieldTestTag";
}
[Reflect(false)]
private sealed class StringInvalid
{
[ValidatePrototypeId<TagPrototype>] public static string Tag = string.Empty;
public static readonly ProtoId<TagPrototype> Tag = string.Empty;
}
[Reflect(false)]
private sealed class StringArrayValid
{
[ValidatePrototypeId<TagPrototype>] public static string[] Tag = ["StaticFieldTestTag", "StaticFieldTestTag"];
public static readonly ProtoId<TagPrototype>[] Tag = ["StaticFieldTestTag", "StaticFieldTestTag"];
}
[Reflect(false)]
private sealed class StringArrayInvalid
{
[ValidatePrototypeId<TagPrototype>] public static string[] Tag = [string.Empty, "StaticFieldTestTag", string.Empty];
public static readonly ProtoId<TagPrototype>[] Tag = [string.Empty, "StaticFieldTestTag", string.Empty];
}
[Reflect(false)]

View File

@@ -1,38 +1,48 @@
#nullable enable
using System.Linq;
using Content.IntegrationTests.Pair;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Players;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared.Players;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Minds;
[TestFixture]
public sealed class GhostRoleTests
{
private const string GhostRoleProtoId = "GhostRoleTestEntity";
private const string TestMobProtoId = "GhostRoleTestMob";
[TestPrototypes]
private const string Prototypes = @"
- type: entity
id: GhostRoleTestEntity
components:
- type: MindContainer
- type: GhostRole
- type: GhostTakeoverAvailable
";
private const string Prototypes = $"""
- type: entity
id: {GhostRoleProtoId}
components:
- type: MindContainer
- type: GhostRole
- type: GhostTakeoverAvailable
- type: MobState
- type: entity
id: {TestMobProtoId}
components:
- type: MobState # MobState is required for correct determination of if the player can return to body or not
""";
/// <summary>
/// This is a simple test that just checks if a player can take a ghost role and then regain control of their
/// original entity without encountering errors.
/// </summary>
[Test]
public async Task TakeRoleAndReturn()
[TestCase(true)]
[TestCase(false)]
public async Task TakeRoleAndReturn(bool adminGhost)
{
var ghostCommand = adminGhost ? "aghost" : "ghost";
await using var pair = await PoolManager.GetServerClient(new PoolSettings
{
Dirty = true,
@@ -49,36 +59,67 @@ public sealed class GhostRoleTests
var conHost = client.ResolveDependency<IConsoleHost>();
var mindSystem = entMan.System<SharedMindSystem>();
var session = sPlayerMan.Sessions.Single();
var originalMindId = session.ContentData()!.Mind!.Value;
var originalPlayerMindId = session.ContentData()!.Mind!.Value;
// Check that there are no ghosts
Assert.That(entMan.Count<GhostComponent>(), Is.Zero);
// Spawn player entity & attach
EntityUid originalMob = default;
EntityUid originalPlayerMob = default;
await server.WaitPost(() =>
{
originalMob = entMan.SpawnEntity(null, mapData.GridCoords);
mindSystem.TransferTo(originalMindId, originalMob, true);
originalPlayerMob = entMan.SpawnEntity(TestMobProtoId, mapData.GridCoords);
mindSystem.TransferTo(originalPlayerMindId, originalPlayerMob, true);
});
// Check player got attached.
await pair.RunTicksSync(10);
Assert.That(session.AttachedEntity, Is.EqualTo(originalMob));
var originalMind = entMan.GetComponent<MindComponent>(originalMindId);
Assert.That(originalMind.OwnedEntity, Is.EqualTo(originalMob));
Assert.That(originalMind.VisitingEntity, Is.Null);
var originalPlayerMind = entMan.GetComponent<MindComponent>(originalPlayerMindId);
Assert.Multiple(() =>
{
// Check player got attached.
Assert.That(session.AttachedEntity, Is.EqualTo(originalPlayerMob));
Assert.That(originalPlayerMind.OwnedEntity, Is.EqualTo(originalPlayerMob));
Assert.That(originalPlayerMind.VisitingEntity, Is.Null);
Assert.That(originalPlayerMind.OriginalOwnerUserId, Is.EqualTo(session.UserId));
// Check that there are still no ghosts
Assert.That(entMan.Count<GhostComponent>(), Is.Zero);
});
// Use the ghost command
conHost.ExecuteCommand("ghost");
conHost.ExecuteCommand(ghostCommand);
await pair.RunTicksSync(10);
var ghost = session.AttachedEntity;
Assert.That(entMan.HasComponent<GhostComponent>(ghost));
Assert.That(ghost, Is.Not.EqualTo(originalMob));
Assert.That(session.ContentData()?.Mind, Is.EqualTo(originalMindId));
Assert.That(originalMind.OwnedEntity, Is.EqualTo(originalMob), $"Original mob: {originalMob}, Ghost: {ghost}");
Assert.That(originalMind.VisitingEntity, Is.EqualTo(ghost));
var ghostOne = session.AttachedEntity;
Assert.Multiple(() =>
{
// Assert that the ghost is a new entity with a new mind
Assert.That(entMan.HasComponent<GhostComponent>(ghostOne));
Assert.That(ghostOne, Is.Not.EqualTo(originalPlayerMob));
Assert.That(session.ContentData()?.Mind, Is.EqualTo(originalPlayerMindId));
if (adminGhost)
{
// aghost, so the player mob should still own the mind, but the mind is visiting the ghost.
Assert.That(originalPlayerMind.OwnedEntity, Is.EqualTo(originalPlayerMob));
Assert.That(originalPlayerMind.VisitingEntity, Is.EqualTo(ghostOne));
Assert.That(originalPlayerMind.UserId, Is.EqualTo(session.UserId));
}
else
{
// player ghost, can't return. The mind is owned by the ghost, and is not visiting.
Assert.That(originalPlayerMind.OwnedEntity, Is.EqualTo(ghostOne));
Assert.That(originalPlayerMind.VisitingEntity, Is.Null);
}
// Check that we're tracking the original owner for round end screen
Assert.That(originalPlayerMind.OriginalOwnerUserId, Is.EqualTo(session.UserId));
// Check that there is only one ghost
Assert.That(entMan.Count<GhostComponent>(), Is.EqualTo(1));
});
// Spawn ghost takeover entity.
EntityUid ghostRole = default;
await server.WaitPost(() => ghostRole = entMan.SpawnEntity("GhostRoleTestEntity", mapData.GridCoords));
await server.WaitPost(() => ghostRole = entMan.SpawnEntity(GhostRoleProtoId, mapData.GridCoords));
// Take the ghost role
await server.WaitPost(() =>
@@ -89,40 +130,118 @@ public sealed class GhostRoleTests
// Check player got attached to ghost role.
await pair.RunTicksSync(10);
var newMindId = session.ContentData()!.Mind!.Value;
var newMind = entMan.GetComponent<MindComponent>(newMindId);
Assert.That(newMindId, Is.Not.EqualTo(originalMindId));
Assert.That(session.AttachedEntity, Is.EqualTo(ghostRole));
Assert.That(newMind.OwnedEntity, Is.EqualTo(ghostRole));
Assert.That(newMind.VisitingEntity, Is.Null);
var ghostRoleMindId = session.ContentData()!.Mind!.Value;
var ghostRoleMind = entMan.GetComponent<MindComponent>(ghostRoleMindId);
Assert.Multiple(() =>
{
// Check that the ghost role mind is new
Assert.That(ghostRoleMindId, Is.Not.EqualTo(originalPlayerMindId));
// Original mind should be unaffected, but the ghost will have deleted itself.
Assert.That(originalMind.OwnedEntity, Is.EqualTo(originalMob));
Assert.That(originalMind.VisitingEntity, Is.Null);
Assert.That(entMan.Deleted(ghost));
// Check that the session and mind are properly attached to the ghost role
Assert.That(session.AttachedEntity, Is.EqualTo(ghostRole));
Assert.That(ghostRoleMind.OwnedEntity, Is.EqualTo(ghostRole));
Assert.That(ghostRoleMind.VisitingEntity, Is.Null);
// Original mind should be unaffected, but the ghost will have deleted itself.
if (adminGhost)
{
// aghost case, the original player mob should still own the mind, and that mind is not visiting.
Assert.That(originalPlayerMind.OwnedEntity, Is.EqualTo(originalPlayerMob));
}
else
{
// player ghost case, the original mind is disconnected and not owned by an entity.
// This mind cannot be returned to
Assert.That(originalPlayerMind.OwnedEntity, Is.Null);
}
// In either case the original player mind is not visiting anything, not connected to any user.
Assert.That(originalPlayerMind.VisitingEntity, Is.Null);
Assert.That(originalPlayerMind.UserId, Is.Null);
// Now the original owner of both minds should permanently be set to this session.
Assert.That(originalPlayerMind.OriginalOwnerUserId, Is.EqualTo(session.UserId));
Assert.That(ghostRoleMind.OriginalOwnerUserId, Is.EqualTo(session.UserId));
// Make sure that the ghost was deleted
Assert.That(entMan.Deleted(ghostOne));
// Check that there is are no lingereing ghosts
Assert.That(entMan.Count<GhostComponent>(), Is.Zero);
});
// Ghost again.
conHost.ExecuteCommand("ghost");
conHost.ExecuteCommand(ghostCommand);
await pair.RunTicksSync(10);
var otherGhost = session.AttachedEntity;
Assert.That(entMan.HasComponent<GhostComponent>(otherGhost));
Assert.That(otherGhost, Is.Not.EqualTo(originalMob));
Assert.That(otherGhost, Is.Not.EqualTo(ghostRole));
Assert.That(session.ContentData()?.Mind, Is.EqualTo(newMindId));
Assert.That(newMind.OwnedEntity, Is.EqualTo(ghostRole));
Assert.That(newMind.VisitingEntity, Is.EqualTo(session.AttachedEntity));
var ghostTwo = session.AttachedEntity;
Assert.Multiple(() =>
{
// Check that the new ghost is a new entity
Assert.That(entMan.HasComponent<GhostComponent>(ghostTwo));
Assert.That(ghostTwo, Is.Not.EqualTo(originalPlayerMob));
Assert.That(ghostTwo, Is.Not.EqualTo(ghostRole));
Assert.That(session.ContentData()?.Mind, Is.EqualTo(ghostRoleMindId));
if(adminGhost)
{
// aghost case, the ghost role mind should be owned by the ghost role entity,
// the ghost role mind is visiting the new ghost
Assert.That(ghostRoleMind.OwnedEntity, Is.EqualTo(ghostRole));
Assert.That(ghostRoleMind.VisitingEntity, Is.EqualTo(ghostTwo));
}
else
{
// player ghost, can't return. The mind is owned by the ghost, and is not visiting.
Assert.That(ghostRoleMind.OwnedEntity, Is.EqualTo(ghostTwo));
Assert.That(ghostRoleMind.VisitingEntity, Is.Null);
}
// Check that the original mind is still not attached to a user
Assert.That(originalPlayerMind.UserId, Is.Null);
// Check that original owners of other minds are still tracked
Assert.That(originalPlayerMind.OriginalOwnerUserId, Is.EqualTo(session.UserId));
Assert.That(ghostRoleMind.OriginalOwnerUserId, Is.EqualTo(session.UserId));
// Check that there is exactly one ghost
Assert.That(entMan.Count<GhostComponent>(), Is.EqualTo(1));
});
if (!adminGhost)
{
// End of the normal player ghost role test
await pair.CleanReturnAsync();
return;
}
// Next, control the original entity again:
await server.WaitPost(() => mindSystem.SetUserId(originalMindId, session.UserId));
await server.WaitPost(() => mindSystem.SetUserId(originalPlayerMindId, session.UserId));
await pair.RunTicksSync(10);
Assert.That(session.AttachedEntity, Is.EqualTo(originalMob));
Assert.That(originalMind.OwnedEntity, Is.EqualTo(originalMob));
Assert.That(originalMind.VisitingEntity, Is.Null);
// the ghost-role mind is unaffected, though the ghost will have deleted itself
Assert.That(newMind.OwnedEntity, Is.EqualTo(ghostRole));
Assert.That(newMind.VisitingEntity, Is.Null);
Assert.That(entMan.Deleted(otherGhost));
Assert.Multiple(() =>
{
// Check that we are attached
Assert.That(session.AttachedEntity, Is.EqualTo(originalPlayerMob));
// Check the ownership of the original mind
Assert.That(originalPlayerMind.OwnedEntity, Is.EqualTo(originalPlayerMob));
Assert.That(originalPlayerMind.VisitingEntity, Is.Null);
Assert.That(originalPlayerMind.UserId, Is.EqualTo(session.UserId));
// Check that the ghost-role mind is unaffected
Assert.That(ghostRoleMind.OwnedEntity, Is.EqualTo(ghostRole));
Assert.That(ghostRoleMind.VisitingEntity, Is.Null);
// Check that the second ghost is deleted
Assert.That(entMan.Deleted(ghostTwo));
// Check that the original owners of the previous minds are still tracked
Assert.That(originalPlayerMind.OriginalOwnerUserId, Is.EqualTo(session.UserId));
Assert.That(ghostRoleMind.OriginalOwnerUserId, Is.EqualTo(session.UserId));
// Check that there is are no lingereing ghosts
Assert.That(entMan.Count<GhostComponent>(), Is.Zero);
});
await pair.CleanReturnAsync();
}

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