Merge branch 'master' into air-alarm-fixup

This commit is contained in:
Flipp Syder
2022-08-23 13:21:05 -07:00
committed by GitHub
498 changed files with 21068 additions and 52226 deletions

View File

@@ -11,7 +11,7 @@ namespace Content.Client.AME.UI
{
private AMEWindow? _window;
public AMEControllerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public AMEControllerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -10,7 +10,7 @@ namespace Content.Client.Access.UI
{
private AgentIDCardWindow? _window;
public AgentIDCardBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public AgentIDCardBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Access.UI
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
public IdCardConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public IdCardConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
@@ -59,7 +59,7 @@ namespace Content.Client.Access.UI
_window?.UpdateState(castState);
}
public void SubmitData(string newFullName, string newJobTitle, List<string> newAccessList)
public void SubmitData(string newFullName, string newJobTitle, List<string> newAccessList, string newJobPrototype)
{
if (newFullName.Length > MaxFullNameLength)
newFullName = newFullName[..MaxFullNameLength];
@@ -70,7 +70,8 @@ namespace Content.Client.Access.UI
SendMessage(new WriteToTargetIdMessage(
newFullName,
newJobTitle,
newAccessList));
newAccessList,
newJobPrototype));
}
}
}

View File

@@ -26,8 +26,10 @@ namespace Content.Client.Access.UI
private string? _lastFullName;
private string? _lastJobTitle;
private string? _lastJobProto;
public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager, List<string> accessLevels)
public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager,
List<string> accessLevels)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
@@ -101,6 +103,7 @@ namespace Content.Client.Access.UI
}
JobTitleLineEdit.Text = Loc.GetString(job.Name);
args.Button.SelectId(args.Id);
ClearAllAccess();
@@ -181,17 +184,29 @@ namespace Content.Client.Access.UI
}
}
var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype);
if (jobIndex >= 0)
{
JobPresetOptionButton.SelectId(jobIndex);
}
_lastFullName = state.TargetIdFullName;
_lastJobTitle = state.TargetIdJobTitle;
_lastJobProto = state.TargetIdJobPrototype;
}
private void SubmitData()
{
// Don't send this if it isn't dirty.
var jobProtoDirty = _lastJobProto != null &&
_jobPrototypeIds[JobPresetOptionButton.SelectedId] != _lastJobProto;
_owner.SubmitData(
FullNameLineEdit.Text,
JobTitleLineEdit.Text,
// Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
_accessButtons.Where(x => x.Value.Pressed).Select(x => x.Key).ToList());
_accessButtons.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(),
jobProtoDirty ? _jobPrototypeIds[JobPresetOptionButton.SelectedId] : string.Empty);
}
}
}

View File

@@ -8,7 +8,7 @@ namespace Content.Client.AirlockPainter.UI
private AirlockPainterWindow? _window;
public List<string> Styles = new();
public AirlockPainterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public AirlockPainterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -8,7 +8,7 @@ namespace Content.Client.Arcade.UI
{
private BlockGameMenu? _menu;
public BlockGameBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public BlockGameBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -11,7 +11,7 @@ namespace Content.Client.Arcade.UI
//public SharedSpaceVillainArcadeComponent SpaceVillainArcade;
public SpaceVillainArcadeBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public SpaceVillainArcadeBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
SendAction(PlayerAction.RequestData);
}

View File

