--- /dev/null
+using Content.Shared.MapText;
+using Robust.Client.Graphics;
+
+namespace Content.Client.MapText;
+
+[RegisterComponent]
+public sealed partial class MapTextComponent : SharedMapTextComponent
+{
+ /// <summary>
+ /// The font that gets cached on component init or state changes
+ /// </summary>
+ [ViewVariables]
+ public VectorFont? CachedFont;
+
+ /// <summary>
+ /// The text currently being displayed. This is either <see cref="SharedMapTextComponent.Text"/> or the
+ /// localized text <see cref="SharedMapTextComponent.LocText"/> or
+ /// </summary>
+ public string CachedText = string.Empty;
+}
--- /dev/null
+using System.Numerics;
+using Content.Shared.MapText;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.RichText;
+using Robust.Shared;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.MapText;
+
+/// <summary>
+/// Draws map text as an overlay
+/// </summary>
+public sealed class MapTextOverlay : Overlay
+{
+ private readonly IConfigurationManager _configManager;
+ private readonly IEntityManager _entManager;
+ private readonly IUserInterfaceManager _uiManager;
+ private readonly SharedTransformSystem _transform;
+ public override OverlaySpace Space => OverlaySpace.ScreenSpace;
+
+ public MapTextOverlay(
+ IConfigurationManager configManager,
+ IEntityManager entManager,
+ IUserInterfaceManager uiManager,
+ SharedTransformSystem transform,
+ IResourceCache resourceCache,
+ IPrototypeManager prototypeManager)
+ {
+ _configManager = configManager;
+ _entManager = entManager;
+ _uiManager = uiManager;
+ _transform = transform;
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (args.ViewportControl == null)
+ return;
+
+ args.DrawingHandle.SetTransform(Matrix3x2.Identity);
+
+ var scale = _configManager.GetCVar(CVars.DisplayUIScale);
+
+ if (scale == 0f)
+ scale = _uiManager.DefaultUIScale;
+
+ DrawWorld(args.ScreenHandle, args, scale);
+
+ args.DrawingHandle.UseShader(null);
+ }
+
+ private void DrawWorld(DrawingHandleScreen handle, OverlayDrawArgs args, float scale)
+ {
+ if ( args.ViewportControl == null)
+ return;
+
+ var matrix = args.ViewportControl.GetWorldToScreenMatrix();
+ var query = _entManager.AllEntityQueryEnumerator<MapTextComponent>();
+
+ // Enlarge bounds to try prevent pop-in due to large text.
+ var bounds = args.WorldBounds.Enlarged(2);
+
+ while(query.MoveNext(out var uid, out var mapText))
+ {
+ var mapPos = _transform.GetMapCoordinates(uid);
+
+ if (mapPos.MapId != args.MapId)
+ continue;
+
+ if (!bounds.Contains(mapPos.Position))
+ continue;
+
+ if (mapText.CachedFont == null)
+ continue;
+
+ var pos = Vector2.Transform(mapPos.Position, matrix) + mapText.Offset;
+ var dimensions = handle.GetDimensions(mapText.CachedFont, mapText.CachedText, scale);
+ handle.DrawString(mapText.CachedFont, pos - dimensions / 2f, mapText.CachedText, scale, mapText.Color);
+ }
+ }
+}
--- /dev/null
+using Content.Shared.MapText;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.RichText;
+using Robust.Shared.Configuration;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.MapText;
+
+/// <inheritdoc/>
+public sealed class MapTextSystem : SharedMapTextSystem
+{
+ [Dependency] private readonly IConfigurationManager _configManager = default!;
+ [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
+ [Dependency] private readonly SharedTransformSystem _transform = default!;
+ [Dependency] private readonly IResourceCache _resourceCache = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IOverlayManager _overlayManager = default!;
+
+ private MapTextOverlay _overlay = default!;
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<MapTextComponent, ComponentStartup>(OnComponentStartup);
+ SubscribeLocalEvent<MapTextComponent, ComponentHandleState>(HandleCompState);
+
+ _overlay = new MapTextOverlay(_configManager, EntityManager, _uiManager, _transform, _resourceCache, _prototypeManager);
+ _overlayManager.AddOverlay(_overlay);
+
+ // TODO move font prototype to robust.shared, then use ProtoId<FontPrototype>
+ DebugTools.Assert(_prototypeManager.HasIndex<FontPrototype>(SharedMapTextComponent.DefaultFont));
+ }
+
+ private void OnComponentStartup(Entity<MapTextComponent> ent, ref ComponentStartup args)
+ {
+ CacheText(ent.Comp);
+ // TODO move font prototype to robust.shared, then use ProtoId<FontPrototype>
+ DebugTools.Assert(_prototypeManager.HasIndex<FontPrototype>(ent.Comp.FontId));
+ }
+
+ private void HandleCompState(Entity<MapTextComponent> ent, ref ComponentHandleState args)
+ {
+ if (args.Current is not MapTextComponentState state)
+ return;
+
+ ent.Comp.Text = state.Text;
+ ent.Comp.LocText = state.LocText;
+ ent.Comp.Color = state.Color;
+ ent.Comp.FontId = state.FontId;
+ ent.Comp.FontSize = state.FontSize;
+ ent.Comp.Offset = state.Offset;
+
+ CacheText(ent.Comp);
+ }
+
+ private void CacheText(MapTextComponent component)
+ {
+ component.CachedFont = null;
+
+ component.CachedText = string.IsNullOrWhiteSpace(component.Text)
+ ? Loc.GetString(component.LocText)
+ : component.Text;
+
+ if (!_prototypeManager.TryIndex<FontPrototype>(component.FontId, out var fontPrototype))
+ {
+ component.CachedText = Loc.GetString("map-text-font-error");
+ component.Color = Color.Red;
+
+ if(_prototypeManager.TryIndex<FontPrototype>(SharedMapTextComponent.DefaultFont, out var @default))
+ component.CachedFont = new VectorFont(_resourceCache.GetResource<FontResource>(@default.Path), 14);
+ return;
+ }
+
+ var fontResource = _resourceCache.GetResource<FontResource>(fontPrototype.Path);
+ component.CachedFont = new VectorFont(fontResource, component.FontSize);
+ }
+}
--- /dev/null
+using Content.Shared.MapText;
+
+namespace Content.Server.MapText;
+
+[RegisterComponent]
+public sealed partial class MapTextComponent : SharedMapTextComponent;
--- /dev/null
+using Content.Shared.MapText;
+using Robust.Shared.GameStates;
+
+namespace Content.Server.MapText;
+
+/// <inheritdoc/>
+public sealed class MapTextSystem : SharedMapTextSystem
+{
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent<MapTextComponent, ComponentGetState>(GetCompState);
+ }
+
+ private void GetCompState(Entity<MapTextComponent> ent, ref ComponentGetState args)
+ {
+ args.State = new MapTextComponentState
+ {
+ Text = ent.Comp.Text,
+ LocText = ent.Comp.LocText,
+ Color = ent.Comp.Color,
+ FontId = ent.Comp.FontId,
+ FontSize = ent.Comp.FontSize,
+ Offset = ent.Comp.Offset
+ };
+ }
+}
--- /dev/null
+using System.Numerics;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.MapText;
+
+/// <summary>
+/// This is used for displaying text in world space
+/// </summary>
+
+[NetworkedComponent, Access(typeof(SharedMapTextSystem))]
+public abstract partial class SharedMapTextComponent : Component
+{
+ public const string DefaultFont = "Default";
+
+ /// <summary>
+ /// The text to display. This will override <see cref="LocText"/>.
+ /// </summary>
+ [DataField]
+ public string? Text;
+
+ /// <summary>
+ /// The localized-id of the text that should be displayed.
+ /// </summary>
+ [DataField]
+ public LocId LocText = "map-text-default";
+ // TODO VV: LocId editing
+
+ [DataField]
+ public Color Color = Color.White;
+
+ [DataField]
+ public string FontId = DefaultFont;
+
+ [DataField]
+ public int FontSize = 12;
+
+ [DataField]
+ public Vector2 Offset = Vector2.Zero;
+}
+
+[Serializable, NetSerializable]
+public sealed class MapTextComponentState : ComponentState
+{
+ public string? Text { get; init;}
+ public LocId LocText { get; init;}
+ public Color Color { get; init;}
+ public string FontId { get; init; } = default!;
+ public int FontSize { get; init;}
+ public Vector2 Offset { get; init;}
+}
--- /dev/null
+namespace Content.Shared.MapText;
+
+/// <summary>
+/// This handles registering the map text overlay, caching the text font and handling component state
+/// </summary>
+public abstract class SharedMapTextSystem : EntitySystem;
--- /dev/null
+map-text-default = Use VV to change the displayed text
+map-text-font-error = "Error - invalid font"
--- /dev/null
+- type: entity
+ id: MapText
+ parent: MarkerBase
+ name: map text
+ placement:
+ mode: PlaceFree
+ components:
+ - type: MapText
+ - type: Sprite
+ state: pink
+