* brigtimer * ok * TextScreen w timer implementation * second commit * working brig timer * signal timers near completion * soon done * removed licenses, fixes noRotation on screens, minor edits * no message * no message * removed my last todos * removed csproj.rej?? * missed a thing with .yml and tests * fix tests * Update base_structureairlocks.yml * timespan type serialize * activation turned into comp * sloth review * Update timer.yml * small changes --------- Co-authored-by: CommieFlowers <rasmus.cedergren@hotmail.com> Co-authored-by: rolfero <45628623+rolfero@users.noreply.github.com>
296 lines
11 KiB
C#
296 lines
11 KiB
C#
using Content.Shared.TextScreen;
|
|
using Robust.Client.GameObjects;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
|
|
namespace Content.Client.TextScreen;
|
|
|
|
/// <summary>
|
|
/// The TextScreenSystem draws text in the game world using 3x5 sprite states for each character.
|
|
/// </summary>
|
|
public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsComponent>
|
|
{
|
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
|
|
/// <summary>
|
|
/// Contains char/state Key/Value pairs. <br/>
|
|
/// The states in Textures/Effects/text.rsi that special character should be replaced with.
|
|
/// </summary>
|
|
private static readonly Dictionary<char, string> CharStatePairs = new()
|
|
{
|
|
{ ':', "colon" },
|
|
{ '!', "exclamation" },
|
|
{ '?', "question" },
|
|
{ '*', "star" },
|
|
{ '+', "plus" },
|
|
{ '-', "dash" },
|
|
{ ' ', "blank" }
|
|
};
|
|
|
|
private const string DefaultState = "blank";
|
|
|
|
/// <summary>
|
|
/// A string prefix for all text layers.
|
|
/// </summary>
|
|
private const string TextScreenLayerMapKey = "textScreenLayerMapKey";
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
SubscribeLocalEvent<TextScreenVisualsComponent, ComponentInit>(OnInit);
|
|
}
|
|
|
|
private void OnInit(EntityUid uid, TextScreenVisualsComponent component, ComponentInit args)
|
|
{
|
|
if (!TryComp(uid, out SpriteComponent? sprite))
|
|
return;
|
|
|
|
ResetTextLength(uid, component, sprite);
|
|
PrepareLayerStatesToDraw(uid, component, sprite);
|
|
UpdateLayersToDraw(uid, component, sprite);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets all TextScreenComponent sprite layers, through removing them and then creating new ones.
|
|
/// </summary>
|
|
public void ResetTextLength(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
|
|
{
|
|
if (!Resolve(uid, ref sprite))
|
|
return;
|
|
|
|
foreach (var (key, _) in component.LayerStatesToDraw)
|
|
{
|
|
sprite.RemoveLayer(key);
|
|
}
|
|
|
|
component.LayerStatesToDraw.Clear();
|
|
|
|
var length = component.TextLength;
|
|
component.TextLength = 0;
|
|
SetTextLength(uid, component, length, sprite);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets <see cref="TextScreenVisualsComponent.TextLength"/>, adding or removing sprite layers if necessary.
|
|
/// </summary>
|
|
public void SetTextLength(EntityUid uid, TextScreenVisualsComponent component, int newLength, SpriteComponent? sprite = null)
|
|
{
|
|
if (newLength == component.TextLength)
|
|
return;
|
|
|
|
if (!Resolve(uid, ref sprite))
|
|
return;
|
|
|
|
if (newLength > component.TextLength)
|
|
{
|
|
for (var i = component.TextLength; i < newLength; i++)
|
|
{
|
|
sprite.LayerMapReserveBlank(TextScreenLayerMapKey + i);
|
|
component.LayerStatesToDraw.Add(TextScreenLayerMapKey + i, null);
|
|
sprite.LayerSetRSI(TextScreenLayerMapKey + i, new ResourcePath("Effects/text.rsi"));
|
|
sprite.LayerSetColor(TextScreenLayerMapKey + i, component.Color);
|
|
sprite.LayerSetState(TextScreenLayerMapKey + i, DefaultState);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (var i = component.TextLength; i > newLength; i--)
|
|
{
|
|
sprite.LayerMapGet(TextScreenLayerMapKey + (i - 1));
|
|
component.LayerStatesToDraw.Remove(TextScreenLayerMapKey + (i - 1));
|
|
sprite.RemoveLayer(TextScreenLayerMapKey + (i - 1));
|
|
}
|
|
}
|
|
|
|
UpdateOffsets(uid, component, sprite);
|
|
|
|
component.TextLength = newLength;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the layers offsets based on the text length, so it is drawn correctly.
|
|
/// </summary>
|
|
public void UpdateOffsets(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
|
|
{
|
|
if (!Resolve(uid, ref sprite))
|
|
return;
|
|
|
|
for (var i = 0; i < component.LayerStatesToDraw.Count; i++)
|
|
{
|
|
var offset = i - (component.LayerStatesToDraw.Count - 1) / 2.0f;
|
|
sprite.LayerSetOffset(TextScreenLayerMapKey + i, new Vector2(offset * TextScreenVisualsComponent.PixelSize * 4f, 0.0f) + component.TextOffset);
|
|
}
|
|
}
|
|
|
|
protected override void OnAppearanceChange(EntityUid uid, TextScreenVisualsComponent component, ref AppearanceChangeEvent args)
|
|
{
|
|
UpdateAppearance(uid, component, args.Component, args.Sprite);
|
|
}
|
|
|
|
public void UpdateAppearance(EntityUid uid, TextScreenVisualsComponent component, AppearanceComponent? appearance = null, SpriteComponent? sprite = null)
|
|
{
|
|
if (!Resolve(uid, ref appearance, ref sprite))
|
|
return;
|
|
|
|
if (AppearanceSystem.TryGetData(uid, TextScreenVisuals.On, out bool on, appearance))
|
|
{
|
|
component.Activated = on;
|
|
UpdateVisibility(uid, component, sprite);
|
|
}
|
|
|
|
if (AppearanceSystem.TryGetData(uid, TextScreenVisuals.Mode, out TextScreenMode mode, appearance))
|
|
{
|
|
component.CurrentMode = mode;
|
|
if (component.CurrentMode == TextScreenMode.Timer)
|
|
EnsureComp<TextScreenTimerComponent>(uid);
|
|
else
|
|
RemComp<TextScreenTimerComponent>(uid);
|
|
|
|
UpdateText(component);
|
|
}
|
|
|
|
if (AppearanceSystem.TryGetData(uid, TextScreenVisuals.TargetTime, out TimeSpan time, appearance))
|
|
{
|
|
component.TargetTime = time;
|
|
}
|
|
|
|
if (AppearanceSystem.TryGetData(uid, TextScreenVisuals.ScreenText, out string text, appearance))
|
|
{
|
|
component.Text = text;
|
|
}
|
|
|
|
UpdateText(component);
|
|
PrepareLayerStatesToDraw(uid, component, sprite);
|
|
UpdateLayersToDraw(uid, component, sprite);
|
|
}
|
|
|
|
/// <summary>
|
|
/// If currently in <see cref="TextScreenMode.Text"/> mode: <br/>
|
|
/// Sets <see cref="TextScreenVisualsComponent.TextToDraw"/> to the value of <see cref="TextScreenVisualsComponent.Text"/>
|
|
/// </summary>
|
|
public static void UpdateText(TextScreenVisualsComponent component)
|
|
{
|
|
if (component.CurrentMode == TextScreenMode.Text)
|
|
component.TextToDraw = component.Text;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets visibility of text to <see cref="TextScreenVisualsComponent.Activated"/>.
|
|
/// </summary>
|
|
public void UpdateVisibility(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
|
|
{
|
|
if (!Resolve(uid, ref sprite))
|
|
return;
|
|
|
|
foreach (var (key, _) in component.LayerStatesToDraw)
|
|
{
|
|
sprite.LayerSetVisible(key, component.Activated);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the states in the <see cref="TextScreenVisualsComponent.LayerStatesToDraw"/> to match the component <see cref="TextScreenVisualsComponent.TextToDraw"/> string.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Remember to set <see cref="TextScreenVisualsComponent.TextToDraw"/> to a string first.
|
|
/// </remarks>
|
|
public void PrepareLayerStatesToDraw(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
|
|
{
|
|
if (!Resolve(uid, ref sprite))
|
|
return;
|
|
|
|
for (var i = 0; i < component.TextLength; i++)
|
|
{
|
|
if (i >= component.TextToDraw.Length)
|
|
{
|
|
component.LayerStatesToDraw[TextScreenLayerMapKey + i] = DefaultState;
|
|
continue;
|
|
}
|
|
component.LayerStatesToDraw[TextScreenLayerMapKey + i] = GetStateFromChar(component.TextToDraw[i]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Iterates through <see cref="TextScreenVisualsComponent.LayerStatesToDraw"/>, setting sprite states to the appropriate layers.
|
|
/// </summary>
|
|
public void UpdateLayersToDraw(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
|
|
{
|
|
if (!Resolve(uid, ref sprite))
|
|
return;
|
|
|
|
foreach (var (key, state) in component.LayerStatesToDraw)
|
|
{
|
|
if (state == null)
|
|
continue;
|
|
sprite.LayerSetState(key, state);
|
|
}
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
var query = EntityQueryEnumerator<TextScreenVisualsComponent, TextScreenTimerComponent>();
|
|
while (query.MoveNext(out var uid, out var comp, out _))
|
|
{
|
|
// Basically Abs(TimeSpan, TimeSpan) -> Gives the difference between the current time and the target time.
|
|
var timeToShow = _gameTiming.CurTime > comp.TargetTime ? _gameTiming.CurTime - comp.TargetTime : comp.TargetTime - _gameTiming.CurTime;
|
|
comp.TextToDraw = TimeToString(timeToShow, false);
|
|
PrepareLayerStatesToDraw(uid, comp);
|
|
UpdateLayersToDraw(uid, comp);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the <paramref name="timeSpan"/> converted to a string in either HH:MM, MM:SS or potentially SS:mm format.
|
|
/// </summary>
|
|
/// <param name="timeSpan">TimeSpan to convert into string.</param>
|
|
/// <param name="getMilliseconds">Should the string be ss:ms if minutes are less than 1?</param>
|
|
public static string TimeToString(TimeSpan timeSpan, bool getMilliseconds = true)
|
|
{
|
|
string firstString;
|
|
string lastString;
|
|
|
|
if (timeSpan.TotalHours >= 1)
|
|
{
|
|
firstString = timeSpan.Hours.ToString("D2");
|
|
lastString = timeSpan.Minutes.ToString("D2");
|
|
}
|
|
else if (timeSpan.TotalMinutes >= 1 || !getMilliseconds)
|
|
{
|
|
firstString = timeSpan.Minutes.ToString("D2");
|
|
// It's nicer to see a timer set at 5 seconds actually start at 00:05 instead of 00:04.
|
|
var seconds = timeSpan.Seconds + (timeSpan.Milliseconds > 500 ? 1 : 0);
|
|
lastString = seconds.ToString("D2");
|
|
}
|
|
else
|
|
{
|
|
firstString = timeSpan.Seconds.ToString("D2");
|
|
var centiseconds = timeSpan.Milliseconds / 10;
|
|
lastString = centiseconds.ToString("D2");
|
|
}
|
|
|
|
return firstString + ':' + lastString;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the Effects/text.rsi state string based on <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.ContainsKey(character.Value))
|
|
return CharStatePairs[character.Value];
|
|
|
|
// Or else it checks if its a normal letter or digit
|
|
if (char.IsLetterOrDigit(character.Value))
|
|
return character.Value.ToString().ToLower();
|
|
|
|
return null;
|
|
}
|
|
}
|