@@ -6,7 +6,7 @@ namespace Content.Client.Atmos.UI
{
public sealed class GasAnalyzerBoundUserInterface : BoundUserInterface
{
public GasAnalyzerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public GasAnalyzerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -14,7 +14,7 @@ namespace Content.Client.Atmos.UI
private GasCanisterWindow? _window;
public GasCanisterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public GasCanisterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -18,7 +18,7 @@ namespace Content.Client.Atmos.UI
private GasFilterWindow? _window;
private const float MaxTransferRate = Atmospherics.MaxTransferRate;
public GasFilterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public GasFilterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -19,7 +19,7 @@ namespace Content.Client.Atmos.UI
private GasMixerWindow? _window;
private const float MaxPressure = Atmospherics.MaxOutputPressure;
public GasMixerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public GasMixerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -19,7 +19,7 @@ namespace Content.Client.Atmos.UI
private GasPressurePumpWindow? _window;
private const float MaxPressure = Atmospherics.MaxOutputPressure;
public GasPressurePumpBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public GasPressurePumpBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -18,7 +18,7 @@ namespace Content.Client.Atmos.UI
private float _minTemp = 0.0f;
private float _maxTemp = 0.0f;
public GasThermomachineBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public GasThermomachineBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -16,7 +16,7 @@ namespace Content.Client.Atmos.UI
private GasVolumePumpWindow? _window;
private const float MaxTransferRate = Atmospherics.MaxTransferRate;
public GasVolumePumpBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public GasVolumePumpBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -14,6 +14,8 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using System.Threading;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.Audio
@@ -44,7 +46,7 @@ namespace Content.Client.Audio
/// <summary>
/// What the ambience has been set to.
/// </summary>
private SoundCollectionPrototype _currentCollection = default!;
private SoundCollectionPrototype? _currentCollection;
private CancellationTokenSource _timerCancelTokenSource = new();
private SoundCollectionPrototype _spaceAmbience = default!;
@@ -58,12 +60,22 @@ namespace Content.Client.Audio
_spaceAmbience = _prototypeManager.Index<SoundCollectionPrototype>("SpaceAmbienceBase");
_currentCollection = _stationAmbience;
// TOOD: Ideally audio loading streamed better / we have more robust audio but this is quite annoying
var cache = IoCManager.Resolve<IResourceCache>();
foreach (var audio in _spaceAmbience.PickFiles)
{
cache.GetResource<AudioResource>(audio.ToString());
}
_configManager.OnValueChanged(CCVars.AmbienceVolume, AmbienceCVarChanged);
_configManager.OnValueChanged(CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged);
_configManager.OnValueChanged(CCVars.StationAmbienceEnabled, StationAmbienceCVarChanged);
_configManager.OnValueChanged(CCVars.SpaceAmbienceEnabled, SpaceAmbienceCVarChanged);
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<EntParentChangedMessage>(EntParentChanged);
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
_stateManager.OnStateChanged += StateManagerOnStateChanged;
@@ -73,10 +85,28 @@ namespace Content.Client.Audio
_gameTicker.LobbyStatusUpdated += LobbySongReceived;
}
private void OnPlayerAttached(PlayerAttachedEvent ev)
{
if (!TryComp<TransformComponent>(ev.Entity, out var xform))
return;
CheckAmbience(xform);
}
private void OnPlayerDetached(PlayerDetachedEvent ev)
{
EndAmbience();
}
public override void Shutdown()
{
base.Shutdown();
_configManager.UnsubValueChanged(CCVars.AmbienceVolume, AmbienceCVarChanged);
_configManager.UnsubValueChanged(CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged);
_configManager.UnsubValueChanged(CCVars.StationAmbienceEnabled, StationAmbienceCVarChanged);
_configManager.UnsubValueChanged(CCVars.SpaceAmbienceEnabled, SpaceAmbienceCVarChanged);
_stateManager.OnStateChanged -= StateManagerOnStateChanged;
_client.PlayerJoinedServer -= OnJoin;
@@ -88,15 +118,12 @@ namespace Content.Client.Audio
EndLobbyMusic();
}
private void EntParentChanged(ref EntParentChangedMessage message)
private void CheckAmbience(TransformComponent xform)
{
if(_playMan.LocalPlayer is null || _playMan.LocalPlayer.ControlledEntity != message.Entity ||
!_timing.IsFirstTimePredicted) return;
// Check if we traversed to grid.
if (message.Transform.GridUid != null)
if (xform.GridUid != null)
{
if (_currentCollection == _stationAmbience) return;
if (_currentCollection == _stationAmbience)
return;
ChangeAmbience(_stationAmbience);
}
else
@@ -105,6 +132,15 @@ namespace Content.Client.Audio
}
}
private void EntParentChanged(ref EntParentChangedMessage message)
{
if(_playMan.LocalPlayer is null || _playMan.LocalPlayer.ControlledEntity != message.Entity ||
!_timing.IsFirstTimePredicted) return;
// Check if we traversed to grid.
CheckAmbience(message.Transform);
}
private void ChangeAmbience(SoundCollectionPrototype newAmbience)
{
if (_currentCollection == newAmbience) return;
@@ -172,7 +208,8 @@ namespace Content.Client.Audio
private void StartAmbience()
{
EndAmbience();
if (!CanPlayCollection(_currentCollection)) return;
if (_currentCollection == null || !CanPlayCollection(_currentCollection))
return;
_playingCollection = _currentCollection;
var file = _robustRandom.Pick(_currentCollection.PickFiles).ToString();
_ambientStream = SoundSystem.Play(file, Filter.Local(), _ambientParams.WithVolume(_ambientParams.Volume + _configManager.GetCVar(CCVars.AmbienceVolume)));
@@ -197,6 +234,9 @@ namespace Content.Client.Audio
private void StationAmbienceCVarChanged(bool enabled)
{
if (_currentCollection == null)
return;
if (enabled && _stateManager.CurrentState is GameScreen && _currentCollection.ID == _stationAmbience.ID)
{
StartAmbience();
@@ -209,6 +249,9 @@ namespace Content.Client.Audio
private void SpaceAmbienceCVarChanged(bool enabled)
{
if (_currentCollection == null)
return;
if (enabled && _stateManager.CurrentState is GameScreen && _currentCollection.ID == _spaceAmbience.ID)
{
StartAmbience();

View File

@@ -14,7 +14,7 @@ namespace Content.Client.Body.UI
[ViewVariables]
private BodyScannerDisplay? _display;
public BodyScannerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { }
public BodyScannerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { }
protected override void Open()
{

View File

@@ -39,7 +39,7 @@ namespace Content.Client.Cargo.BUI
/// </summary>
private CargoProductPrototype? _product;
public CargoOrderConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public CargoOrderConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -11,7 +11,7 @@ public sealed class CargoShuttleConsoleBoundUserInterface : BoundUserInterface
{
private CargoShuttleMenu? _menu;
public CargoShuttleConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
public CargoShuttleConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {}
protected override void Open()
{

View File

@@ -23,7 +23,7 @@ namespace Content.Client.CharacterAppearance
{
private MagicMirrorWindow? _window;
public MagicMirrorBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public MagicMirrorBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -14,7 +14,7 @@ namespace Content.Client.Chemistry.UI
{
private ChemMasterWindow? _window;
public ChemMasterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public ChemMasterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -17,7 +17,7 @@ namespace Content.Client.Chemistry.UI
private ReagentDispenserWindow? _window;
private ReagentDispenserBoundUserInterfaceState? _lastState;
public ReagentDispenserBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public ReagentDispenserBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -27,7 +27,7 @@ namespace Content.Client.Chemistry.UI
_window.OpenCentered();
}
public TransferAmountBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public TransferAmountBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -9,7 +9,7 @@ namespace Content.Client.CloningConsole.UI
{
private CloningConsoleWindow? _window;
public CloningConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public CloningConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -26,7 +26,7 @@ namespace Content.Client.Communications.UI
public int Countdown => _expectedCountdownTime == null ? 0 : Math.Max((int)_expectedCountdownTime.Value.Subtract(_gameTiming.CurTime).TotalSeconds, 0);
private TimeSpan? _expectedCountdownTime;
public CommunicationsConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public CommunicationsConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -26,7 +26,7 @@ namespace Content.Client.Computer
}
// Alas, this constructor has to be copied to the subclass. :(
public ComputerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
public ComputerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {}
protected override void UpdateState(BoundUserInterfaceState state)
{
@@ -58,7 +58,7 @@ namespace Content.Client.Computer
[Virtual]
public class ComputerBoundUserInterfaceBase : BoundUserInterface
{
public ComputerBoundUserInterfaceBase(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
public ComputerBoundUserInterfaceBase(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {}
public new void SendMessage(BoundUserInterfaceMessage msg)
{

View File

@@ -10,7 +10,7 @@ namespace Content.Client.Configurable.UI
{
public Regex? Validation { get; internal set; }
public ConfigurationBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public ConfigurationBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -167,7 +167,7 @@ namespace Content.Client.Construction.UI
continue;
}
if (!string.IsNullOrEmpty(category) && category != Loc.GetString("construction-presenter-category-all"))
if (!string.IsNullOrEmpty(category) && category != Loc.GetString("construction-category-all"))
{
if (recipe.Category != category)
continue;
@@ -191,11 +191,11 @@ namespace Content.Client.Construction.UI
var uniqueCategories = new HashSet<string>();
// hard-coded to show all recipes
uniqueCategories.Add(Loc.GetString("construction-presenter-category-all"));
uniqueCategories.Add(Loc.GetString("construction-category-all"));
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
var category = Loc.GetString(prototype.Category);
var category = prototype.Category;
if (!string.IsNullOrEmpty(category))
uniqueCategories.Add(category);

View File

@@ -10,7 +10,7 @@ namespace Content.Client.Crayon.UI
{
public sealed class CrayonBoundUserInterface : BoundUserInterface
{
public CrayonBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public CrayonBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -131,11 +131,11 @@ public sealed partial class CrewManifestUi : DefaultWindow
foreach (var entry in entries)
{
var name = new Label()
var name = new RichTextLabel()
{
HorizontalExpand = true,
Text = entry.Name
};
name.SetMessage(entry.Name);
var titleContainer = new BoxContainer()
{
@@ -143,10 +143,8 @@ public sealed partial class CrewManifestUi : DefaultWindow
HorizontalExpand = true
};
var title = new Label()
{
Text = Loc.GetString(entry.JobTitle)
};
var title = new RichTextLabel();
title.SetMessage(Loc.GetString(entry.JobTitle));
if (rsi != null)

View File

@@ -1,857 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Utility;
// apologies in advance for all the != null checks,
// my IDE wouldn't stop complaining about these
namespace Content.Client.Damage
{
/// <summary>
/// A simple visualizer for any entity with a DamageableComponent
/// to display the status of how damaged it is.
///
/// Can either be an overlay for an entity, or target multiple
/// layers on the same entity.
///
/// This can be disabled dynamically by passing into SetData,
/// key DamageVisualizerKeys.Disabled, value bool
/// (DamageVisualizerKeys lives in Content.Shared.Damage)
///
/// Damage layers, if targetting layers, can also be dynamically
/// disabled if needed by passing into SetData, the name/enum
/// of the sprite layer, and then passing in a bool value
/// (true to enable, false to disable).
/// </summary>
public sealed class DamageVisualizer : AppearanceVisualizer
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private const string _name = "DamageVisualizer";
/// <summary>
/// Damage thresholds between damage state changes.
///
/// If there are any negative thresholds, or there is
/// less than one threshold, the visualizer is marked
/// as invalid.
/// </summary>
/// <remarks>
/// A 'zeroth' threshold is automatically added,
/// and this list is automatically sorted for
/// efficiency beforehand. As such, the zeroth
/// threshold is not required - and negative
/// thresholds are automatically caught as
/// invalid. The zeroth threshold automatically
/// sets all layers to invisible, so a sprite
/// isn't required for it.
/// </remarks>
[DataField("thresholds", required: true)]
private List<FixedPoint2> _thresholds = new();
/// <summary>
/// Layers to target, by layerMapKey.
/// If a target layer map key is invalid
/// (in essence, undefined), then the target
/// layer is removed from the list for efficiency.
///
/// If no layers are valid, then the visualizer
/// is marked as invalid.
///
/// If this is not defined, however, the visualizer
/// instead adds an overlay to the sprite.
/// </summary>
/// <remarks>
/// Layers can be disabled here by passing
/// the layer's name as a key to SetData,
/// and passing in a bool set to either 'false'
/// to disable it, or 'true' to enable it.
/// Setting the layer as disabled will make it
/// completely invisible.
/// </remarks>
[DataField("targetLayers")]
private List<Enum>? _targetLayers;
/// <summary>
/// The actual sprites for every damage group
/// that the entity should display visually.
///
/// This is keyed by a damage group identifier
/// (for example, Brute), and has a value
/// of a DamageVisualizerSprite (see below)
/// </summary>
[DataField("damageOverlayGroups")]
private readonly Dictionary<string, DamageVisualizerSprite>? _damageOverlayGroups;
/// <summary>
/// Sets if you want sprites to overlay the
/// entity when damaged, or if you would
/// rather have each target layer's state
/// replaced by a different state
/// within its RSI.
///
/// This cannot be set to false if:
/// - There are no target layers
/// - There is no damage group
/// </summary>
[DataField("overlay")]
private readonly bool _overlay = true;
/// <summary>
/// A single damage group to target.
/// This should only be defined if
/// overlay is set to false.
/// If this is defined with damageSprites,
/// this will be ignored.
/// </summary>
/// <remarks>
/// This is here because otherwise,
/// you would need several permutations
/// of group sprites depending on
/// what kind of damage combination
/// you would want, on which threshold.
/// </remarks>
[DataField("damageGroup")]
private readonly string? _damageGroup;
/// <summary>
/// Set this if you want incoming damage to be
/// divided.
/// </summary>
/// <remarks>
/// This is more useful if you have similar
/// damage sprites inbetween entities,
/// but with different damage thresholds
/// and you want to avoid duplicating
/// these sprites.
/// </remarks>
[DataField("damageDivisor")]
private float _divisor = 1;
/// <summary>
/// Set this to track all damage, instead of specific groups.
/// </summary>
/// <remarks>
/// This will only work if you have damageOverlay
/// defined - otherwise, it will not work.
/// </remarks>
[DataField("trackAllDamage")]
private readonly bool _trackAllDamage = false;
/// <summary>
/// This is the overlay sprite used, if _trackAllDamage is
/// enabled. Supports no complex per-group layering,
/// just an actually simple damage overlay. See
/// DamageVisualizerSprite for more information.
/// </summary>
[DataField("damageOverlay")]
private readonly DamageVisualizerSprite? _damageOverlay;
// deals with the edge case of human damage visuals not
// being in color without making a Dict<Dict<Dict<Dict<Dict<Dict...
[DataDefinition]
internal sealed class DamageVisualizerSprite
{
/// <summary>
/// The RSI path for the damage visualizer
/// group overlay.
/// </summary>
/// <remarks>
/// States in here will require one of four
/// forms:
///
/// If tracking damage groups:
/// - {base_state}_{group}_{threshold} if targetting
/// a static layer on a sprite (either as an
/// overlay or as a state change)
/// - DamageOverlay_{group}_{threshold} if not
/// targetting a layer on a sprite.
///
/// If not tracking damage groups:
/// - {base_state}_{threshold} if it is targetting
/// a layer
/// - DamageOverlay_{threshold} if not targetting
/// a layer.
/// </remarks>
[DataField("sprite", required: true)]
public readonly string Sprite = default!;
/// <summary>
/// The color of this sprite overlay.
/// Supports only hexadecimal format.
/// </summary>
[DataField("color")]
public readonly string? Color;
}
/// <summary>
/// Initializes an entity to be managed by this appearance controller.
/// DO NOT assume this is your only entity. Visualizers are shared.
/// </summary>
[Obsolete("Subscribe to your component being initialised instead.")]
public override void InitializeEntity(EntityUid entity)
{
base.InitializeEntity(entity);
IoCManager.InjectDependencies(this);
var damageData = _entityManager.EnsureComponent<DamageVisualizerDataComponent>(entity);
VerifyVisualizerSetup(entity, damageData);
if (damageData.Valid)
InitializeVisualizer(entity, damageData);
}
private void VerifyVisualizerSetup(EntityUid entity, DamageVisualizerDataComponent damageData)
{
if (_thresholds.Count < 1)
{
Logger.ErrorS(_name, $"Thresholds were invalid for entity {entity}. Thresholds: {_thresholds}");
damageData.Valid = false;
return;
}
if (_divisor == 0)
{
Logger.ErrorS(_name, $"Divisor for {entity} is set to zero.");
damageData.Valid = false;
return;
}
if (_overlay)
{
if (_damageOverlayGroups == null && _damageOverlay == null)
{
Logger.ErrorS(_name, $"Enabled overlay without defined damage overlay sprites on {entity}.");
damageData.Valid = false;
return;
}
if (_trackAllDamage && _damageOverlay == null)
{
Logger.ErrorS(_name, $"Enabled all damage tracking without a damage overlay sprite on {entity}.");
damageData.Valid = false;
return;
}
if (!_trackAllDamage && _damageOverlay != null)
{
Logger.WarningS(_name, $"Disabled all damage tracking with a damage overlay sprite on {entity}.");
damageData.Valid = false;
return;
}
if (_trackAllDamage && _damageOverlayGroups != null)
{
Logger.WarningS(_name, $"Enabled all damage tracking with damage overlay groups on {entity}.");
damageData.Valid = false;
return;
}
}
else if (!_overlay)
{
if (_targetLayers == null)
{
Logger.ErrorS(_name, $"Disabled overlay without target layers on {entity}.");
damageData.Valid = false;
return;
}
if (_damageOverlayGroups != null || _damageOverlay != null)
{
Logger.ErrorS(_name, $"Disabled overlay with defined damage overlay sprites on {entity}.");
damageData.Valid = false;
return;
}
if (_damageGroup == null)
{
Logger.ErrorS(_name, $"Disabled overlay without defined damage group on {entity}.");
damageData.Valid = false;
return;
}
}
if (_damageOverlayGroups != null && _damageGroup != null)
{
Logger.WarningS(_name, $"Damage overlay sprites and damage group are both defined on {entity}.");
}
if (_damageOverlay != null && _damageGroup != null)
{
Logger.WarningS(_name, $"Damage overlay sprites and damage group are both defined on {entity}.");
}
}
private void InitializeVisualizer(EntityUid entity, DamageVisualizerDataComponent damageData)
{
if (!_entityManager.TryGetComponent(entity, out SpriteComponent? spriteComponent)
|| !_entityManager.TryGetComponent<DamageableComponent?>(entity, out var damageComponent)
|| !_entityManager.HasComponent<AppearanceComponent>(entity))
return;
_thresholds.Add(FixedPoint2.Zero);
_thresholds.Sort();
if (_thresholds[0] != 0)
{
Logger.ErrorS(_name, $"Thresholds were invalid for entity {entity}. Thresholds: {_thresholds}");
damageData.Valid = false;
return;
}
// If the damage container on our entity's DamageableComponent
// is not null, we can try to check through its groups.
if (damageComponent.DamageContainerID != null
&& _prototypeManager.TryIndex<DamageContainerPrototype>(damageComponent.DamageContainerID, out var damageContainer))
{
// Are we using damage overlay sprites by group?
// Check if the container matches the supported groups,
// and start cacheing the last threshold.
if (_damageOverlayGroups != null)
{
foreach (string damageType in _damageOverlayGroups.Keys)
{
if (!damageContainer.SupportedGroups.Contains(damageType))
{
Logger.ErrorS(_name, $"Damage key {damageType} was invalid for entity {entity}.");
damageData.Valid = false;
return;
}
damageData.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero);
}
}
// Are we tracking a single damage group without overlay instead?
// See if that group is in our entity's damage container.
else if (!_overlay && _damageGroup != null)
{
if (!damageContainer.SupportedGroups.Contains(_damageGroup))
{
Logger.ErrorS(_name, $"Damage keys were invalid for entity {entity}.");
damageData.Valid = false;
return;
}
damageData.LastThresholdPerGroup.Add(_damageGroup, FixedPoint2.Zero);
}
}
// Ditto above, but instead we go through every group.
else // oh boy! time to enumerate through every single group!
{
var damagePrototypeIdList = _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>()
.Select((p, _) => p.ID)
.ToList();
if (_damageOverlayGroups != null)
foreach (string damageType in _damageOverlayGroups.Keys)
{
if (!damagePrototypeIdList.Contains(damageType))
{
Logger.ErrorS(_name, $"Damage keys were invalid for entity {entity}.");
damageData.Valid = false;
return;
}
damageData.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero);
}
else if (_damageGroup != null)
{
if (!damagePrototypeIdList.Contains(_damageGroup))
{
Logger.ErrorS(_name, $"Damage keys were invalid for entity {entity}.");
damageData.Valid = false;
return;
}
damageData.LastThresholdPerGroup.Add(_damageGroup, FixedPoint2.Zero);
}
}
// If we're targetting any layers, and the amount of
// layers is greater than zero, we start reserving
// all the layers needed to track damage groups
// on the entity.
if (_targetLayers != null && _targetLayers.Count > 0)
{
// This should ensure that the layers we're targetting
// are valid for the visualizer's use.
//
// If the layer doesn't have a base state, or
// the layer key just doesn't exist, we skip it.
foreach (var key in _targetLayers)
{
if (!spriteComponent.LayerMapTryGet(key, out int index)
|| spriteComponent.LayerGetState(index).ToString() == null)
{
Logger.WarningS(_name, $"Layer at key {key} was invalid for entity {entity}.");
continue;
}
damageData.TargetLayerMapKeys.Add(key);
};
// Similar to damage overlay groups, if none of the targetted
// sprite layers could be used, we display an error and
// invalidate the visualizer without crashing.
if (damageData.TargetLayerMapKeys.Count == 0)
{
Logger.ErrorS(_name, $"Target layers were invalid for entity {entity}.");
damageData.Valid = false;
return;
}
// Otherwise, we start reserving layers. Since the filtering
// loop above ensures that all of these layers are not null,
// and have valid state IDs, there should be no issues.
foreach (object layer in damageData.TargetLayerMapKeys)
{
int layerCount = spriteComponent.AllLayers.Count();
int index = spriteComponent.LayerMapGet(layer);
string layerState = spriteComponent.LayerGetState(index)!.ToString()!;
if (index + 1 != layerCount)
{
index += 1;
}
damageData.LayerMapKeyStates.Add(layer, layerState);
// If we're an overlay, and we're targetting groups,
// we reserve layers per damage group.
if (_overlay && _damageOverlayGroups != null)
{
foreach (var (group, sprite) in _damageOverlayGroups)
{
AddDamageLayerToSprite(spriteComponent,
sprite,
$"{layerState}_{group}_{_thresholds[1]}",
$"{layer}{group}",
index);
}
damageData.DisabledLayers.Add(layer, false);
}
// If we're not targetting groups, and we're still
// using an overlay, we instead just add a general
// overlay that reflects on how much damage
// was taken.
else if (_damageOverlay != null)
{
AddDamageLayerToSprite(spriteComponent,
_damageOverlay,
$"{layerState}_{_thresholds[1]}",
$"{layer}trackDamage",
index);
damageData.DisabledLayers.Add(layer, false);
}
}
}
// If we're not targetting layers, however,
// we should ensure that we instead
// reserve it as an overlay.
else
{
if (_damageOverlayGroups != null)
{
foreach (var (group, sprite) in _damageOverlayGroups)
{
AddDamageLayerToSprite(spriteComponent,
sprite,
$"DamageOverlay_{group}_{_thresholds[1]}",
$"DamageOverlay{group}");
damageData.TopMostLayerKey = $"DamageOverlay{group}";
}
}
else if (_damageOverlay != null)
{
AddDamageLayerToSprite(spriteComponent,
_damageOverlay,
$"DamageOverlay_{_thresholds[1]}",
"DamageOverlay");
damageData.TopMostLayerKey = $"DamageOverlay";
}
}
}
/// <summary>
/// Adds a damage tracking layer to a given sprite component.
/// </summary>
private void AddDamageLayerToSprite(SpriteComponent spriteComponent, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null)
{
int newLayer = spriteComponent.AddLayer(
new SpriteSpecifier.Rsi(
new ResourcePath(sprite.Sprite), state
), index);
spriteComponent.LayerMapSet(mapKey, newLayer);
if (sprite.Color != null)
spriteComponent.LayerSetColor(newLayer, Color.FromHex(sprite.Color));
spriteComponent.LayerSetVisible(newLayer, false);
}
[Obsolete("Subscribe to AppearanceChangeEvent instead.")]
public override void OnChangeData(AppearanceComponent component)
{
var entities = _entityManager;
if (!entities.TryGetComponent(component.Owner, out DamageVisualizerDataComponent? damageData))
return;
if (!damageData.Valid)
return;
// If this was passed into the component, we update
// the data to ensure that the current disabled
// bool matches.
if (component.TryGetData<bool>(DamageVisualizerKeys.Disabled, out var disabledStatus))
if (disabledStatus != damageData.Disabled)
damageData.Disabled = disabledStatus;
if (damageData.Disabled)
return;
HandleDamage(component, damageData);
}
private void HandleDamage(AppearanceComponent component, DamageVisualizerDataComponent damageData)
{
var entities = _entityManager;
if (!entities.TryGetComponent(component.Owner, out SpriteComponent? spriteComponent)
|| !entities.TryGetComponent(component.Owner, out DamageableComponent? damageComponent))
return;
if (_targetLayers != null && _damageOverlayGroups != null)
UpdateDisabledLayers(spriteComponent, component, damageData);
if (_overlay && _damageOverlayGroups != null && _targetLayers == null)
CheckOverlayOrdering(spriteComponent, damageData);
if (component.TryGetData<bool>(DamageVisualizerKeys.ForceUpdate, out bool update)
&& update)
{
ForceUpdateLayers(damageComponent, spriteComponent, damageData);
return;
}
if (_trackAllDamage)
{
UpdateDamageVisuals(damageComponent, spriteComponent, damageData);
}
else if (component.TryGetData(DamageVisualizerKeys.DamageUpdateGroups, out DamageVisualizerGroupData data))
{
UpdateDamageVisuals(data.GroupList, damageComponent, spriteComponent, damageData);
}
}
/// <summary>
/// Checks if any layers were disabled in the last
/// data update. Disabled layers mean that the
/// layer will no longer be visible, or obtain
/// any damage updates.
/// </summary>
private void UpdateDisabledLayers(SpriteComponent spriteComponent, AppearanceComponent component, DamageVisualizerDataComponent damageData)
{
foreach (var layer in damageData.TargetLayerMapKeys)
{
bool? layerStatus = null;
if (component.TryGetData<bool>(layer, out var layerStateEnum))
layerStatus = layerStateEnum;
if (layerStatus == null)
continue;
if (damageData.DisabledLayers[layer] != (bool) layerStatus)
{
damageData.DisabledLayers[layer] = (bool) layerStatus;
if (!_trackAllDamage && _damageOverlayGroups != null)
foreach (string damageGroup in _damageOverlayGroups!.Keys)
spriteComponent.LayerSetVisible($"{layer}{damageGroup}", damageData.DisabledLayers[layer]);
else if (_trackAllDamage)
spriteComponent.LayerSetVisible($"{layer}trackDamage", damageData.DisabledLayers[layer]);
}
}
}
/// <summary>
/// Checks the overlay ordering on the current
/// sprite component, compared to the
/// data for the visualizer. If the top
/// most layer doesn't match, the sprite
/// layers are recreated and placed on top.
/// </summary>
private void CheckOverlayOrdering(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData)
{
if (spriteComponent[damageData.TopMostLayerKey] != spriteComponent[spriteComponent.AllLayers.Count() - 1])
{
if (!_trackAllDamage && _damageOverlayGroups != null)
{
foreach (var (damageGroup, sprite) in _damageOverlayGroups)
{
FixedPoint2 threshold = damageData.LastThresholdPerGroup[damageGroup];
ReorderOverlaySprite(spriteComponent,
damageData,
sprite,
$"DamageOverlay{damageGroup}",
$"DamageOverlay_{damageGroup}",
threshold);
}
}
else if (_trackAllDamage && _damageOverlay != null)
{
ReorderOverlaySprite(spriteComponent,
damageData,
_damageOverlay,
$"DamageOverlay",
$"DamageOverlay",
damageData.LastDamageThreshold);
}
}
}
private void ReorderOverlaySprite(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData, DamageVisualizerSprite sprite, string key, string statePrefix, FixedPoint2 threshold)
{
spriteComponent.LayerMapTryGet(key, out int spriteLayer);
bool visibility = spriteComponent[spriteLayer].Visible;
spriteComponent.RemoveLayer(spriteLayer);
if (threshold == FixedPoint2.Zero) // these should automatically be invisible
threshold = _thresholds[1];
spriteLayer = spriteComponent.AddLayer(
new SpriteSpecifier.Rsi(
new ResourcePath(sprite.Sprite),
$"{statePrefix}_{threshold}"
),
spriteLayer);
spriteComponent.LayerMapSet(key, spriteLayer);
spriteComponent.LayerSetVisible(spriteLayer, visibility);
// this is somewhat iffy since it constantly reallocates
damageData.TopMostLayerKey = key;
}
/// <summary>
/// Updates damage visuals without tracking
/// any damage groups.
/// </summary>
private void UpdateDamageVisuals(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData)
{
if (!CheckThresholdBoundary(damageComponent.TotalDamage, damageData.LastDamageThreshold, out FixedPoint2 threshold))
return;
damageData.LastDamageThreshold = threshold;
if (_targetLayers != null)
{
foreach (var layerMapKey in damageData.TargetLayerMapKeys)
UpdateTargetLayer(spriteComponent, damageData, layerMapKey, threshold);
}
else
{
UpdateOverlay(spriteComponent, threshold);
}
}
/// <summary>
/// Updates damage visuals by damage group,
/// according to the list of damage groups
/// passed into it.
/// </summary>
private void UpdateDamageVisuals(List<string> delta, DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData)
{
foreach (var damageGroup in delta)
{
if (!_overlay && damageGroup != _damageGroup)
continue;
if (!_prototypeManager.TryIndex<DamageGroupPrototype>(damageGroup, out var damageGroupPrototype)
|| !damageComponent.Damage.TryGetDamageInGroup(damageGroupPrototype, out FixedPoint2 damageTotal))
continue;
if (!damageData.LastThresholdPerGroup.TryGetValue(damageGroup, out FixedPoint2 lastThreshold)
|| !CheckThresholdBoundary(damageTotal, lastThreshold, out FixedPoint2 threshold))
continue;
damageData.LastThresholdPerGroup[damageGroup] = threshold;
if (_targetLayers != null)
{
foreach (var layerMapKey in damageData.TargetLayerMapKeys)
UpdateTargetLayer(spriteComponent, damageData, layerMapKey, damageGroup, threshold);
}
else
{
UpdateOverlay(spriteComponent, damageGroup, threshold);
}
}
}
/// <summary>
/// Checks if a threshold boundary was passed.
/// </summary>
private bool CheckThresholdBoundary(FixedPoint2 damageTotal, FixedPoint2 lastThreshold, out FixedPoint2 threshold)
{
threshold = FixedPoint2.Zero;
damageTotal = damageTotal / _divisor;
int thresholdIndex = _thresholds.BinarySearch(damageTotal);
if (thresholdIndex < 0)
{
thresholdIndex = ~thresholdIndex;
threshold = _thresholds[thresholdIndex - 1];
}
else
{
threshold = _thresholds[thresholdIndex];
}
if (threshold == lastThreshold)
return false;
return true;
}
/// <summary>
/// This is the entry point for
/// forcing an update on all damage layers.
/// Does different things depending on
/// the configuration of the visualizer.
/// </summary>
private void ForceUpdateLayers(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData)
{
if (_damageOverlayGroups != null)
{
UpdateDamageVisuals(_damageOverlayGroups.Keys.ToList(), damageComponent, spriteComponent, damageData);
}
else if (_damageGroup != null)
{
UpdateDamageVisuals(new List<string>(){ _damageGroup }, damageComponent, spriteComponent, damageData);
}
else if (_damageOverlay != null)
{
UpdateDamageVisuals(damageComponent, spriteComponent, damageData);
}
}
/// <summary>
/// Updates a target layer. Without a damage group passed in,
/// it assumes you're updating a layer that is tracking all
/// damage.
/// </summary>
private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData, object layerMapKey, FixedPoint2 threshold)
{
if (_overlay && _damageOverlayGroups != null)
{
if (!damageData.DisabledLayers[layerMapKey])
{
string layerState = damageData.LayerMapKeyStates[layerMapKey];
spriteComponent.LayerMapTryGet($"{layerMapKey}trackDamage", out int spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"{layerState}",
threshold);
}
}
else if (!_overlay)
{
string layerState = damageData.LayerMapKeyStates[layerMapKey];
spriteComponent.LayerMapTryGet(layerMapKey, out int spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"{layerState}",
threshold);
}
}
/// <summary>
/// Updates a target layer by damage group.
/// </summary>
private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualizerDataComponent damageData, object layerMapKey, string damageGroup, FixedPoint2 threshold)
{
if (_overlay && _damageOverlayGroups != null)
{
if (_damageOverlayGroups.ContainsKey(damageGroup) && !damageData.DisabledLayers[layerMapKey])
{
string layerState = damageData.LayerMapKeyStates[layerMapKey];
spriteComponent.LayerMapTryGet($"{layerMapKey}{damageGroup}", out int spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"{layerState}_{damageGroup}",
threshold);
}
}
else if (!_overlay)
{
string layerState = damageData.LayerMapKeyStates[layerMapKey];
spriteComponent.LayerMapTryGet(layerMapKey, out int spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"{layerState}_{damageGroup}",
threshold);
}
}
/// <summary>
/// Updates an overlay that is tracking all damage.
/// </summary>
private void UpdateOverlay(SpriteComponent spriteComponent, FixedPoint2 threshold)
{
spriteComponent.LayerMapTryGet($"DamageOverlay", out int spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"DamageOverlay",
threshold);
}
/// <summary>
/// Updates an overlay based on damage group.
/// </summary>
private void UpdateOverlay(SpriteComponent spriteComponent, string damageGroup, FixedPoint2 threshold)
{
if (_damageOverlayGroups != null)
{
if (_damageOverlayGroups.ContainsKey(damageGroup))
{
spriteComponent.LayerMapTryGet($"DamageOverlay{damageGroup}", out int spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"DamageOverlay_{damageGroup}",
threshold);
}
}
}
/// <summary>
/// Updates a layer on the sprite by what
/// prefix it has (calculated by whatever
/// function calls it), and what threshold
/// was passed into it.
/// </summary>
private void UpdateDamageLayerState(SpriteComponent spriteComponent, int spriteLayer, string statePrefix, FixedPoint2 threshold)
{
if (threshold == 0)
{
spriteComponent.LayerSetVisible(spriteLayer, false);
}
else
{
if (!spriteComponent[spriteLayer].Visible)
{
spriteComponent.LayerSetVisible(spriteLayer, true);
}
spriteComponent.LayerSetState(spriteLayer, $"{statePrefix}_{threshold}");
}
}
}
}

View File

@@ -1,25 +0,0 @@
using System.Collections.Generic;
using Content.Shared.FixedPoint;
using Robust.Shared.GameObjects;
namespace Content.Client.Damage
{
// Stores all the data for a DamageVisualizer.
//
// Storing it inside of the AppearanceComponent's data
// dictionary was too messy, but at least we can
// store it in the entity itself as a separate,
// dynamically added component.
[RegisterComponent]
public sealed class DamageVisualizerDataComponent : Component
{
public List<Enum> TargetLayerMapKeys = new();
public bool Disabled = false;
public bool Valid = true;
public FixedPoint2 LastDamageThreshold = FixedPoint2.Zero;
public Dictionary<object, bool> DisabledLayers = new();
public Dictionary<object, string> LayerMapKeyStates = new();
public Dictionary<string, FixedPoint2> LastThresholdPerGroup = new();
public string TopMostLayerKey = default!;
}
}

View File

@@ -0,0 +1,161 @@
using Content.Shared.FixedPoint;
namespace Content.Client.Damage;
[RegisterComponent]
public sealed class DamageVisualsComponent : Component
{
/// <summary>
/// Damage thresholds between damage state changes.
///
/// If there are any negative thresholds, or there is
/// less than one threshold, the visualizer is marked
/// as invalid.
/// </summary>
/// <remarks>
/// A 'zeroth' threshold is automatically added,
/// and this list is automatically sorted for
/// efficiency beforehand. As such, the zeroth
/// threshold is not required - and negative
/// thresholds are automatically caught as
/// invalid. The zeroth threshold automatically
/// sets all layers to invisible, so a sprite
/// isn't required for it.
/// </remarks>
[DataField("thresholds", required: true)]
public List<FixedPoint2> Thresholds = new();
/// <summary>
/// Layers to target, by layerMapKey.
/// If a target layer map key is invalid
/// (in essence, undefined), then the target
/// layer is removed from the list for efficiency.
///
/// If no layers are valid, then the visualizer
/// is marked as invalid.
///
/// If this is not defined, however, the visualizer
/// instead adds an overlay to the sprite.
/// </summary>
/// <remarks>
/// Layers can be disabled here by passing
/// the layer's name as a key to SetData,
/// and passing in a bool set to either 'false'
/// to disable it, or 'true' to enable it.
/// Setting the layer as disabled will make it
/// completely invisible.
/// </remarks>
[DataField("targetLayers")] public List<Enum>? TargetLayers;
/// <summary>
/// The actual sprites for every damage group
/// that the entity should display visually.
///
/// This is keyed by a damage group identifier
/// (for example, Brute), and has a value
/// of a DamageVisualizerSprite (see below)
/// </summary>
[DataField("damageOverlayGroups")] public readonly Dictionary<string, DamageVisualizerSprite>? DamageOverlayGroups;
/// <summary>
/// Sets if you want sprites to overlay the
/// entity when damaged, or if you would
/// rather have each target layer's state
/// replaced by a different state
/// within its RSI.
///
/// This cannot be set to false if:
/// - There are no target layers
/// - There is no damage group
/// </summary>
[DataField("overlay")] public readonly bool Overlay = true;
/// <summary>
/// A single damage group to target.
/// This should only be defined if
/// overlay is set to false.
/// If this is defined with damageSprites,
/// this will be ignored.
/// </summary>
/// <remarks>
/// This is here because otherwise,
/// you would need several permutations
/// of group sprites depending on
/// what kind of damage combination
/// you would want, on which threshold.
/// </remarks>
[DataField("damageGroup")] public readonly string? DamageGroup;
/// <summary>
/// Set this if you want incoming damage to be
/// divided.
/// </summary>
/// <remarks>
/// This is more useful if you have similar
/// damage sprites in between entities,
/// but with different damage thresholds
/// and you want to avoid duplicating
/// these sprites.
/// </remarks>
[DataField("damageDivisor")] public float Divisor = 1;
/// <summary>
/// Set this to track all damage, instead of specific groups.
/// </summary>
/// <remarks>
/// This will only work if you have damageOverlay
/// defined - otherwise, it will not work.
/// </remarks>
[DataField("trackAllDamage")] public readonly bool TrackAllDamage;
/// <summary>
/// This is the overlay sprite used, if _trackAllDamage is
/// enabled. Supports no complex per-group layering,
/// just an actually simple damage overlay. See
/// DamageVisualizerSprite for more information.
/// </summary>
[DataField("damageOverlay")] public readonly DamageVisualizerSprite? DamageOverlay;
public readonly List<Enum> TargetLayerMapKeys = new();
public bool Disabled = false;
public bool Valid = true;
public FixedPoint2 LastDamageThreshold = FixedPoint2.Zero;
public readonly Dictionary<object, bool> DisabledLayers = new();
public readonly Dictionary<object, string> LayerMapKeyStates = new();
public readonly Dictionary<string, FixedPoint2> LastThresholdPerGroup = new();
public string TopMostLayerKey = default!;
}
// deals with the edge case of human damage visuals not
// being in color without making a Dict<Dict<Dict<Dict<Dict<Dict...
[DataDefinition]
public sealed class DamageVisualizerSprite
{
/// <summary>
/// The RSI path for the damage visualizer
/// group overlay.
/// </summary>
/// <remarks>
/// States in here will require one of four
/// forms:
///
/// If tracking damage groups:
/// - {base_state}_{group}_{threshold} if targeting
/// a static layer on a sprite (either as an
/// overlay or as a state change)
/// - DamageOverlay_{group}_{threshold} if not
/// targeting a layer on a sprite.
///
/// If not tracking damage groups:
/// - {base_state}_{threshold} if it is targeting
/// a layer
/// - DamageOverlay_{threshold} if not targeting
/// a layer.
/// </remarks>
[DataField("sprite", required: true)] public readonly string Sprite = default!;
/// <summary>
/// The color of this sprite overlay.
/// Supports only hexadecimal format.
/// </summary>
[DataField("color")] public readonly string? Color;
}

View File

@@ -0,0 +1,698 @@
using System.Linq;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Damage;
/// <summary>
/// A simple visualizer for any entity with a DamageableComponent
/// to display the status of how damaged it is.
///
/// Can either be an overlay for an entity, or target multiple
/// layers on the same entity.
///
/// This can be disabled dynamically by passing into SetData,
/// key DamageVisualizerKeys.Disabled, value bool
/// (DamageVisualizerKeys lives in Content.Shared.Damage)
///
/// Damage layers, if targeting layers, can also be dynamically
/// disabled if needed by passing into SetData, the name/enum
/// of the sprite layer, and then passing in a bool value
/// (true to enable, false to disable).
/// </summary>
public sealed class DamageVisualsSystem : VisualizerSystem<DamageVisualsComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private const string SawmillName = "DamageVisuals";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DamageVisualsComponent, ComponentInit>(InitializeEntity);
}
private void InitializeEntity(EntityUid entity, DamageVisualsComponent comp, ComponentInit args)
{
VerifyVisualizerSetup(entity, comp);
if (!comp.Valid)
{
RemCompDeferred<DamageVisualsComponent>(entity);
return;
}
InitializeVisualizer(entity, comp);
}
private void VerifyVisualizerSetup(EntityUid entity, DamageVisualsComponent damageVisComp)
{
if (damageVisComp.Thresholds.Count < 1)
{
Logger.ErrorS(SawmillName, $"Thresholds were invalid for entity {entity}. Thresholds: {damageVisComp.Thresholds}");
damageVisComp.Valid = false;
return;
}
if (damageVisComp.Divisor == 0)
{
Logger.ErrorS(SawmillName, $"Divisor for {entity} is set to zero.");
damageVisComp.Valid = false;
return;
}
if (damageVisComp.Overlay)
{
if (damageVisComp.DamageOverlayGroups == null && damageVisComp.DamageOverlay == null)
{
Logger.ErrorS(SawmillName, $"Enabled overlay without defined damage overlay sprites on {entity}.");
damageVisComp.Valid = false;
return;
}
if (damageVisComp.TrackAllDamage && damageVisComp.DamageOverlay == null)
{
Logger.ErrorS(SawmillName, $"Enabled all damage tracking without a damage overlay sprite on {entity}.");
damageVisComp.Valid = false;
return;
}
if (!damageVisComp.TrackAllDamage && damageVisComp.DamageOverlay != null)
{
Logger.WarningS(SawmillName, $"Disabled all damage tracking with a damage overlay sprite on {entity}.");
damageVisComp.Valid = false;
return;
}
if (damageVisComp.TrackAllDamage && damageVisComp.DamageOverlayGroups != null)
{
Logger.WarningS(SawmillName, $"Enabled all damage tracking with damage overlay groups on {entity}.");
damageVisComp.Valid = false;
return;
}
}
else if (!damageVisComp.Overlay)
{
if (damageVisComp.TargetLayers == null)
{
Logger.ErrorS(SawmillName, $"Disabled overlay without target layers on {entity}.");
damageVisComp.Valid = false;
return;
}
if (damageVisComp.DamageOverlayGroups != null || damageVisComp.DamageOverlay != null)
{
Logger.ErrorS(SawmillName, $"Disabled overlay with defined damage overlay sprites on {entity}.");
damageVisComp.Valid = false;
return;
}
if (damageVisComp.DamageGroup == null)
{
Logger.ErrorS(SawmillName, $"Disabled overlay without defined damage group on {entity}.");
damageVisComp.Valid = false;
return;
}
}
if (damageVisComp.DamageOverlayGroups != null && damageVisComp.DamageGroup != null)
{
Logger.WarningS(SawmillName, $"Damage overlay sprites and damage group are both defined on {entity}.");
}
if (damageVisComp.DamageOverlay != null && damageVisComp.DamageGroup != null)
{
Logger.WarningS(SawmillName, $"Damage overlay sprites and damage group are both defined on {entity}.");
}
}
private void InitializeVisualizer(EntityUid entity, DamageVisualsComponent damageVisComp)
{
if (!TryComp(entity, out SpriteComponent? spriteComponent)
|| !TryComp<DamageableComponent?>(entity, out var damageComponent)
|| !HasComp<AppearanceComponent>(entity))
return;
damageVisComp.Thresholds.Add(FixedPoint2.Zero);
damageVisComp.Thresholds.Sort();
if (damageVisComp.Thresholds[0] != 0)
{
Logger.ErrorS(SawmillName, $"Thresholds were invalid for entity {entity}. Thresholds: {damageVisComp.Thresholds}");
damageVisComp.Valid = false;
return;
}
// If the damage container on our entity's DamageableComponent
// is not null, we can try to check through its groups.
if (damageComponent.DamageContainerID != null
&& _prototypeManager.TryIndex<DamageContainerPrototype>(damageComponent.DamageContainerID, out var damageContainer))
{
// Are we using damage overlay sprites by group?
// Check if the container matches the supported groups,
// and start caching the last threshold.
if (damageVisComp.DamageOverlayGroups != null)
{
foreach (var damageType in damageVisComp.DamageOverlayGroups.Keys)
{
if (!damageContainer.SupportedGroups.Contains(damageType))
{
Logger.ErrorS(SawmillName, $"Damage key {damageType} was invalid for entity {entity}.");
damageVisComp.Valid = false;
return;
}
damageVisComp.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero);
}
}
// Are we tracking a single damage group without overlay instead?
// See if that group is in our entity's damage container.
else if (!damageVisComp.Overlay && damageVisComp.DamageGroup != null)
{
if (!damageContainer.SupportedGroups.Contains(damageVisComp.DamageGroup))
{
Logger.ErrorS(SawmillName, $"Damage keys were invalid for entity {entity}.");
damageVisComp.Valid = false;
return;
}
damageVisComp.LastThresholdPerGroup.Add(damageVisComp.DamageGroup, FixedPoint2.Zero);
}
}
// Ditto above, but instead we go through every group.
else // oh boy! time to enumerate through every single group!
{
var damagePrototypeIdList = _prototypeManager.EnumeratePrototypes<DamageGroupPrototype>()
.Select((p, _) => p.ID)
.ToList();
if (damageVisComp.DamageOverlayGroups != null)
{
foreach (var damageType in damageVisComp.DamageOverlayGroups.Keys)
{
if (!damagePrototypeIdList.Contains(damageType))
{
Logger.ErrorS(SawmillName, $"Damage keys were invalid for entity {entity}.");
damageVisComp.Valid = false;
return;
}
damageVisComp.LastThresholdPerGroup.Add(damageType, FixedPoint2.Zero);
}
}
else if (damageVisComp.DamageGroup != null)
{
if (!damagePrototypeIdList.Contains(damageVisComp.DamageGroup))
{
Logger.ErrorS(SawmillName, $"Damage keys were invalid for entity {entity}.");
damageVisComp.Valid = false;
return;
}
damageVisComp.LastThresholdPerGroup.Add(damageVisComp.DamageGroup, FixedPoint2.Zero);
}
}
// If we're targeting any layers, and the amount of
// layers is greater than zero, we start reserving
// all the layers needed to track damage groups
// on the entity.
if (damageVisComp.TargetLayers is { Count: > 0 })
{
// This should ensure that the layers we're targeting
// are valid for the visualizer's use.
//
// If the layer doesn't have a base state, or
// the layer key just doesn't exist, we skip it.
foreach (var key in damageVisComp.TargetLayers)
{
if (!spriteComponent.LayerMapTryGet(key, out var index)
|| spriteComponent.LayerGetState(index).ToString() == null)
{
Logger.WarningS(SawmillName, $"Layer at key {key} was invalid for entity {entity}.");
continue;
}
damageVisComp.TargetLayerMapKeys.Add(key);
}
// Similar to damage overlay groups, if none of the targeted
// sprite layers could be used, we display an error and
// invalidate the visualizer without crashing.
if (damageVisComp.TargetLayerMapKeys.Count == 0)
{
Logger.ErrorS(SawmillName, $"Target layers were invalid for entity {entity}.");
damageVisComp.Valid = false;
return;
}
// Otherwise, we start reserving layers. Since the filtering
// loop above ensures that all of these layers are not null,
// and have valid state IDs, there should be no issues.
foreach (object layer in damageVisComp.TargetLayerMapKeys)
{
var layerCount = spriteComponent.AllLayers.Count();
var index = spriteComponent.LayerMapGet(layer);
var layerState = spriteComponent.LayerGetState(index).ToString()!;
if (index + 1 != layerCount)
{
index += 1;
}
damageVisComp.LayerMapKeyStates.Add(layer, layerState);
// If we're an overlay, and we're targeting groups,
// we reserve layers per damage group.
if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null)
{
foreach (var (group, sprite) in damageVisComp.DamageOverlayGroups)
{
AddDamageLayerToSprite(spriteComponent,
sprite,
$"{layerState}_{group}_{damageVisComp.Thresholds[1]}",
$"{layer}{group}",
index);
}
damageVisComp.DisabledLayers.Add(layer, false);
}
// If we're not targeting groups, and we're still
// using an overlay, we instead just add a general
// overlay that reflects on how much damage
// was taken.
else if (damageVisComp.DamageOverlay != null)
{
AddDamageLayerToSprite(spriteComponent,
damageVisComp.DamageOverlay,
$"{layerState}_{damageVisComp.Thresholds[1]}",
$"{layer}trackDamage",
index);
damageVisComp.DisabledLayers.Add(layer, false);
}
}
}
// If we're not targeting layers, however,
// we should ensure that we instead
// reserve it as an overlay.
else
{
if (damageVisComp.DamageOverlayGroups != null)
{
foreach (var (group, sprite) in damageVisComp.DamageOverlayGroups)
{
AddDamageLayerToSprite(spriteComponent,
sprite,
$"DamageOverlay_{group}_{damageVisComp.Thresholds[1]}",
$"DamageOverlay{group}");
damageVisComp.TopMostLayerKey = $"DamageOverlay{group}";
}
}
else if (damageVisComp.DamageOverlay != null)
{
AddDamageLayerToSprite(spriteComponent,
damageVisComp.DamageOverlay,
$"DamageOverlay_{damageVisComp.Thresholds[1]}",
"DamageOverlay");
damageVisComp.TopMostLayerKey = $"DamageOverlay";
}
}
}
/// <summary>
/// Adds a damage tracking layer to a given sprite component.
/// </summary>
private void AddDamageLayerToSprite(SpriteComponent spriteComponent, DamageVisualizerSprite sprite, string state, string mapKey, int? index = null)
{
var newLayer = spriteComponent.AddLayer(
new SpriteSpecifier.Rsi(
new ResourcePath(sprite.Sprite), state
), index);
spriteComponent.LayerMapSet(mapKey, newLayer);
if (sprite.Color != null)
spriteComponent.LayerSetColor(newLayer, Color.FromHex(sprite.Color));
spriteComponent.LayerSetVisible(newLayer, false);
}
protected override void OnAppearanceChange(EntityUid uid, DamageVisualsComponent damageVisComp, ref AppearanceChangeEvent args)
{
// how is this still here?
if (!damageVisComp.Valid)
return;
// If this was passed into the component, we update
// the data to ensure that the current disabled
// bool matches.
if (args.Component.TryGetData<bool>(DamageVisualizerKeys.Disabled, out var disabledStatus))
damageVisComp.Disabled = disabledStatus;
if (damageVisComp.Disabled)
return;
HandleDamage(args.Component, damageVisComp);
}
private void HandleDamage(AppearanceComponent component, DamageVisualsComponent damageVisComp)
{
if (!TryComp(component.Owner, out SpriteComponent? spriteComponent)
|| !TryComp(component.Owner, out DamageableComponent? damageComponent))
return;
if (damageVisComp.TargetLayers != null && damageVisComp.DamageOverlayGroups != null)
UpdateDisabledLayers(spriteComponent, component, damageVisComp);
if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null && damageVisComp.TargetLayers == null)
CheckOverlayOrdering(spriteComponent, damageVisComp);
if (component.TryGetData<bool>(DamageVisualizerKeys.ForceUpdate, out var update)
&& update)
{
ForceUpdateLayers(damageComponent, spriteComponent, damageVisComp);
return;
}
if (damageVisComp.TrackAllDamage)
{
UpdateDamageVisuals(damageComponent, spriteComponent, damageVisComp);
}
else if (component.TryGetData(DamageVisualizerKeys.DamageUpdateGroups, out DamageVisualizerGroupData data))
{
UpdateDamageVisuals(data.GroupList, damageComponent, spriteComponent, damageVisComp);
}
}
/// <summary>
/// Checks if any layers were disabled in the last
/// data update. Disabled layers mean that the
/// layer will no longer be visible, or obtain
/// any damage updates.
/// </summary>
private void UpdateDisabledLayers(SpriteComponent spriteComponent, AppearanceComponent component, DamageVisualsComponent damageVisComp)
{
foreach (var layer in damageVisComp.TargetLayerMapKeys)
{
bool? layerStatus = null;
if (component.TryGetData<bool>(layer, out var layerStateEnum))
layerStatus = layerStateEnum;
if (layerStatus == null)
continue;
if (damageVisComp.DisabledLayers[layer] != (bool) layerStatus)
{
damageVisComp.DisabledLayers[layer] = (bool) layerStatus;
if (!damageVisComp.TrackAllDamage && damageVisComp.DamageOverlayGroups != null)
{
foreach (var damageGroup in damageVisComp.DamageOverlayGroups!.Keys)
{
spriteComponent.LayerSetVisible($"{layer}{damageGroup}", damageVisComp.DisabledLayers[layer]);
}
}
else if (damageVisComp.TrackAllDamage)
spriteComponent.LayerSetVisible($"{layer}trackDamage", damageVisComp.DisabledLayers[layer]);
}
}
}
/// <summary>
/// Checks the overlay ordering on the current
/// sprite component, compared to the
/// data for the visualizer. If the top
/// most layer doesn't match, the sprite
/// layers are recreated and placed on top.
/// </summary>
private void CheckOverlayOrdering(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp)
{
if (spriteComponent[damageVisComp.TopMostLayerKey] != spriteComponent[spriteComponent.AllLayers.Count() - 1])
{
if (!damageVisComp.TrackAllDamage && damageVisComp.DamageOverlayGroups != null)
{
foreach (var (damageGroup, sprite) in damageVisComp.DamageOverlayGroups)
{
var threshold = damageVisComp.LastThresholdPerGroup[damageGroup];
ReorderOverlaySprite(spriteComponent,
damageVisComp,
sprite,
$"DamageOverlay{damageGroup}",
$"DamageOverlay_{damageGroup}",
threshold);
}
}
else if (damageVisComp.TrackAllDamage && damageVisComp.DamageOverlay != null)
{
ReorderOverlaySprite(spriteComponent,
damageVisComp,
damageVisComp.DamageOverlay,
$"DamageOverlay",
$"DamageOverlay",
damageVisComp.LastDamageThreshold);
}
}
}
private void ReorderOverlaySprite(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, DamageVisualizerSprite sprite, string key, string statePrefix, FixedPoint2 threshold)
{
spriteComponent.LayerMapTryGet(key, out var spriteLayer);
var visibility = spriteComponent[spriteLayer].Visible;
spriteComponent.RemoveLayer(spriteLayer);
if (threshold == FixedPoint2.Zero) // these should automatically be invisible
threshold = damageVisComp.Thresholds[1];
spriteLayer = spriteComponent.AddLayer(
new SpriteSpecifier.Rsi(
new ResourcePath(sprite.Sprite),
$"{statePrefix}_{threshold}"
),
spriteLayer);
spriteComponent.LayerMapSet(key, spriteLayer);
spriteComponent.LayerSetVisible(spriteLayer, visibility);
// this is somewhat iffy since it constantly reallocates
damageVisComp.TopMostLayerKey = key;
}
/// <summary>
/// Updates damage visuals without tracking
/// any damage groups.
/// </summary>
private void UpdateDamageVisuals(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp)
{
if (!CheckThresholdBoundary(damageComponent.TotalDamage, damageVisComp.LastDamageThreshold, damageVisComp, out var threshold))
return;
damageVisComp.LastDamageThreshold = threshold;
if (damageVisComp.TargetLayers != null)
{
foreach (var layerMapKey in damageVisComp.TargetLayerMapKeys)
{
UpdateTargetLayer(spriteComponent, damageVisComp, layerMapKey, threshold);
}
}
else
{
UpdateOverlay(spriteComponent, threshold);
}
}
/// <summary>
/// Updates damage visuals by damage group,
/// according to the list of damage groups
/// passed into it.
/// </summary>
private void UpdateDamageVisuals(List<string> delta, DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp)
{
foreach (var damageGroup in delta)
{
if (!damageVisComp.Overlay && damageGroup != damageVisComp.DamageGroup)
continue;
if (!_prototypeManager.TryIndex<DamageGroupPrototype>(damageGroup, out var damageGroupPrototype)
|| !damageComponent.Damage.TryGetDamageInGroup(damageGroupPrototype, out var damageTotal))
continue;
if (!damageVisComp.LastThresholdPerGroup.TryGetValue(damageGroup, out var lastThreshold)
|| !CheckThresholdBoundary(damageTotal, lastThreshold, damageVisComp, out var threshold))
continue;
damageVisComp.LastThresholdPerGroup[damageGroup] = threshold;
if (damageVisComp.TargetLayers != null)
{
foreach (var layerMapKey in damageVisComp.TargetLayerMapKeys)
{
UpdateTargetLayer(spriteComponent, damageVisComp, layerMapKey, damageGroup, threshold);
}
}
else
{
UpdateOverlay(spriteComponent, damageVisComp, damageGroup, threshold);
}
}
}
/// <summary>
/// Checks if a threshold boundary was passed.
/// </summary>
private bool CheckThresholdBoundary(FixedPoint2 damageTotal, FixedPoint2 lastThreshold, DamageVisualsComponent damageVisComp, out FixedPoint2 threshold)
{
threshold = FixedPoint2.Zero;
damageTotal = damageTotal / damageVisComp.Divisor;
var thresholdIndex = damageVisComp.Thresholds.BinarySearch(damageTotal);
if (thresholdIndex < 0)
{
thresholdIndex = ~thresholdIndex;
threshold = damageVisComp.Thresholds[thresholdIndex - 1];
}
else
{
threshold = damageVisComp.Thresholds[thresholdIndex];
}
if (threshold == lastThreshold)
return false;
return true;
}
/// <summary>
/// This is the entry point for
/// forcing an update on all damage layers.
/// Does different things depending on
/// the configuration of the visualizer.
/// </summary>
private void ForceUpdateLayers(DamageableComponent damageComponent, SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp)
{
if (damageVisComp.DamageOverlayGroups != null)
{
UpdateDamageVisuals(damageVisComp.DamageOverlayGroups.Keys.ToList(), damageComponent, spriteComponent, damageVisComp);
}
else if (damageVisComp.DamageGroup != null)
{
UpdateDamageVisuals(new List<string>(){ damageVisComp.DamageGroup }, damageComponent, spriteComponent, damageVisComp);
}
else if (damageVisComp.DamageOverlay != null)
{
UpdateDamageVisuals(damageComponent, spriteComponent, damageVisComp);
}
}
/// <summary>
/// Updates a target layer. Without a damage group passed in,
/// it assumes you're updating a layer that is tracking all
/// damage.
/// </summary>
private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, object layerMapKey, FixedPoint2 threshold)
{
if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null)
{
if (!damageVisComp.DisabledLayers[layerMapKey])
{
var layerState = damageVisComp.LayerMapKeyStates[layerMapKey];
spriteComponent.LayerMapTryGet($"{layerMapKey}trackDamage", out var spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"{layerState}",
threshold);
}
}
else if (!damageVisComp.Overlay)
{
var layerState = damageVisComp.LayerMapKeyStates[layerMapKey];
spriteComponent.LayerMapTryGet(layerMapKey, out var spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"{layerState}",
threshold);
}
}
/// <summary>
/// Updates a target layer by damage group.
/// </summary>
private void UpdateTargetLayer(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, object layerMapKey, string damageGroup, FixedPoint2 threshold)
{
if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null)
{
if (damageVisComp.DamageOverlayGroups.ContainsKey(damageGroup) && !damageVisComp.DisabledLayers[layerMapKey])
{
var layerState = damageVisComp.LayerMapKeyStates[layerMapKey];
spriteComponent.LayerMapTryGet($"{layerMapKey}{damageGroup}", out var spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"{layerState}_{damageGroup}",
threshold);
}
}
else if (!damageVisComp.Overlay)
{
var layerState = damageVisComp.LayerMapKeyStates[layerMapKey];
spriteComponent.LayerMapTryGet(layerMapKey, out var spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"{layerState}_{damageGroup}",
threshold);
}
}
/// <summary>
/// Updates an overlay that is tracking all damage.
/// </summary>
private void UpdateOverlay(SpriteComponent spriteComponent, FixedPoint2 threshold)
{
spriteComponent.LayerMapTryGet($"DamageOverlay", out var spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"DamageOverlay",
threshold);
}
/// <summary>
/// Updates an overlay based on damage group.
/// </summary>
private void UpdateOverlay(SpriteComponent spriteComponent, DamageVisualsComponent damageVisComp, string damageGroup, FixedPoint2 threshold)
{
if (damageVisComp.DamageOverlayGroups != null)
{
if (damageVisComp.DamageOverlayGroups.ContainsKey(damageGroup))
{
spriteComponent.LayerMapTryGet($"DamageOverlay{damageGroup}", out var spriteLayer);
UpdateDamageLayerState(spriteComponent,
spriteLayer,
$"DamageOverlay_{damageGroup}",
threshold);
}
}
}
/// <summary>
/// Updates a layer on the sprite by what
/// prefix it has (calculated by whatever
/// function calls it), and what threshold
/// was passed into it.
/// </summary>
private void UpdateDamageLayerState(SpriteComponent spriteComponent, int spriteLayer, string statePrefix, FixedPoint2 threshold)
{
if (threshold == 0)
{
spriteComponent.LayerSetVisible(spriteLayer, false);
}
else
{
if (!spriteComponent[spriteLayer].Visible)
{
spriteComponent.LayerSetVisible(spriteLayer, true);
}
spriteComponent.LayerSetState(spriteLayer, $"{statePrefix}_{threshold}");
}
}
}

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Disposal.UI
{
private DisposalRouterWindow? _window;
public DisposalRouterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public DisposalRouterBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Disposal.UI
{
private DisposalTaggerWindow? _window;
public DisposalTaggerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public DisposalTaggerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -20,7 +20,7 @@ namespace Content.Client.Disposal.UI
public MailingUnitWindow? MailingUnitWindow;
public DisposalUnitWindow? DisposalUnitWindow;
public DisposalUnitBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public DisposalUnitBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -85,7 +85,7 @@ namespace Content.Client.Doors
{
var flickMaintenancePanel = new AnimationTrackSpriteFlick();
CloseAnimation.AnimationTracks.Add(flickMaintenancePanel);
flickMaintenancePanel.LayerKey = WiresVisualizer.WiresVisualLayers.MaintenancePanel;
flickMaintenancePanel.LayerKey = WiresVisualLayers.MaintenancePanel;
flickMaintenancePanel.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame("panel_closing", 0f));
}
}
@@ -109,7 +109,7 @@ namespace Content.Client.Doors
{
var flickMaintenancePanel = new AnimationTrackSpriteFlick();
OpenAnimation.AnimationTracks.Add(flickMaintenancePanel);
flickMaintenancePanel.LayerKey = WiresVisualizer.WiresVisualLayers.MaintenancePanel;
flickMaintenancePanel.LayerKey = WiresVisualLayers.MaintenancePanel;
flickMaintenancePanel.KeyFrames.Add(new AnimationTrackSpriteFlick.KeyFrame("panel_opening", 0f));
}
}

View File

@@ -27,9 +27,9 @@ public sealed class DoorSystem : SharedDoorSystem
}
// TODO AUDIO PREDICT see comments in server-side PlaySound()
protected override void PlaySound(EntityUid uid, string sound, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted)
protected override void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted)
{
if (GameTiming.InPrediction && GameTiming.IsFirstTimePredicted)
SoundSystem.Play(sound, Filter.Local(), uid, audioParams);
Audio.Play(soundSpecifier, Filter.Local(), uid, audioParams);
}
}

View File

@@ -1,11 +1,9 @@
using System;
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.CharacterInterface;
using Content.Client.Chat.Managers;
using Content.Client.EscapeMenu;
using Content.Client.Eui;
using Content.Client.Eye.Blinding;
using Content.Client.Flash;
using Content.Client.GhostKick;
using Content.Client.HUD;
@@ -14,40 +12,33 @@ using Content.Client.Input;
using Content.Client.IoC;
using Content.Client.Launcher;
using Content.Client.MainMenu;
using Content.Client.MobState.Overlays;
using Content.Client.Parallax;
using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Preferences;
using Content.Client.Radiation;
using Content.Client.Sandbox;
using Content.Client.Screenshot;
using Content.Client.Singularity;
using Content.Client.Stylesheets;
using Content.Client.Viewport;
using Content.Client.Voting;
using Content.Shared.Actions;
using Content.Shared.Administration;
using Content.Shared.Alert;
using Content.Shared.AME;
using Content.Shared.Cargo.Components;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Dispenser;
using Content.Shared.Gravity;
using Content.Shared.Lathe;
using Content.Shared.Markers;
using Content.Shared.Research.Components;
using Content.Shared.VendingMachines;
using Content.Shared.Wires;
using Robust.Client;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Client.UserInterface;
#if FULL_RELEASE
using Robust.Shared;
using Robust.Shared.Configuration;
#endif
using Robust.Shared.ContentPack;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -130,6 +121,11 @@ namespace Content.Client.Entry
IoCManager.InjectDependencies(this);
#if FULL_RELEASE
// if FULL_RELEASE, because otherwise this breaks some integration tests.
IoCManager.Resolve<IConfigurationManager>().OverrideDefault(CVars.NetBufferSize, 2);
#endif
_escapeMenuOwner.Initialize();
_baseClient.PlayerJoinedServer += (_, _) =>

View File

@@ -8,7 +8,7 @@
<Label Text="{Loc 'ui-options-net-interp-ratio'}" />
<Control MinSize="8 0" />
<Slider Name="NetInterpRatioSlider"
MinValue="0"
ToolTip="{Loc 'ui-options-net-interp-ratio-tooltip'}"
MaxValue="6"
HorizontalExpand="True"
MinSize="80 0"

View File

@@ -1,15 +1,10 @@
using System;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Maths;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Client.GameStates;
namespace Content.Client.EscapeMenu.UI.Tabs
{
@@ -17,6 +12,7 @@ namespace Content.Client.EscapeMenu.UI.Tabs
public sealed partial class NetworkTab : Control
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClientGameStateManager _stateMan = default!;
public NetworkTab()
{
@@ -26,6 +22,7 @@ namespace Content.Client.EscapeMenu.UI.Tabs
ApplyButton.OnPressed += OnApplyButtonPressed;
ResetButton.OnPressed += OnResetButtonPressed;
NetInterpRatioSlider.OnValueChanged += OnNetInterpRatioSliderChanged;
NetInterpRatioSlider.MinValue = _stateMan.MinBufferSize;
Reset();
}
@@ -45,7 +42,7 @@ namespace Content.Client.EscapeMenu.UI.Tabs
private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
{
_cfg.SetCVar(CVars.NetInterpRatio, (int) NetInterpRatioSlider.Value);
_cfg.SetCVar(CVars.NetBufferSize, (int) NetInterpRatioSlider.Value - _stateMan.MinBufferSize);
_cfg.SaveToFile();
UpdateChanges();
}
@@ -57,13 +54,13 @@ namespace Content.Client.EscapeMenu.UI.Tabs
private void Reset()
{
NetInterpRatioSlider.Value = _cfg.GetCVar(CVars.NetInterpRatio);
NetInterpRatioSlider.Value = _cfg.GetCVar(CVars.NetBufferSize) + _stateMan.MinBufferSize;
UpdateChanges();
}
private void UpdateChanges()
{
var isEverythingSame = NetInterpRatioSlider.Value == _cfg.GetCVar(CVars.NetInterpRatio);
var isEverythingSame = NetInterpRatioSlider.Value == _cfg.GetCVar(CVars.NetBufferSize) + _stateMan.MinBufferSize;
ApplyButton.Disabled = isEverythingSame;
ResetButton.Disabled = isEverythingSame;
NetInterpRatioLabel.Text = NetInterpRatioSlider.Value.ToString();

View File

@@ -14,15 +14,13 @@ namespace Content.Client.Eye.Blinding
public override bool RequestScreenTexture => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private readonly ShaderInstance _blurryVisionXShader;
private readonly ShaderInstance _blurryVisionYShader;
private readonly ShaderInstance _dim;
private BlurryVisionComponent _blurryVisionComponent = default!;
public BlurryVisionOverlay()
{
IoCManager.InjectDependencies(this);
_blurryVisionXShader = _prototypeManager.Index<ShaderPrototype>("BlurryVisionX").InstanceUnique();
_blurryVisionYShader = _prototypeManager.Index<ShaderPrototype>("BlurryVisionY").InstanceUnique();
_dim = _prototypeManager.Index<ShaderPrototype>("Dim").InstanceUnique();
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
@@ -51,18 +49,17 @@ namespace Content.Client.Eye.Blinding
if (ScreenTexture == null)
return;
_blurryVisionXShader?.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_blurryVisionXShader?.SetParameter("BLUR_AMOUNT", (_blurryVisionComponent.Magnitude / 10));
_blurryVisionYShader?.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_blurryVisionYShader?.SetParameter("BLUR_AMOUNT", (_blurryVisionComponent.Magnitude / 10));
var opacity = -(_blurryVisionComponent.Magnitude / 15) + 0.9f;
_dim.SetParameter("DAMAGE_AMOUNT", opacity);
var worldHandle = args.WorldHandle;
var viewport = args.WorldBounds;
worldHandle.UseShader(_dim);
worldHandle.SetTransform(Matrix3.Identity);
worldHandle.UseShader(_blurryVisionXShader);
worldHandle.DrawRect(viewport, Color.White);
worldHandle.UseShader(_blurryVisionYShader);
worldHandle.DrawRect(viewport, Color.White);
worldHandle.DrawRect(viewport, Color.Black);
worldHandle.UseShader(null);
}
}
}

View File

@@ -7,7 +7,7 @@ namespace Content.Client.Forensics
{
private ForensicScannerMenu? _window;
public ForensicScannerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public ForensicScannerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -10,7 +10,7 @@ namespace Content.Client.Gravity.UI
{
private GravityGeneratorWindow? _window;
public GravityGeneratorBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base (owner, uiKey)
public GravityGeneratorBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base (owner, uiKey)
{
}

View File

@@ -10,7 +10,7 @@ namespace Content.Client.HealthAnalyzer.UI
{
private HealthAnalyzerWindow? _window;
public HealthAnalyzerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public HealthAnalyzerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -12,7 +12,7 @@ namespace Content.Client.Instruments.UI
public InstrumentComponent? Instrument { get; set; }
public InstrumentBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public InstrumentBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -16,7 +16,7 @@ namespace Content.Client.Inventory
[ViewVariables]
private StrippingMenu? _strippingMenu;
public StrippableBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public StrippableBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -24,7 +24,7 @@ namespace Content.Client.Kitchen.UI
private readonly Dictionary<int, EntityUid> _solids = new();
private readonly Dictionary<int, Solution.ReagentQuantity> _reagents =new();
public MicrowaveBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner,uiKey)
public MicrowaveBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner,uiKey)
{
}

View File

@@ -16,7 +16,7 @@ namespace Content.Client.Kitchen.UI
private GrinderMenu? _menu;
public ReagentGrinderBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) { }
public ReagentGrinderBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) { }
protected override void Open()
{

View File

@@ -11,7 +11,7 @@ namespace Content.Client.Labels.UI
{
private HandLabelerWindow? _window;
public HandLabelerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public HandLabelerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -247,7 +247,7 @@ namespace Content.Client.LateJoin
string? reason = null;
if (value == 0 || !tracker.IsAllowed(prototype, out reason))
if (!tracker.IsAllowed(prototype, out reason))
{
jobButton.Disabled = true;
@@ -256,6 +256,10 @@ namespace Content.Client.LateJoin
jobButton.ToolTip = reason;
}
}
else if (value == 0)
{
jobButton.Disabled = true;
}
_jobButtons[id][prototype.ID] = jobButton;
}

View File

@@ -2,8 +2,6 @@ using Robust.Client.GameObjects;
using Content.Shared.Lathe;
using Content.Shared.Power;
using Content.Client.Power;
using Content.Client.Wires.Visualizers;
using Content.Shared.Wires;
namespace Content.Client.Lathe
{
@@ -20,12 +18,6 @@ namespace Content.Client.Lathe
args.Sprite.LayerSetVisible(PowerDeviceVisualLayers.Powered, powered);
}
if (args.Component.TryGetData(WiresVisuals.MaintenancePanelState, out bool panel)
&& args.Sprite.LayerMapTryGet(WiresVisualizer.WiresVisualLayers.MaintenancePanel, out _))
{
args.Sprite.LayerSetVisible(WiresVisualizer.WiresVisualLayers.MaintenancePanel, panel);
}
// Lathe specific stuff
if (args.Component.TryGetData(LatheVisuals.IsRunning, out bool isRunning))
{

View File

@@ -29,7 +29,7 @@ namespace Content.Client.Lathe.UI
public Queue<LatheRecipePrototype> QueuedRecipes => _queuedRecipes;
private readonly Queue<LatheRecipePrototype> _queuedRecipes = new();
public LatheBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public LatheBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
SendMessage(new LatheSyncRequestMessage());
}

View File

@@ -12,7 +12,7 @@ namespace Content.Client.MachineLinking.UI
private string? _selectedTransmitterPort;
private string? _selectedReceiverPort;
public SignalPortSelectorBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] object uiKey) : base(owner, uiKey) { }
public SignalPortSelectorBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] Enum uiKey) : base(owner, uiKey) { }
protected override void Open()
{

View File

@@ -9,7 +9,7 @@ namespace Content.Client.Medical.CrewMonitoring
{
private CrewMonitoringWindow? _menu;
public CrewMonitoringBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] object uiKey) : base(owner, uiKey)
public CrewMonitoringBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -9,7 +9,7 @@ public sealed class NetworkConfiguratorBoundUserInterface : BoundUserInterface
private NetworkConfiguratorListMenu? _listMenu;
private NetworkConfiguratorConfigurationMenu? _configurationMenu;
public NetworkConfiguratorBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public NetworkConfiguratorBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -10,7 +10,7 @@ namespace Content.Client.Nuke
{
private NukeMenu? _menu;
public NukeBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] object uiKey) : base(owner, uiKey)
public NukeBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -16,7 +16,7 @@ namespace Content.Client.PDA
private PDAMenu? _menu;
public PDABoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public PDABoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}

