using System.Numerics; 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 ResPath("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; } }