using Content.Shared.TextScreen;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.TextScreen;
///
/// The TextScreenSystem draws text in the game world using 3x5 sprite states for each character.
///
public sealed class TextScreenSystem : VisualizerSystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
///
/// Contains char/state Key/Value pairs.
/// The states in Textures/Effects/text.rsi that special character should be replaced with.
///
private static readonly Dictionary CharStatePairs = new()
{
{ ':', "colon" },
{ '!', "exclamation" },
{ '?', "question" },
{ '*', "star" },
{ '+', "plus" },
{ '-', "dash" },
{ ' ', "blank" }
};
private const string DefaultState = "blank";
///
/// A string prefix for all text layers.
///
private const string TextScreenLayerMapKey = "textScreenLayerMapKey";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(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);
}
///
/// Resets all TextScreenComponent sprite layers, through removing them and then creating new ones.
///
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);
}
///
/// Sets , adding or removing sprite layers if necessary.
///
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;
}
///
/// Updates the layers offsets based on the text length, so it is drawn correctly.
///
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(uid);
else
RemComp(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);
}
///
/// If currently in mode:
/// Sets to the value of
///
public static void UpdateText(TextScreenVisualsComponent component)
{
if (component.CurrentMode == TextScreenMode.Text)
component.TextToDraw = component.Text;
}
///
/// Sets visibility of text to .
///
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);
}
}
///
/// Sets the states in the to match the component string.
///
///
/// Remember to set to a string first.
///
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]);
}
}
///
/// Iterates through , setting sprite states to the appropriate layers.
///
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();
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);
}
}
///
/// Returns the converted to a string in either HH:MM, MM:SS or potentially SS:mm format.
///
/// TimeSpan to convert into string.
/// Should the string be ss:ms if minutes are less than 1?
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;
}
///
/// Returns the Effects/text.rsi state string based on , or null if none available.
///
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;
}
}