View File

@@ -9,7 +9,7 @@ namespace Content.Client.PDA.Ringer
{
private RingtoneMenu? _menu;
public RingerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public RingerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -12,7 +12,7 @@ namespace Content.Client.Paper.UI
{
private PaperWindow? _window;
public PaperBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public PaperBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -8,7 +8,7 @@ namespace Content.Client.ParticleAccelerator.UI
{
private ParticleAcceleratorControlMenu? _menu;
public ParticleAcceleratorBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public ParticleAcceleratorBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -21,7 +21,7 @@ namespace Content.Client.Power.APC
_menu.OpenCentered();
}
public ApcBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public ApcBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -80,6 +80,6 @@ public sealed partial class PowerMonitoringWindow : DefaultWindow, IComputerWind
[UsedImplicitly]
public sealed class PowerMonitoringConsoleBoundUserInterface : ComputerBoundUserInterface<PowerMonitoringWindow, PowerMonitoringConsoleBoundInterfaceState>
{
public PowerMonitoringConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
public PowerMonitoringConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {}
}

View File

@@ -161,6 +161,6 @@ namespace Content.Client.Power
[UsedImplicitly]
public sealed class SolarControlConsoleBoundUserInterface : ComputerBoundUserInterface<SolarControlWindow, SolarControlConsoleBoundInterfaceState>
{
public SolarControlConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
public SolarControlConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {}
}
}

View File

@@ -7,7 +7,7 @@ namespace Content.Client.Research.UI
{
private ResearchClientServerSelectionMenu? _menu;
public ResearchClientBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public ResearchClientBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
SendMessage(new ResearchClientSyncMessage());
}

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Research.UI
private ResearchConsoleMenu? _consoleMenu;
private TechnologyDatabaseComponent? _technologyDatabase;
public ResearchConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public ResearchConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
SendMessage(new ConsoleServerSyncMessage());
}

