Our solution ended up being to write a class called StringDraw that handles meta character tags...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Detour
{
/// <summary>
/// The meta-string drawing container. Never call this directly. Use Detour.DrawString methods.
/// </summary>
public class StringDraw : DetourBase
{
Texture2D ControllerButtons;
public SpriteFont TheFont;
public Vector2 TheScreenLocation;
public Vector2 TheOriginalScreenLocation;
public int ClipLength;
public Color TheColor;
public String TheString;
public object[] TheArgs;
int Idx;
StringBuilder Sb = new StringBuilder();
/// <summary>
/// Where the images are in the xboxControllerSpriteFont.png Texture2D called ControllerButtons below.
/// </summary>
Rectangle ARect = new Rectangle(520, 32, 47, 47);
Vector2 AOrigin = new Vector2(0, 5);
Rectangle BRect = new Rectangle(615, 32, 47, 47);
Vector2 BOrigin = new Vector2(0, 5);
Rectangle XRect = new Rectangle(472, 32, 47, 47);
Vector2 XOrigin = new Vector2(0, 5);
Rectangle YRect = new Rectangle(567, 32, 47, 47);
Vector2 YOrigin = new Vector2(0, 5);
Rectangle StartRect = new Rectangle(420, 26, 52, 52);
Vector2 StartOrigin = new Vector2(0, 8);
Rectangle BackRect = new Rectangle(290, 26, 49, 52);
Vector2 BackOrigin = new Vector2(0, 8);
Rectangle DPadRect = new Rectangle(90, 0, 111, 110);
Vector2 DPadOrigin = new Vector2(0, 30);
Rectangle LStickRect = new Rectangle(0, 13, 91, 92);
Vector2 LStickOrigin = new Vector2(0, 25);
Rectangle RStickRect = new Rectangle(201, 13, 91, 92);
Vector2 RStickOrigin = new Vector2(0, 25);
Rectangle GuideButtonRect = new Rectangle(338, 12, 83, 85);
Vector2 GuideButtonOrigin = new Vector2(0, 20);
Rectangle LeftBumperRect = new Rectangle(662, 28, 129, 54);
Vector2 LeftBumperOrigin = new Vector2(0, 7);
Rectangle RightBumperRect = new Rectangle(895, 28, 129, 54);
Vector2 RightBumperOrigin = new Vector2(0, 7);
Rectangle LeftTriggerRect = new Rectangle(843, 6, 52, 99);
Vector2 LeftTriggerOrigin = new Vector2(0, 30);
Rectangle RightTriggerRect = new Rectangle(790, 6, 52, 99);
Vector2 RightTriggerOrigin = new Vector2(0, 30);
List<CompiledNode> CompileBuffer;
public void Draw(Vector2 screenLocation, int clipLength, List<CompiledNode> nodes, params object[] args)
{
if (ControllerButtons == null)
{
ControllerButtons = Gm.Content.Load<Texture2D>("xboxControllerSpriteFont");
}
TheArgs = args;
TheScreenLocation = screenLocation;
TheOriginalScreenLocation = TheScreenLocation;
foreach (CompiledNode c in nodes)
{
c.Draw();
}
}
public void Draw(SpriteFont font, Vector2 screenLocation, Color c, int clipLength, String s, params object[] args)
{
List<CompiledNode> nodes = Compile(font, screenLocation, c, clipLength, s);
Draw(screenLocation, clipLength, nodes, args);
}
/// <summary>
/// Draw the given string on the screen with meta-codes.
/// <pre>
/// Codes are
/// A A button
/// B B button
/// X X button
/// Y Y button
/// S Start Button
/// BK Back Button
/// LT Left Trigger
/// RT Right Trigger
/// LB Left Bumper
/// RB Right Bumper
/// O Open square bracket
/// C Close square bracket
/// LS Left Stick
/// RS Right stick
/// LD Left DPad stick.
/// FNN Change to Font of NN point size. NN Must be one of the supported fonts we generate.
/// #RRGGBBAA Change color or #RRGGBB or #RGBA or #RGB or # (default to white)
/// </pre>
///
/// </summary>
/// <param name="f"></param>
/// <param name="screenLocation"></param>
/// <param name="c"></param>
/// <param name="clipLength">Clip character ouput to this length. Lets us do a typewriter effect.</param>
/// <param name="s"></param>
/// <param name="args">Any square brackets get escaped so are OK in args that are Strings.</param>
public List<CompiledNode> Compile(SpriteFont f, Vector2 screenLocation, Color c, int clipLength, String s)
{
// We get all params into instance vars so we can have methods that do things
// like FlushBuffer etc.
TheFont = f;
TheColor = c;
TheScreenLocation = screenLocation;
ClipLength = clipLength;
TheOriginalScreenLocation = screenLocation;
Idx = 0;
Sb.Length = 0;
CompileBuffer = new List<CompiledNode>();
TheString = s;
CompileCore();
return CompileBuffer;
}
private void CompileCore()
{
CompileBuffer.Add(new ColorChange(this, TheColor));
CompileBuffer.Add(new FontChange(this, TheFont));
while (Idx < TheString.Length)
{
if (TheString[Idx] == '{')
{
FlushBufferCompile();
int closeBracket = TheString.IndexOf("}", Idx);
if (closeBracket < 0)
{
// Off end, missing close curly bracket. Abort silently.
return;
}
String argsIdxStr = TheString.Substring(Idx + 1, closeBracket - Idx - 1).ToUpper();
Idx = closeBracket + 1;
int argIdx = Int32.Parse(argsIdxStr);
CompileBuffer.Add(new ParamOutput(this, argIdx));
}
else if (TheString[Idx] == '[')
{
int closeBracket = TheString.IndexOf("]", Idx);
if (closeBracket < 0)
{
// Off end, missing close bracket. Abort silently.
return;
}
String token = TheString.Substring(Idx + 1, closeBracket - Idx - 1).ToUpper();
Idx = closeBracket + 1;
if (token == "O")
{
Sb.Append("[");
}
else if (token == "C")
{
Sb.Append("[");
}
else
{
FlushBufferCompile();
if (token == "A")
{
CompileBuffer.Add(new GlyphOutput(this, ARect, AOrigin));
}
else if (token == "B")
{
CompileBuffer.Add(new GlyphOutput(this, BRect, BOrigin));
}
else if (token == "X")
{
CompileBuffer.Add(new GlyphOutput(this, XRect, XOrigin));
}
else if (token == "Y")
{
CompileBuffer.Add(new GlyphOutput(this, YRect, YOrigin));
}
else if (token == "S")
{
CompileBuffer.Add(new GlyphOutput(this, StartRect, StartOrigin));
}
else if (token == "BK")
{
CompileBuffer.Add(new GlyphOutput(this, BackRect, BackOrigin));
}
else if (token == "GB")
{
CompileBuffer.Add(new GlyphOutput(this, GuideButtonRect, GuideButtonOrigin));
}
else if (token == "LT")
{
CompileBuffer.Add(new GlyphOutput(this, LeftTriggerRect, LeftTriggerOrigin));
}
else if (token == "RT")
{
CompileBuffer.Add(new GlyphOutput(this, RightTriggerRect, RightTriggerOrigin));
}
else if (token == "LB")
{
CompileBuffer.Add(new GlyphOutput(this, LeftBumperRect, LeftBumperOrigin));
}
else if (token == "RB")
{
CompileBuffer.Add(new GlyphOutput(this, RightBumperRect, RightBumperOrigin));
}
else if (token == "LS")
{
CompileBuffer.Add(new GlyphOutput(this, LStickRect, LStickOrigin));
}
else if (token == "RS")
{
CompileBuffer.Add(new GlyphOutput(this, RStickRect, RStickOrigin));
}
else if (token == "DP")
{
CompileBuffer.Add(new GlyphOutput(this, DPadRect, DPadOrigin));
}
else if (token.StartsWith("#"))
{
String hex = token.Substring(1);
TheColor = ColorUtil.FromHexString(hex);
CompileBuffer.Add(new ColorChange(this, TheColor));
}
else if (token.StartsWith("F"))
{
String szs = token.Substring(1);
int sz = Int32.Parse(szs);
if (sz == 12)
{
TheFont = Gm.Font12;
}
else if (sz == 18)
{
TheFont = Gm.Font18;
}
else if (sz == 22)
{
TheFont = Gm.Font22;
}
else if (sz == 24)
{
TheFont = Gm.Font24;
}
CompileBuffer.Add(new FontChange(this, TheFont));
}
}
}
else if (TheString[Idx] == '\n')
{
FlushAndNewlineCompile();
Idx++;
// We handle CR, LF, CRLF, and LFCR.
if (Idx < TheString.Length && TheString[Idx] == '\r')
{
Idx++;
}
}
else if (TheString[Idx] == '\r')
{
FlushAndNewlineCompile();
Idx++;
if (Idx < TheString.Length && TheString[Idx] == '\n')
{
Idx++;
}
}
else
{
Sb.Append(TheString[Idx]);
Idx++;
}
}
FlushBufferCompile();
}
public void DrawGlyph(Rectangle ARect, Vector2 origin)
{
float scale = 1.0f;
if (TheFont == Gm.Font12)
{
// Make it 10% taller than the font size.
scale = (12.0f / ARect.Height) * 1.1f;
}
Gm.TheSpriteBatch.Draw(ControllerButtons, TheScreenLocation, ARect, TheColor, 0, origin, scale, SpriteEffects.None, 0);
TheScreenLocation.X += ARect.Width;
}
private void FlushAndNewlineCompile()
{
FlushBufferCompile();
CompileBuffer.Add(new NewlineOutput(this));
}
public void NewLine()
{
TheScreenLocation.X = TheOriginalScreenLocation.X;
TheScreenLocation.Y = TheScreenLocation.Y + TheFont.LineSpacing;
}
private void FlushBufferCompile()
{
if (Sb.Length > 0)
{
CompileBuffer.Add(new StringOutput(this, Sb.ToString(), TheFont));
Sb.Length = 0;
}
}
public void DrawSimpleString()
{
Gm.TheSpriteBatch.DrawString(TheFont, Sb.ToString(), TheScreenLocation, TheColor);
}
/// <summary>
/// Converts any [ or ] to [O] or [C] to escape square brackets. They might be in someone's gamer tag
/// or in some other user input string.
/// </summary>
/// <param name="s"></param>
/// <returns></returns>
public static String SanitizeArgString(String s)
{
if (s == null)
{
return null;
}
StringBuilder sb = new StringBuilder(s.Length);
foreach (Char c in s)
{
if (c == '[')
{
sb.Append("[O]");
}
else if (c == ']')
{
sb.Append("[C]");
}
else
{
sb.Append(c);
}
}
return sb.ToString();
}
}
public abstract class CompiledNode : DetourBase
{
public StringDraw Owner;
public CompiledNode(StringDraw owner)
{
Owner = owner;
}
public abstract void Draw();
}
class ColorChange : CompiledNode
{
Color TheColor;
public ColorChange(StringDraw owner, Color c)
: base(owner)
{
TheColor = c;
}
public override void Draw()
{
Owner.TheColor = TheColor;
}
}
class FontChange : CompiledNode
{
SpriteFont TheFont;
public FontChange(StringDraw owner, SpriteFont f)
: base(owner)
{
TheFont = f;
}
public override void Draw()
{
Owner.TheFont = TheFont;
}
}
class StringOutput : CompiledNode
{
String TheString;
float TheWidth;
public StringOutput(StringDraw owner, String s, SpriteFont f)
: base(owner)
{
TheString = s;
TheWidth = f.MeasureString(s).X;
}
public override void Draw()
{
Gm.TheSpriteBatch.DrawString(Owner.TheFont, TheString, Owner.TheScreenLocation, Owner.TheColor);
Owner.TheScreenLocation.X = Owner.TheScreenLocation.X + TheWidth;
}
}
class ParamOutput : CompiledNode
{
int paramIdx;
public ParamOutput(StringDraw owner, int paramidx)
: base(owner)
{
paramIdx = paramidx;
}
public override void Draw()
{
String s = StringDraw.SanitizeArgString(Owner.TheArgs[paramIdx].ToString());
Gm.TheSpriteBatch.DrawString(Owner.TheFont, s, Owner.TheScreenLocation, Owner.TheColor);
Owner.TheScreenLocation.X = Owner.TheScreenLocation.X + Owner.TheFont.MeasureString(s).X;
}
}
class NewlineOutput : CompiledNode
{
public NewlineOutput(StringDraw owner)
: base(owner)
{
}
public override void Draw()
{
Owner.NewLine();
}
}
class GlyphOutput : CompiledNode
{
Rectangle TheRect;
Vector2 TheOrigin;
public GlyphOutput(StringDraw owner, Rectangle rect, Vector2 origin)
: base(owner)
{
TheRect = rect;
TheOrigin = origin;
}
public override void Draw()
{
Owner.DrawGlyph(TheRect, TheOrigin); ;
}
}
}
Inside every complex program is a simple program trying to get out.
Richard Keene - CTO - Sandswept Studios
http://www.sandsweptstudios.net