mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
* Half the way to making the text screen less jank * quarter-assed timer * partial update * fleshing out timing & scrolling in update * common IGameTiming * Back to AppearanceData * partial appearance data system * funny timer states * mostly working timer scroll (damned edge pixels) * screen: scroll offset + hide * SignalTimer: remove time set on text update * extend for timer * TextScreenVisuals: cleanup * restore old names * Return old names, write more comments * fix textscreensystem ref, consistent timer frame * comments, "TextScreenVisualsComponent" * TextScreenTimerVisualsComponent, diff reduction * region * system order * no "client-side" comments * small bugfixes & cleanup with devicenet refactor * consolidate string formatting * Restore ETA/ETD text * No need for DefaultText for regular screen text * Address Pok's suggestions Co-authored-by: Pok27 <113675512+Pok27@users.noreply.github.com> * formatting * pok suggesto round 2, blyatin-1 also clean up the string vs. display value mess * TextScreenSystem.GetString -> TimerDisplay.ToStr * D2 begone! * constants to component values * comment update + SignalTimerWindow whitespace * a touch of numerical safety fixes a few errors from pasting huge text * fields aren't properties :^) * DataDef time * Update TextScreenSystem.cs * Update TextScreenTimerVisualsComponent.cs * return the pause, bunch of comments, missed a comp --------- Co-authored-by: Pok27 <113675512+Pok27@users.noreply.github.com>
540 lines
20 KiB
C#
540 lines
20 KiB
C#
using System.Collections.Frozen;
|
|
using System.Numerics;
|
|
using Content.Shared.TextScreen;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Client.TextScreen;
|
|
|
|
/// <summary>
|
|
/// Draws text on screens using 3x5 sprite states for each character. Optionally supports scrolling text.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Data is passed from server to client through <see cref="SharedAppearanceSystem.SetData"/>,
|
|
/// calling <see cref="OnAppearanceChange"/>. This sets <see cref="TextScreenVisualsComponent.RowData"/>,
|
|
/// which will be drawn in the next Update call.
|
|
/// <br/>
|
|
/// Layers for the text screen are set up on the ComponentStartup event, and stored in tuples
|
|
/// in the <see cref="TextScreenVisualsComponent.RowData"/>. An additional character per row is used for
|
|
/// screens that support scrolling.
|
|
/// </remarks>
|
|
/// <seealso cref="TextScreenVisualsComponent"/>
|
|
/// <seealso cref="TextScreenTimerVisualsComponent"/>
|
|
public sealed partial class TextScreenSystem : VisualizerSystem<TextScreenVisualsComponent>
|
|
{
|
|
[Dependency] private IGameTiming _timing = default!;
|
|
|
|
[Dependency] private EntityQuery<SpriteComponent> _spriteQuery;
|
|
[Dependency] private EntityQuery<TextScreenTimerVisualsComponent> _screenTimerQuery;
|
|
|
|
/// <summary>
|
|
/// Contains the state that should be drawn for each non-alphanumeric character.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Each state is expected to be in Textures/Effects/text.rsi.
|
|
/// </remarks>
|
|
private static readonly FrozenDictionary<char, string> CharStatePairs = new Dictionary<char, string>
|
|
{
|
|
{ '<', "angle-l" },
|
|
{ '>', "angle-r" },
|
|
{'\'', "apostrophe" },
|
|
{'\\', "backslash" },
|
|
{ '[', "bracket-l" },
|
|
{ ']', "bracket-r" },
|
|
{ '^', "caret" },
|
|
{ ':', "colon" },
|
|
{ ',', "comma" },
|
|
{ '-', "dash" },
|
|
{ '=', "equals" },
|
|
{ '!', "exclamation" },
|
|
{ '#', "hash" },
|
|
{ '(', "paren-l" },
|
|
{ ')', "paren-r" },
|
|
{ '%', "percent" },
|
|
{ '.', "period" },
|
|
{ '+', "plus" },
|
|
{ '?', "question" },
|
|
{ '"', "quotation" },
|
|
{ ';', "semicolon" },
|
|
{ '/', "slash" },
|
|
{ '$', "speso" },
|
|
{ '*', "star" },
|
|
{ '_', "underscore" },
|
|
}.ToFrozenDictionary();
|
|
|
|
/// <summary>
|
|
/// A string prefix for all text layers.
|
|
/// </summary>
|
|
private const string TextMapKey = "textMapKey";
|
|
|
|
/// <summary>
|
|
/// The path to the RSI containing the text sprites.
|
|
/// </summary>
|
|
private const string TextPath = "Effects/text.rsi";
|
|
|
|
/// <summary>
|
|
/// The width of an individual character, in pixels.
|
|
/// </summary>
|
|
private const int CharWidth = 4;
|
|
|
|
#region Inherited
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
UpdatesOutsidePrediction = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appearance data handler - drives the actual text/timer.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Sets <see cref="TextScreenVisualsComponent.NewTextToDisplay"/> on any change,
|
|
/// which will be picked up in the next Update.
|
|
/// Color data, updating less frequently, is updated on the layers in the call directly.
|
|
/// </remarks>
|
|
protected override void OnAppearanceChange(EntityUid uid, TextScreenVisualsComponent comp, ref AppearanceChangeEvent args)
|
|
{
|
|
bool anyChange;
|
|
if (args.TryGetData(TextScreenVisuals.Color, out Color color))
|
|
{
|
|
anyChange = comp.CurrentColor != color;
|
|
comp.CurrentColor = color;
|
|
}
|
|
else
|
|
{
|
|
anyChange = comp.CurrentColor != comp.Color;
|
|
comp.CurrentColor = comp.Color;
|
|
}
|
|
|
|
// Update layer color - less frequent to change, no need to change in update.
|
|
if (anyChange && _spriteQuery.TryComp(uid, out var sprite))
|
|
{
|
|
foreach (var row in comp.RowData)
|
|
{
|
|
foreach (var layer in row.Layers)
|
|
{
|
|
SpriteSystem.LayerSetColor((uid, sprite), layer.Key, comp.CurrentColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
args.TryGetData(TextScreenVisuals.ScreenText, out string? screenTextValue);
|
|
args.TryGetData(TextScreenVisuals.DefaultText, out string? defaultTextValue);
|
|
|
|
if (!args.TryGetData(TextScreenVisuals.ScreenTextTime, out TimeSpan? scrollTime))
|
|
scrollTime = _timing.CurTime;
|
|
|
|
if (_screenTimerQuery.TryComp(uid, out var timer)
|
|
&& args.TryGetData(TextScreenVisuals.TargetTime, out TimeSpan? textTime))
|
|
{
|
|
// If we have a valid timer, draw the timer.
|
|
if (defaultTextValue != null && defaultTextValue != timer.FinishedText)
|
|
{
|
|
timer.FinishedText = defaultTextValue;
|
|
anyChange = true;
|
|
}
|
|
if (screenTextValue != null && screenTextValue != timer.RunningText)
|
|
{
|
|
timer.RunningText = screenTextValue;
|
|
anyChange = true;
|
|
}
|
|
if (textTime != timer.TargetTime)
|
|
{
|
|
timer.TargetTime = textTime;
|
|
anyChange = true;
|
|
}
|
|
comp.TextTime = scrollTime.Value;
|
|
comp.NewTextToDisplay = anyChange;
|
|
}
|
|
else
|
|
{
|
|
// Otherwise, if we have text, draw our text.
|
|
var newTextValue = screenTextValue ?? defaultTextValue;
|
|
if (newTextValue != comp.TextToDisplay)
|
|
{
|
|
comp.TextToDisplay = newTextValue;
|
|
anyChange = true;
|
|
}
|
|
comp.TextTime = scrollTime.Value;
|
|
comp.NewTextToDisplay = anyChange;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update handler - keep timers and scrolling text up to date.
|
|
/// </summary>
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
// Timers: update the printed value before handling text screen logic.
|
|
var timerQuery = EntityQueryEnumerator<TextScreenTimerVisualsComponent>();
|
|
while (timerQuery.MoveNext(out var uid, out var timer))
|
|
{
|
|
if (timer.TargetTime == null)
|
|
continue;
|
|
|
|
if (timer.TargetTime <= _timing.CurTime)
|
|
{
|
|
// Timer finished, draw finished text and reset timer state.
|
|
SetTextToDisplay(uid, timer.FinishedText);
|
|
UpdateTimerSprite((uid, timer), false);
|
|
|
|
timer.TargetTime = null;
|
|
timer.ScreenValue = new(0, 0);
|
|
}
|
|
else
|
|
{
|
|
// Check if we need to update our time by the value it would print.
|
|
var screenValue = ConvertTimeToDisplayValue(timer.TargetTime.Value - _timing.CurTime, timer.ShowCentiseconds);
|
|
if (screenValue == timer.ScreenValue)
|
|
continue;
|
|
|
|
var timerText = GetTimerString((uid, timer), screenValue);
|
|
SetTextToDisplay(uid, timerText);
|
|
UpdateTimerSprite((uid, timer), true);
|
|
timer.ScreenValue = screenValue;
|
|
}
|
|
}
|
|
|
|
// Text screens: update layers on changed, scroll if needed.
|
|
var screenQuery = EntityQueryEnumerator<TextScreenVisualsComponent, SpriteComponent>();
|
|
while (screenQuery.MoveNext(out var uid, out var screen, out var sprite))
|
|
{
|
|
if (screen.NewTextToDisplay)
|
|
{
|
|
// Update text layers
|
|
UpdateAndDrawText((uid, screen, sprite));
|
|
screen.NewTextToDisplay = false;
|
|
}
|
|
else if (screen.ScrollEnabled)
|
|
{
|
|
for (var i = 0; i < screen.RowData.Length; i++)
|
|
{
|
|
var rowData = screen.RowData[i];
|
|
if (rowData.NextScroll > _timing.CurTime)
|
|
continue;
|
|
|
|
ScrollRow(ref rowData);
|
|
DrawLayers((uid, screen, sprite), ref rowData, i);
|
|
|
|
// Commit changes to struct.
|
|
screen.RowData[i] = rowData;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion Inherited
|
|
|
|
#region Public API
|
|
/// <summary>
|
|
/// Converts <paramref name="duration"/> into a <see cref="TimerDisplay"/> for display.
|
|
/// </summary>
|
|
/// <param name="duration">The duration to convert.</param>
|
|
/// <param name="showCentiseconds">If true, enables sub-second precision for small durations.</param>
|
|
public static TimerDisplay ConvertTimeToDisplayValue(TimeSpan duration, bool showCentiseconds)
|
|
{
|
|
if (duration < TimeSpan.Zero)
|
|
return new(0, 0);
|
|
|
|
var millis = duration.TotalMilliseconds;
|
|
if (showCentiseconds && millis < 100_000) // 9999 centiseconds, 99:99, the largest value that could fit in two fields.
|
|
{
|
|
var centis = (int)millis / 10;
|
|
return new(centis / 100, centis % 100);
|
|
}
|
|
|
|
if (millis < TimeSpan.MillisecondsPerHour)
|
|
return new(duration.Minutes, duration.Seconds);
|
|
|
|
return new(duration.Hours, duration.Minutes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the text to display for a text screen, flags an update on the next Update.
|
|
/// </summary>
|
|
public void SetTextToDisplay(Entity<TextScreenVisualsComponent?> ent, string? text)
|
|
{
|
|
if (!Resolve(ent, ref ent.Comp))
|
|
return;
|
|
|
|
if (ent.Comp.TextToDisplay == text)
|
|
return;
|
|
|
|
ent.Comp.TextToDisplay = text;
|
|
ent.Comp.NewTextToDisplay = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the <c>Effects/text.rsi</c> state to draw for <paramref name="character"/>, or null if none available.
|
|
/// </summary>
|
|
public static string? GetStateFromChar(char? character)
|
|
{
|
|
if (character == null)
|
|
return null;
|
|
|
|
// First checks if its one of our special characters
|
|
if (CharStatePairs.TryGetValue(character.Value, out var value))
|
|
return value;
|
|
|
|
// Or else it checks if its a normal letter or digit
|
|
// (With apologies to those using non-latin character sets)
|
|
if (char.IsAscii(character.Value) && char.IsLetterOrDigit(character.Value))
|
|
return character.Value.ToString().ToLower();
|
|
|
|
return null;
|
|
}
|
|
#endregion Public API
|
|
|
|
#region Event Handlers
|
|
[SubscribeLocalEvent]
|
|
private void OnStartup(Entity<TextScreenVisualsComponent> ent, ref ComponentStartup args)
|
|
{
|
|
if (!_spriteQuery.TryComp(ent, out var sprite))
|
|
return;
|
|
|
|
if (ent.Comp.CurrentColor == default)
|
|
ent.Comp.CurrentColor = ent.Comp.Color;
|
|
|
|
// Create text layers
|
|
var textRsiPath = new ResPath(TextPath);
|
|
for (var rowIdx = 0; rowIdx < ent.Comp.RowData.Length; rowIdx++)
|
|
{
|
|
var maxIndex = ent.Comp.ScrollEnabled ? ent.Comp.RowLength + 1 : ent.Comp.RowLength;
|
|
var layers = ent.Comp.RowData[rowIdx].Layers;
|
|
|
|
for (var chr = 0; chr < maxIndex; chr++)
|
|
{
|
|
var newKey = TextMapKey + rowIdx + chr;
|
|
var layerIndex = SpriteSystem.LayerMapReserve((ent, sprite), newKey);
|
|
SpriteSystem.LayerSetRsi((ent, sprite), layerIndex, textRsiPath);
|
|
SpriteSystem.LayerSetColor((ent, sprite), layerIndex, ent.Comp.CurrentColor);
|
|
layers.Add((newKey, null));
|
|
}
|
|
}
|
|
|
|
// Place frame on top of text layers (obscuring the scroll trick)
|
|
if (ent.Comp.FrameState != null)
|
|
SpriteSystem.AddLayer((ent, sprite), ent.Comp.FrameState, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles non-trivial pause timing for scrolling.
|
|
/// </summary>
|
|
[SubscribeLocalEvent]
|
|
private void OnUnpaused(Entity<TextScreenVisualsComponent> ent, ref EntityUnpausedEvent args)
|
|
{
|
|
for (int i = 0; i < ent.Comp.RowData.Length; i++)
|
|
{
|
|
var rowData = ent.Comp.RowData[i];
|
|
if (rowData.NextScroll != TimeSpan.MaxValue) // Reserved value, should stay at max.
|
|
{
|
|
rowData.NextScroll += args.PausedTime;
|
|
ent.Comp.RowData[i] = rowData;
|
|
}
|
|
}
|
|
}
|
|
#endregion Event Handlers
|
|
|
|
#region Internal
|
|
/// <summary>
|
|
/// Returns the string (newline separated rows) that should be displayed on a screen for a particular timer value.
|
|
/// </summary>
|
|
private string GetTimerString(Entity<TextScreenTimerVisualsComponent> ent, TimerDisplay newScreenValue)
|
|
{
|
|
if (ent.Comp.TimerRow < 0)
|
|
return ent.Comp.RunningText;
|
|
|
|
var strings = ent.Comp.RunningText.Split("\n");
|
|
var timerString = newScreenValue.ToString();
|
|
|
|
if (ent.Comp.TimerRow < strings.Length)
|
|
{
|
|
// Timer row within array bounds.
|
|
strings[ent.Comp.TimerRow] = timerString;
|
|
}
|
|
else
|
|
{
|
|
// Extend our array until we can fit the timer row.
|
|
var newStrings = new string[ent.Comp.TimerRow + 1];
|
|
for (var i = 0; i < strings.Length; i++)
|
|
{
|
|
newStrings[i] = strings[i];
|
|
}
|
|
|
|
for (var i = strings.Length; i < ent.Comp.TimerRow; i++)
|
|
{
|
|
newStrings[i] = "";
|
|
}
|
|
|
|
newStrings[ent.Comp.TimerRow] = timerString;
|
|
|
|
strings = newStrings;
|
|
}
|
|
return string.Join('\n', strings);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates row data for a given text screen before drawing all of its rows.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Should be called whenever the screen has updates to its text strings.
|
|
/// If you only need to scroll existing text, DrawLayers can be used directly.
|
|
/// </remarks>
|
|
private void UpdateAndDrawText(Entity<TextScreenVisualsComponent, SpriteComponent> ent)
|
|
{
|
|
var screen = ent.Comp1;
|
|
var sprite = ent.Comp2;
|
|
|
|
var texts = screen.TextToDisplay?.Split("\n") ?? [];
|
|
|
|
// Update each row from the split text.
|
|
for (var i = 0; i < screen.RowData.Length; i++)
|
|
{
|
|
var rowData = screen.RowData[i];
|
|
|
|
if (i >= texts.Length || texts[i].Length == 0)
|
|
{
|
|
// Invalid text: clear all row states (no need to offset, just set state to null).
|
|
for (var j = 0; j < rowData.Layers.Count; j++)
|
|
{
|
|
var layerTuple = rowData.Layers[j];
|
|
|
|
if (SpriteSystem.LayerMapTryGet((ent, sprite), layerTuple.Key, out var layerIndex, false))
|
|
SpriteSystem.LayerSetRsiState((ent, sprite), layerIndex, null);
|
|
|
|
rowData.Layers[j] = new(layerTuple.Key, null);
|
|
}
|
|
|
|
// Set data back to a default state.
|
|
rowData.ScrollDelay = TimeSpan.MaxValue;
|
|
rowData.NextScroll = TimeSpan.MaxValue;
|
|
rowData.ScrollPosition = 0;
|
|
rowData.Text = "";
|
|
}
|
|
else
|
|
{
|
|
if (!screen.ScrollEnabled || texts[i].Length <= screen.RowLength)
|
|
{
|
|
// Non-scrolling: ensure that our string fits on one screen.
|
|
rowData.ScrollDelay = TimeSpan.MaxValue;
|
|
rowData.NextScroll = TimeSpan.MaxValue;
|
|
rowData.ScrollPosition = 0;
|
|
rowData.Text = texts[i][..int.Min(texts[i].Length, screen.RowLength)];
|
|
}
|
|
else
|
|
{
|
|
// Scrolling: find our timing, adjust scroll position within the text.
|
|
var rowText = texts[i][..int.Min(texts[i].Length, screen.MaxScrollingCharacters)];
|
|
rowData.Text = rowText.PadRight(rowText.Length + screen.RowLength - 1);
|
|
|
|
var newMaxPixelScrollTime = screen.MaxMessageScrollTime / rowText.Length / CharWidth; // Scroll speed per pixel at the max message scroll length.
|
|
rowData.ScrollDelay = newMaxPixelScrollTime < screen.MaxPixelScrollTime ? newMaxPixelScrollTime : screen.MaxPixelScrollTime;
|
|
|
|
rowData.NextScroll = screen.TextTime;
|
|
rowData.ScrollPosition = 0;
|
|
|
|
// Update the scroll timing & position to where it should be.
|
|
ScrollRow(ref rowData);
|
|
}
|
|
|
|
// Draw our new layers.
|
|
DrawLayers((ent.Owner, screen, sprite), ref rowData, i);
|
|
}
|
|
|
|
// Finally, commit the row state.
|
|
screen.RowData[i] = rowData;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates a TextScreenRow's scroll position and timing based on current state and sim time.
|
|
/// </summary>
|
|
private void ScrollRow(ref TextScreenRow rowData)
|
|
{
|
|
var difference = (_timing.CurTime - rowData.NextScroll).TotalSeconds;
|
|
if (!double.IsPositive(difference))
|
|
difference = 0.0;
|
|
|
|
var increments = 1;
|
|
if (rowData.ScrollDelay.TotalSeconds > 0.0)
|
|
increments += (int)Math.Truncate(difference / rowData.ScrollDelay.TotalSeconds);
|
|
|
|
rowData.ScrollPosition += increments;
|
|
rowData.NextScroll += increments * rowData.ScrollDelay;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws sprite layers for the given row on the given entity.
|
|
/// </summary>
|
|
private void DrawLayers(Entity<TextScreenVisualsComponent, SpriteComponent> ent, ref TextScreenRow rowData, int rowIndex)
|
|
{
|
|
Entity<SpriteComponent?> sprite = (ent.Owner, ent.Comp2);
|
|
var screen = ent.Comp1;
|
|
|
|
// Find our horizontal offset, if we're scrolling (prevents text from going out of bounds when well set)
|
|
var textIsScrolling = rowData.Text.Length > screen.RowLength;
|
|
var scrollOffset = textIsScrolling ? screen.HorizontalScrollOffset : 0;
|
|
|
|
// The horizontal shift, in pixels, that each character is drawn at. For non-scrolling text, ScrollPosition must be 0.
|
|
var subCharOffset = rowData.ScrollPosition % CharWidth;
|
|
|
|
// Draw all of the characters in our row's text.
|
|
var maxCharIndex = int.Min(rowData.Layers.Count, rowData.Text.Length);
|
|
for (var j = 0; j < maxCharIndex; j++)
|
|
{
|
|
var layerTuple = rowData.Layers[j];
|
|
var charIndex = (j + rowData.ScrollPosition / CharWidth) % rowData.Text.Length;
|
|
|
|
var newState = GetStateFromChar(rowData.Text[charIndex]);
|
|
|
|
if (SpriteSystem.LayerMapTryGet(sprite, layerTuple.Key, out var layerIndex, false))
|
|
{
|
|
SpriteSystem.LayerSetRsiState(sprite, layerIndex, newState);
|
|
SpriteSystem.LayerSetOffset(sprite, layerIndex, Vector2.Multiply(
|
|
screen.TextOffset +
|
|
new Vector2((j - maxCharIndex / 2f + 0.5f) * CharWidth - subCharOffset + scrollOffset, -rowIndex * screen.RowOffset),
|
|
TextScreenVisualsComponent.PixelSize));
|
|
}
|
|
|
|
rowData.Layers[j] = new(layerTuple.Key, newState);
|
|
}
|
|
|
|
// Hide the remaining layers (fill with null state).
|
|
for (var j = maxCharIndex; j < rowData.Layers.Count; j++)
|
|
{
|
|
var layerTuple = rowData.Layers[j];
|
|
|
|
if (SpriteSystem.LayerMapTryGet((ent, sprite), layerTuple.Key, out var layerIndex, false))
|
|
SpriteSystem.LayerSetRsiState((ent, sprite), layerIndex, null);
|
|
|
|
rowData.Layers[j] = new(layerTuple.Key, null);
|
|
}
|
|
|
|
// Handle leftmost/rightmost scroll hiding, if needed.
|
|
// NOTE: As subCharOffset increases, the text scrolls leftwards.
|
|
// Leftmost state should be hidden at high values, rightmost at low values.
|
|
if (rowData.Layers.Count > 0)
|
|
{
|
|
var hideLeft = textIsScrolling && subCharOffset >= CharWidth - screen.LeftInvisiblePixels;
|
|
var hideRight = textIsScrolling && subCharOffset < screen.RightInvisiblePixels;
|
|
SpriteSystem.LayerSetVisible((ent, sprite), rowData.Layers[0].Key, !hideLeft);
|
|
SpriteSystem.LayerSetVisible((ent, sprite), rowData.Layers[^1].Key, !hideRight);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the light on a timer's sprite based on if it's currently running or not.
|
|
/// </summary>
|
|
private void UpdateTimerSprite(Entity<TextScreenTimerVisualsComponent> ent, bool running)
|
|
{
|
|
if (_spriteQuery.TryComp(ent, out var sprite)
|
|
&& SpriteSystem.LayerMapTryGet((ent, sprite), TimerVisualLayers.Light, out var layerIndex, logMissing: false))
|
|
{
|
|
SpriteSystem.LayerSetRsiState((ent, sprite), layerIndex, running ? ent.Comp.RunningState : ent.Comp.FinishedState);
|
|
}
|
|
}
|
|
#endregion Internal
|
|
}
|