View File

@@ -1,56 +0,0 @@
using Content.Client.Traitor.Uplink;
using Content.Shared.Revenant;
using Content.Shared.Traitor.Uplink;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Revenant.Ui;
[UsedImplicitly]
public sealed class RevenantBoundUserInterface : BoundUserInterface
{
private RevenantMenu? _menu;
public RevenantBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
_menu = new();
_menu.OpenCentered();
_menu.OnClose += Close;
_menu.OnListingButtonPressed += (_, listing) =>
{
SendMessage(new RevenantBuyListingMessage(listing));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_menu == null)
return;
switch (state)
{
case RevenantUpdateState msg:
_menu.UpdateEssence(msg.Essence);
_menu.UpdateListing(msg.Listings);
break;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_menu?.Close();
_menu?.Dispose();
}
}

View File

@@ -1,26 +0,0 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Maths;
namespace Content.Client.Revenant.Ui;
[GenerateTypedNameReferences]
public sealed partial class RevenantListingControl : Control
{
public RevenantListingControl(string itemName, string itemDescription,
int itemPrice, bool canBuy, Texture? texture = null)
{
RobustXamlLoader.Load(this);
RevenantItemName.Text = itemName;
RevenantItemDescription.SetMessage(itemDescription);
RevenantItemBuyButton.Text = Loc.GetString("revenant-user-interface-cost", ("price", itemPrice));
RevenantItemBuyButton.Disabled = !canBuy;
RevenantItemTexture.Texture = texture;
}
}

View File

@@ -1,62 +0,0 @@
using Content.Client.Message;
using Content.Shared.FixedPoint;
using Content.Shared.Revenant;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
namespace Content.Client.Revenant.Ui;
[GenerateTypedNameReferences]
public sealed partial class RevenantMenu : DefaultWindow
{
private FixedPoint2 _essence = 0f;
public event Action<BaseButton.ButtonEventArgs, RevenantStoreListingPrototype>? OnListingButtonPressed;
public RevenantMenu()
{
RobustXamlLoader.Load(this);
}
public void UpdateEssence(float essence)
{
// update balance label
_essence = essence;
var balanceStr = Loc.GetString("revenant-user-interface-essence-amount", ("amount", Math.Round(_essence.Float())));
BalanceInfo.SetMarkup(balanceStr);
}
public void UpdateListing(List<RevenantStoreListingPrototype> listings)
{
// should probably chunk these out instead. to-do if this clogs the internet tubes.
// maybe read clients prototypes instead?
ClearListings();
foreach (var item in listings)
{
AddListingGui(item);
}
}
private void AddListingGui(RevenantStoreListingPrototype listing)
{
var listingName = listing.ListingName;
var listingDesc = listing.Description;
var listingPrice = listing.Price;
var canBuy = _essence > listing.Price;
var texture = listing.Icon?.Frame0();
var newListing = new RevenantListingControl(listingName, listingDesc, listingPrice, canBuy, texture);
newListing.RevenantItemBuyButton.OnButtonDown += args
=> OnListingButtonPressed?.Invoke(args, listing);
RevenantListingsContainer.AddChild(newListing);
}
private void ClearListings()
{
RevenantListingsContainer.Children.Clear();
}
}

View File

@@ -9,5 +9,5 @@ namespace Content.Client.Shuttles.BUI;
[UsedImplicitly]
public sealed class EmergencyConsoleBoundUserInterface : ComputerBoundUserInterface<EmergencyConsoleWindow, EmergencyConsoleBoundUserInterfaceState>
{
public EmergencyConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
public EmergencyConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {}
}

View File

@@ -11,7 +11,7 @@ public sealed class IFFConsoleBoundUserInterface : BoundUserInterface
{
private IFFConsoleWindow? _window;
public IFFConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public IFFConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -10,7 +10,7 @@ public sealed class RadarConsoleBoundUserInterface : BoundUserInterface
{
private RadarConsoleWindow? _window;
public RadarConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
public RadarConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {}
protected override void Open()
{

View File

@@ -12,7 +12,7 @@ public sealed class ShuttleConsoleBoundUserInterface : BoundUserInterface
{
private ShuttleConsoleWindow? _window;
public ShuttleConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
public ShuttleConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey) {}
protected override void Open()
{

View File

@@ -7,7 +7,7 @@ public sealed class GeneralStationRecordConsoleBoundUserInterface : BoundUserInt
{
private GeneralStationRecordConsoleWindow? _window = default!;
public GeneralStationRecordConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public GeneralStationRecordConsoleBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{}
protected override void Open()

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Storage
{
[ViewVariables] private StorageWindow? _window;
public StorageBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public StorageBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -133,7 +133,7 @@ namespace Content.Client.Storage.UI
new Label
{
Align = Label.AlignMode.Right,
Text = item?.Size.ToString() ?? Loc.GetString("no-item-size")
Text = item?.Size.ToString() ?? Loc.GetString("comp-storage-no-item-size"),
}
}
});

View File

@@ -0,0 +1,79 @@
using Content.Shared.Store;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using System.Linq;
namespace Content.Client.Store.Ui;
[UsedImplicitly]
public sealed class StoreBoundUserInterface : BoundUserInterface
{
private StoreMenu? _menu;
private string _windowName = Loc.GetString("store-ui-default-title");
public StoreBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
_menu = new StoreMenu(_windowName);
_menu.OpenCentered();
_menu.OnClose += Close;
_menu.OnListingButtonPressed += (_, listing) =>
{
if (_menu.CurrentBuyer != null)
SendMessage(new StoreBuyListingMessage(_menu.CurrentBuyer.Value, listing));
};
_menu.OnCategoryButtonPressed += (_, category) =>
{
_menu.CurrentCategory = category;
if (_menu.CurrentBuyer != null)
SendMessage(new StoreRequestUpdateInterfaceMessage(_menu.CurrentBuyer.Value));
};
_menu.OnWithdrawAttempt += (_, type, amount) =>
{
if (_menu.CurrentBuyer != null)
SendMessage(new StoreRequestWithdrawMessage(_menu.CurrentBuyer.Value, type, amount));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_menu == null)
return;
switch (state)
{
case StoreUpdateState msg:
if (msg.Buyer != null)
_menu.CurrentBuyer = msg.Buyer;
_menu.UpdateBalance(msg.Balance);
_menu.PopulateStoreCategoryButtons(msg.Listings);
_menu.UpdateListing(msg.Listings.ToList());
break;
case StoreInitializeState msg:
_windowName = msg.Name;
if (_menu != null && _menu.Window != null)
_menu.Window.Title = msg.Name;
break;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_menu?.Close();
_menu?.Dispose();
}
}

View File

@@ -1,9 +1,9 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Margin="8,8,8,8" Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Name="RevenantItemName" HorizontalExpand="True" />
<Label Name="StoreItemName" HorizontalExpand="True" />
<Button
Name="RevenantItemBuyButton"
Name="StoreItemBuyButton"
MinWidth="64"
HorizontalAlignment="Right"
Access="Public" />
@@ -11,11 +11,11 @@
<PanelContainer StyleClasses="HighDivider" />
<BoxContainer HorizontalExpand="True" Orientation="Horizontal">
<TextureRect
Name="RevenantItemTexture"
Name="StoreItemTexture"
Margin="0,0,4,0"
MinSize="48 48"
Stretch="KeepAspectCentered" />
<RichTextLabel Name="RevenantItemDescription" />
<RichTextLabel Name="StoreItemDescription" />
</BoxContainer>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,24 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Store.Ui;
[GenerateTypedNameReferences]
public sealed partial class StoreListingControl : Control
{
public StoreListingControl(string itemName, string itemDescription,
string price, bool canBuy, Texture? texture = null)
{
RobustXamlLoader.Load(this);
StoreItemName.Text = itemName;
StoreItemDescription.SetMessage(itemDescription);
StoreItemBuyButton.Text = price;
StoreItemBuyButton.Disabled = !canBuy;
StoreItemTexture.Texture = texture;
}
}

View File

@@ -1,7 +1,7 @@
<DefaultWindow
xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'revenant-user-interface-title'}"
Title="{Loc 'store-ui-default-title'}"
MinSize="512 512"
SetSize="512 512">
<BoxContainer Orientation="Vertical">
@@ -12,21 +12,34 @@
HorizontalAlignment="Left"
Access="Public"
HorizontalExpand="True" />
<Button
Name="WithdrawButton"
MinWidth="64"
HorizontalAlignment="Right"
Text="{Loc 'store-ui-default-withdraw-text'}" />
</BoxContainer>
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#80808005" />
</PanelContainer.PanelOverride>
<BoxContainer Name="CategoryListContainer" Orientation="Vertical">
<!-- Category buttons are added here by code -->
</BoxContainer>
</PanelContainer>
<ScrollContainer
Name="RevenantListingsScroll"
Name="StoreListingsScroll"
HScrollEnabled="False"
HorizontalExpand="True"
MinSize="100 256"
SizeFlagsStretchRatio="2"
VerticalExpand="True">
<BoxContainer
Name="RevenantListingsContainer"
Name="StoreListingsContainer"
MinSize="100 256"
Orientation="Vertical"
SizeFlagsStretchRatio="2"

View File

@@ -0,0 +1,222 @@
using Content.Client.Message;
using Content.Shared.Store;
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.Client.Graphics;
using Content.Shared.Actions.ActionTypes;
using System.Linq;
using Content.Shared.FixedPoint;
namespace Content.Client.Store.Ui;
[GenerateTypedNameReferences]
public sealed partial class StoreMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private StoreWithdrawWindow? _withdrawWindow;
public event Action<BaseButton.ButtonEventArgs, ListingData>? OnListingButtonPressed;
public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
public EntityUid? CurrentBuyer;
public Dictionary<string, FixedPoint2> Balance = new();
public string CurrentCategory = string.Empty;
public StoreMenu(string name)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
if (Window != null)
Window.Title = name;
}
public void UpdateBalance(Dictionary<string, FixedPoint2> balance)
{
Balance = balance;
var currency = new Dictionary<(string, FixedPoint2), CurrencyPrototype>();
foreach (var type in balance)
{
currency.Add((type.Key, type.Value), _prototypeManager.Index<CurrencyPrototype>(type.Key));
}
var balanceStr = string.Empty;
foreach (var type in currency)
{
balanceStr += $"{Loc.GetString(type.Value.BalanceDisplay, ("amount", type.Key.Item2))}\n";
}
BalanceInfo.SetMarkup(balanceStr.TrimEnd());
var disabled = true;
foreach (var type in currency)
{
if (type.Value.CanWithdraw && type.Value.EntityId != null && type.Key.Item2 > 0)
disabled = false;
}
WithdrawButton.Disabled = disabled;
}
public void UpdateListing(List<ListingData> listings)
{
var sorted = listings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum());
// should probably chunk these out instead. to-do if this clogs the internet tubes.
// maybe read clients prototypes instead?
ClearListings();
foreach (var item in sorted)
{
AddListingGui(item);
}
}
private void OnWithdrawButtonDown(BaseButton.ButtonEventArgs args)
{
// check if window is already open
if (_withdrawWindow != null && _withdrawWindow.IsOpen)
{
_withdrawWindow.MoveToFront();
return;
}
// open a new one
_withdrawWindow = new StoreWithdrawWindow();
_withdrawWindow.OpenCentered();
_withdrawWindow.CreateCurrencyButtons(Balance);
_withdrawWindow.OnWithdrawAttempt += OnWithdrawAttempt;
}
private void AddListingGui(ListingData listing)
{
if (!listing.Categories.Contains(CurrentCategory))
return;
string listingName = new (listing.Name);
string listingDesc = new (listing.Description);
var listingPrice = listing.Cost;
var canBuy = CanBuyListing(Balance, listingPrice);
var spriteSys = _entityManager.EntitySysManager.GetEntitySystem<SpriteSystem>();
Texture? texture = null;
if (listing.Icon != null)
texture = spriteSys.Frame0(listing.Icon);
if (listing.ProductEntity != null)
{
if (texture == null)
texture = spriteSys.GetPrototypeIcon(listing.ProductEntity).Default;
var proto = _prototypeManager.Index<EntityPrototype>(listing.ProductEntity);
if (listingName == string.Empty)
listingName = proto.Name;
if (listingDesc == string.Empty)
listingDesc = proto.Description;
}
else if (listing.ProductAction != null)
{
var action = _prototypeManager.Index<InstantActionPrototype>(listing.ProductAction);
if (action.Icon != null)
texture = spriteSys.Frame0(action.Icon);
}
var newListing = new StoreListingControl(listingName, listingDesc, GetListingPriceString(listing), canBuy, texture);
newListing.StoreItemBuyButton.OnButtonDown += args
=> OnListingButtonPressed?.Invoke(args, listing);
StoreListingsContainer.AddChild(newListing);
}
public bool CanBuyListing(Dictionary<string, FixedPoint2> currency, Dictionary<string, FixedPoint2> price)
{
foreach (var type in price)
{
if (!currency.ContainsKey(type.Key))
return false;
if (currency[type.Key] < type.Value)
return false;
}
return true;
}
public string GetListingPriceString(ListingData listing)
{
var text = string.Empty;
foreach (var type in listing.Cost)
{
var currency = _prototypeManager.Index<CurrencyPrototype>(type.Key);
text += $"{Loc.GetString(currency.PriceDisplay, ("amount", type.Value))}\n";
}
if (listing.Cost.Count < 1)
text = Loc.GetString("store-currency-free");
return text.TrimEnd();
}
private void ClearListings()
{
StoreListingsContainer.Children.Clear();
}
public void PopulateStoreCategoryButtons(HashSet<ListingData> listings)
{
var allCategories = new List<StoreCategoryPrototype>();
foreach (var listing in listings)
{
foreach (var cat in listing.Categories)
{
var proto = _prototypeManager.Index<StoreCategoryPrototype>(cat);
if (!allCategories.Contains(proto))
allCategories.Add(proto);
}
}
allCategories = allCategories.OrderBy(c => c.Priority).ToList();
if (CurrentCategory == string.Empty && allCategories.Count > 0)
CurrentCategory = allCategories.First().ID;
if (allCategories.Count <= 1)
return;
CategoryListContainer.Children.Clear();
foreach (var proto in allCategories)
{
var catButton = new StoreCategoryButton
{
Text = Loc.GetString(proto.Name),
Id = proto.ID
};
catButton.OnPressed += args => OnCategoryButtonPressed?.Invoke(args, catButton.Id);
CategoryListContainer.AddChild(catButton);
}
}
public override void Close()
{
base.Close();
CurrentBuyer = null;
_withdrawWindow?.Close();
}
private sealed class StoreCategoryButton : Button
{
public string? Id;
}
}

View File

@@ -0,0 +1,16 @@
<DefaultWindow
xmlns="https://spacestation14.io"
Title="{Loc 'store-ui-default-withdraw-text'}"
MinSize="256 128">
<BoxContainer
HorizontalExpand="True"
Orientation="Vertical"
VerticalExpand="True">
<SliderIntInput Name="WithdrawSlider" HorizontalExpand="True" />
<BoxContainer
Name="ButtonContainer"
VerticalAlignment="Bottom"
Orientation="Vertical"
VerticalExpand="True" />
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,102 @@
using System.Linq;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
using Robust.Client.UserInterface;
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.Client.Graphics;
using Content.Shared.Actions.ActionTypes;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
namespace Content.Client.Store.Ui;
/// <summary>
/// Window to select amount TC to withdraw from Uplink account
/// Used as sub-window in Uplink UI
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class StoreWithdrawWindow : DefaultWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private Dictionary<FixedPoint2, CurrencyPrototype> _validCurrencies = new();
private HashSet<CurrencyWithdrawButton> _buttons = new();
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
public StoreWithdrawWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void CreateCurrencyButtons(Dictionary<string, FixedPoint2> balance)
{
_validCurrencies.Clear();
foreach (var currency in balance)
{
if (!_prototypeManager.TryIndex<CurrencyPrototype>(currency.Key, out var proto))
continue;
_validCurrencies.Add(currency.Value, proto);
}
//this shouldn't ever happen but w/e
if (_validCurrencies.Count < 1)
return;
ButtonContainer.Children.Clear();
_buttons.Clear();
foreach (var currency in _validCurrencies)
{
Logger.Debug((currency.Value.PriceDisplay));
var button = new CurrencyWithdrawButton()
{
Id = currency.Value.ID,
Amount = currency.Key,
MinHeight = 20,
Text = Loc.GetString("store-withdraw-button-ui", ("currency",Loc.GetString(currency.Value.PriceDisplay))),
};
button.Disabled = false;
button.OnPressed += args =>
{
OnWithdrawAttempt?.Invoke(args, button.Id, WithdrawSlider.Value);
Close();
};
_buttons.Add(button);
ButtonContainer.AddChild(button);
}
var maxWithdrawAmount = _validCurrencies.Keys.Max().Int();
// setup withdraw slider
WithdrawSlider.MinValue = 1;
WithdrawSlider.MaxValue = maxWithdrawAmount;
WithdrawSlider.OnValueChanged += OnValueChanged;
OnValueChanged(WithdrawSlider.Value);
}
public void OnValueChanged(int i)
{
foreach (var button in _buttons)
{
button.Disabled = button.Amount < WithdrawSlider.Value;
}
}
private sealed class CurrencyWithdrawButton : Button
{
public string? Id;
public FixedPoint2 Amount = FixedPoint2.Zero;
}
}

View File

@@ -13,7 +13,7 @@ public sealed class SurveillanceCameraMonitorBoundUserInterface : BoundUserInter
private SurveillanceCameraMonitorWindow? _window;
private EntityUid? _currentCamera;
public SurveillanceCameraMonitorBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public SurveillanceCameraMonitorBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
_eyeLerpingSystem = _entityManager.EntitySysManager.GetEntitySystem<EyeLerpingSystem>();

View File

@@ -8,7 +8,7 @@ public sealed class SurveillanceCameraSetupBoundUi : BoundUserInterface
private SurveillanceCameraSetupWindow? _window;
private SurveillanceCameraSetupUiKey _type;
public SurveillanceCameraSetupBoundUi(ClientUserInterfaceComponent component, object uiKey) : base(component, uiKey)
public SurveillanceCameraSetupBoundUi(ClientUserInterfaceComponent component, Enum uiKey) : base(component, uiKey)
{
if (uiKey is not SurveillanceCameraSetupUiKey key)
{

View File

@@ -1,72 +1,50 @@
using Content.Client.Items.Components;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Tools.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Timing;
using Robust.Shared.ViewVariables;
namespace Content.Client.Tools.Components
{
[RegisterComponent]
public sealed class MultipleToolComponent : SharedMultipleToolComponent, IItemStatus
[ComponentReference(typeof(SharedMultipleToolComponent))]
public sealed class MultipleToolComponent : SharedMultipleToolComponent
{
private string? _behavior;
[ViewVariables(VVAccess.ReadWrite)]
public bool UiUpdateNeeded;
[DataField("statusShowBehavior")]
private bool _statusShowBehavior = true;
[ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
[ViewVariables] public bool StatusShowBehavior => _statusShowBehavior;
[ViewVariables] public string? Behavior => _behavior;
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not MultipleToolComponentState tool) return;
_behavior = tool.QualityName;
_uiUpdateNeeded = true;
public bool StatusShowBehavior = true;
}
public Control MakeControl() => new StatusControl(this);
private sealed class StatusControl : Control
public sealed class MultipleToolStatusControl : Control
{
private readonly MultipleToolComponent _parent;
private readonly RichTextLabel _label;
public StatusControl(MultipleToolComponent parent)
public MultipleToolStatusControl(MultipleToolComponent parent)
{
_parent = parent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
_label.SetMarkup(_parent.StatusShowBehavior ? _parent.CurrentQualityName : string.Empty);
AddChild(_label);
UpdateDraw();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_parent._uiUpdateNeeded)
if (_parent.UiUpdateNeeded)
{
return;
}
_parent.UiUpdateNeeded = false;
Update();
}
}
public void Update()
{
_parent._uiUpdateNeeded = false;
_label.SetMarkup(_parent.StatusShowBehavior ? _parent.Behavior ?? string.Empty : string.Empty);
}
_label.SetMarkup(_parent.StatusShowBehavior ? _parent.CurrentQualityName : string.Empty);
}
}
}

View File

@@ -1,17 +1,48 @@
using Content.Client.Items;
using Content.Client.Tools.Components;
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Robust.Shared.GameObjects;
using Robust.Client.GameObjects;
using Robust.Shared.GameStates;
namespace Content.Client.Tools
{
public sealed class ToolSystem : EntitySystem
public sealed class ToolSystem : SharedToolSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WelderComponent, ComponentHandleState>(OnWelderHandleState);
SubscribeLocalEvent<MultipleToolComponent, ItemStatusCollectMessage>(OnGetStatusMessage);
}
public override void SetMultipleTool(EntityUid uid,
SharedMultipleToolComponent? multiple = null,
ToolComponent? tool = null,
bool playSound = false,
EntityUid? user = null)
{
if (!Resolve(uid, ref multiple))
return;
base.SetMultipleTool(uid, multiple, tool, playSound, user);
((MultipleToolComponent)multiple).UiUpdateNeeded = true;
// TODO replace this with appearance + visualizer
// in order to convert this to a specifier, the manner in which the sprite is specified in yaml needs to be updated.
if (multiple.Entries.Length > multiple.CurrentEntry && TryComp(uid, out SpriteComponent? sprite))
{
var current = multiple.Entries[multiple.CurrentEntry];
if (current.Sprite != null)
sprite.LayerSetSprite(0, current.Sprite);
}
}
private void OnGetStatusMessage(EntityUid uid, MultipleToolComponent welder, ItemStatusCollectMessage args)
{
args.Controls.Add(new MultipleToolStatusControl(welder));
}
private void OnWelderHandleState(EntityUid uid, WelderComponent welder, ref ComponentHandleState args)

View File

@@ -1,65 +0,0 @@
using Content.Shared.Traitor.Uplink;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.Traitor.Uplink
{
[UsedImplicitly]
public sealed class UplinkBoundUserInterface : BoundUserInterface
{
private UplinkMenu? _menu;
public UplinkBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
_menu = new UplinkMenu();
_menu.OpenCentered();
_menu.OnClose += Close;
_menu.OnListingButtonPressed += (_, listing) =>
{
SendMessage(new UplinkBuyListingMessage(listing.ItemId));
};
_menu.OnCategoryButtonPressed += (_, category) =>
{
_menu.CurrentFilterCategory = category;
SendMessage(new UplinkRequestUpdateInterfaceMessage());
};
_menu.OnWithdrawAttempt += (tc) =>
{
SendMessage(new UplinkTryWithdrawTC(tc));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_menu == null)
return;
switch (state)
{
case UplinkUpdateState msg:
_menu.UpdateAccount(msg.Account);
_menu.UpdateListing(msg.Listings);
break;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_menu?.Close();
_menu?.Dispose();
}
}
}

View File

@@ -1,22 +0,0 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Orientation="Vertical"
Margin="8 8 8 8">
<BoxContainer Orientation="Horizontal">
<Label Name="UplinkItemName"
HorizontalExpand="True"/>
<Button Name="UplinkItemBuyButton"
Access="Public"
HorizontalAlignment="Right"
MinWidth="64"/>
</BoxContainer>
<PanelContainer StyleClasses="HighDivider" />
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True">
<TextureRect Name="UplinkItemTexture"
MinSize="48 48"
Margin="0 0 4 0"
Stretch="KeepAspectCentered"/>
<RichTextLabel Name="UplinkItemDescription" />
</BoxContainer>
</BoxContainer>
</Control>

View File

@@ -1,28 +0,0 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Maths;
namespace Content.Client.Traitor.Uplink
{
[GenerateTypedNameReferences]
public sealed partial class UplinkListingControl : Control
{
public UplinkListingControl(string itemName, string itemDescription,
int itemPrice, bool canBuy, Texture? texture = null)
{
RobustXamlLoader.Load(this);
UplinkItemName.Text = itemName;
UplinkItemDescription.SetMessage(itemDescription);
UplinkItemBuyButton.Text = $"{itemPrice} TC";
UplinkItemBuyButton.Disabled = !canBuy;
UplinkItemTexture.Texture = texture;
}
}
}

View File

@@ -1,53 +0,0 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'uplink-user-interface-title'}"
MinSize="512 512"
SetSize="512 512">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical"
VerticalExpand="True">
<BoxContainer Orientation="Horizontal"
Margin="4 4 4 4">
<RichTextLabel Name="BalanceInfo"
Access="Public"
HorizontalExpand="True"
HorizontalAlignment="Left" />
<Button Name="WithdrawButton"
Text="{Loc 'uplink-user-interface-withdraw-button'}"
HorizontalAlignment="Right"
MinWidth="64"/>
</BoxContainer>
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000FF" />
</PanelContainer.PanelOverride>
<BoxContainer Orientation="Horizontal"
VerticalExpand="True">
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#80808005" />
</PanelContainer.PanelOverride>
<BoxContainer Name="CategoryListContainer"
Orientation="Vertical">
<!-- Category buttons are added here by code -->
</BoxContainer>
</PanelContainer>
<ScrollContainer Name="UplinkListingsScroll"
HorizontalExpand="True"
VerticalExpand="True"
SizeFlagsStretchRatio="2"
HScrollEnabled="False"
MinSize="100 256">
<BoxContainer Name="UplinkListingsContainer"
Orientation="Vertical"
VerticalExpand="True"
SizeFlagsStretchRatio="2"
MinSize="100 256">
<!-- Listings are added here by code -->
</BoxContainer>
</ScrollContainer>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -1,163 +0,0 @@
using Content.Client.Message;
using Content.Shared.PDA;
using Content.Shared.Traitor.Uplink;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Prototypes;
namespace Content.Client.Traitor.Uplink
{
[GenerateTypedNameReferences]
public sealed partial class UplinkMenu : DefaultWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
private UplinkWithdrawWindow? _withdrawWindow;
public event Action<BaseButton.ButtonEventArgs, UplinkListingData>? OnListingButtonPressed;
public event Action<BaseButton.ButtonEventArgs, UplinkCategory>? OnCategoryButtonPressed;
public event Action<int>? OnWithdrawAttempt;
private UplinkCategory _currentFilter;
private UplinkAccountData? _loggedInUplinkAccount;
public UplinkMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
PopulateUplinkCategoryButtons();
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
}
public UplinkCategory CurrentFilterCategory
{
get => _currentFilter;
set
{
if (value.GetType() != typeof(UplinkCategory))
{
return;
}
_currentFilter = value;
}
}
public void UpdateAccount(UplinkAccountData account)
{
_loggedInUplinkAccount = account;
// update balance label
var balance = account.DataBalance;
var weightedColor = balance switch
{
<= 0 => "gray",
<= 5 => "green",
<= 20 => "yellow",
<= 50 => "purple",
_ => "gray"
};
var balanceStr = Loc.GetString("uplink-bound-user-interface-tc-balance-popup",
("weightedColor", weightedColor),
("balance", balance));
BalanceInfo.SetMarkup(balanceStr);
// you can't withdraw if you don't have TC
WithdrawButton.Disabled = balance <= 0;
}
public void UpdateListing(UplinkListingData[] listings)
{
// should probably chunk these out instead. to-do if this clogs the internet tubes.
// maybe read clients prototypes instead?
ClearListings();
foreach (var item in listings)
{
AddListingGui(item);
}
}
private void OnWithdrawButtonDown(BaseButton.ButtonEventArgs args)
{
if (_loggedInUplinkAccount == null)
return;
// check if window is already open
if (_withdrawWindow != null && _withdrawWindow.IsOpen)
{
_withdrawWindow.MoveToFront();
return;
}
// open a new one
_withdrawWindow = new UplinkWithdrawWindow(_loggedInUplinkAccount.DataBalance);
_withdrawWindow.OpenCentered();
_withdrawWindow.OnWithdrawAttempt += OnWithdrawAttempt;
}
private void AddListingGui(UplinkListingData listing)
{
if (!_prototypeManager.TryIndex(listing.ItemId, out EntityPrototype? prototype) || listing.Category != CurrentFilterCategory)
{
return;
}
var listingName = listing.ListingName == string.Empty ? prototype.Name : listing.ListingName;
var listingDesc = listing.Description == string.Empty ? prototype.Description : listing.Description;
var listingPrice = listing.Price;
var canBuy = _loggedInUplinkAccount?.DataBalance >= listing.Price;
var texture = listing.Icon?.Frame0();
if (texture == null)
texture = SpriteComponent.GetPrototypeIcon(prototype, _resourceCache).Default;
var newListing = new UplinkListingControl(listingName, listingDesc, listingPrice, canBuy, texture);
newListing.UplinkItemBuyButton.OnButtonDown += args
=> OnListingButtonPressed?.Invoke(args, listing);
UplinkListingsContainer.AddChild(newListing);
}
private void ClearListings()
{
UplinkListingsContainer.Children.Clear();
}
private void PopulateUplinkCategoryButtons()
{
foreach (UplinkCategory cat in Enum.GetValues(typeof(UplinkCategory)))
{
var catButton = new PDAUplinkCategoryButton
{
Text = Loc.GetString(cat.ToString()),
ButtonCategory = cat
};
//It'd be neat if it could play a cool tech ping sound when you switch categories,
//but right now there doesn't seem to be an easy way to do client-side audio without still having to round trip to the server and
//send to a specific client INetChannel.
catButton.OnPressed += args => OnCategoryButtonPressed?.Invoke(args, catButton.ButtonCategory);
CategoryListContainer.AddChild(catButton);
}
}
public override void Close()
{
base.Close();
_withdrawWindow?.Close();
}
private sealed class PDAUplinkCategoryButton : Button
{
public UplinkCategory ButtonCategory;
}
}
}

View File

@@ -1,22 +0,0 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'uplink-user-interface-withdraw-title'}"
MinSize="256 128">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<SliderIntInput Name="WithdrawSlider"
HorizontalExpand="True"/>
<BoxContainer Orientation="Horizontal"
VerticalExpand="True"
VerticalAlignment="Bottom">
<Button Name="ApplyButton"
Text="{Loc 'uplink-user-interface-withdraw-withdraw-button'}"
HorizontalAlignment="Left"
HorizontalExpand="True"/>
<Button Name="CancelButton"
Text="{Loc 'uplink-user-interface-withdraw-cancel-button'}"
HorizontalAlignment="Right"
HorizontalExpand="True"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -1,34 +0,0 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Localization;
namespace Content.Client.Traitor.Uplink
{
/// <summary>
/// Window to select amount TC to withdraw from Uplink account
/// Used as sub-window in Uplink UI
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class UplinkWithdrawWindow : DefaultWindow
{
public event System.Action<int>? OnWithdrawAttempt;
public UplinkWithdrawWindow(int tcCount)
{
RobustXamlLoader.Load(this);
// setup withdraw slider
WithdrawSlider.MinValue = 1;
WithdrawSlider.MaxValue = tcCount;
// and buttons
ApplyButton.OnButtonDown += _ =>
{
OnWithdrawAttempt?.Invoke(WithdrawSlider.Value);
Close();
};
CancelButton.OnButtonDown += _ => Close();
}
}
}

View File

@@ -9,7 +9,7 @@ namespace Content.Client.UserInterface.Atmos.GasTank
public sealed class GasTankBoundUserInterface
: BoundUserInterface
{
public GasTankBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) :
public GasTankBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) :
base(owner, uiKey)
{
}

View File

@@ -14,7 +14,7 @@ namespace Content.Client.VendingMachines
public SharedVendingMachineComponent? VendingMachine { get; private set; }
public VendingMachineBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public VendingMachineBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
SendMessage(new InventorySyncRequestMessage());
}

View File

@@ -13,6 +13,7 @@ public sealed class FlyBySoundSystem : SharedFlyBySoundSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public override void Initialize()
{
@@ -28,11 +29,18 @@ public sealed class FlyBySoundSystem : SharedFlyBySoundSystem
if (attachedEnt == null ||
args.OtherFixture.Body.Owner != attachedEnt ||
TryComp<ProjectileComponent>(args.OurFixture.Body.Owner, out var projectile) &&
projectile.Shooter == attachedEnt) return;
projectile.Shooter == attachedEnt)
{
return;
}
if (args.OurFixture.ID != FlyByFixture ||
!_random.Prob(component.Prob)) return;
!_random.Prob(component.Prob))
{
return;
}
SoundSystem.Play(component.Sound.GetSound(), Filter.Local(), uid, component.Sound.Params);
// Play attached to our entity because the projectile may immediately delete or the likes.
_audio.Play(component.Sound, Filter.Local(), attachedEnt.Value);
}
}

View File

@@ -8,7 +8,7 @@ namespace Content.Client.Wires.UI
{
private WiresMenu? _menu;
public WiresBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
public WiresBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}

View File

@@ -1,31 +0,0 @@
using Content.Shared.Wires;
using Robust.Client.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Content.Client.Wires.Visualizers
{
public sealed class WiresVisualizer : AppearanceVisualizer
{
[Obsolete("Subscribe to AppearanceChangeEvent instead.")]
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
if (component.TryGetData<bool>(WiresVisuals.MaintenancePanelState, out var state))
{
sprite.LayerSetVisible(WiresVisualLayers.MaintenancePanel, state);
}
// Mainly for spawn window
else
{
sprite.LayerSetVisible(WiresVisualLayers.MaintenancePanel, false);
}
}
public enum WiresVisualLayers : byte
{
MaintenancePanel,
}
}
}

View File

@@ -0,0 +1,32 @@
using Content.Shared.Wires;
using Robust.Client.GameObjects;
namespace Content.Client.Wires.Visualizers
{
public sealed class WiresVisualizerSystem : VisualizerSystem<WiresVisualsComponent>
{
protected override void OnAppearanceChange(EntityUid uid, WiresVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
var layer = args.Sprite.LayerMapReserveBlank(WiresVisualLayers.MaintenancePanel);
if(args.AppearanceData.TryGetValue(WiresVisuals.MaintenancePanelState, out var panelStateObject) &&
panelStateObject is bool panelState)
{
args.Sprite.LayerSetVisible(layer, panelState);
}
else
{
//Mainly for spawn window
args.Sprite.LayerSetVisible(layer, false);
}
}
}
public enum WiresVisualLayers : byte
{
MaintenancePanel
}
}

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