// LEDboard.cs // An LED sign drawn on a script canvas: every cell is a character from an LED sprite sheet, in one of 10 colours // (0-9) and 10 glow levels (0-9). Programs paint text, sprites, shapes, borders and waves, and animate with for() // loops; see the LEDboard example page for the whole program language. Programs come from the Programs list (shown // in turn), from script events, or from moderators in chat: /ledeval , /ledstop, /ledreset, /board . using IHXR.Simulation; public class LEDboard : ScriptBase { protected override bool SyncTransformStateByDefault => false; [Tooltip("The sign (its picture is drawn on BoardMaterial). Empty: this object.")] public SceneObject Board; [Tooltip("Which material of the sign shows the LEDs (0 = the first).")] public int BoardMaterial = 0; [Tooltip("See-through background: the sign shows through wherever the LED pictures are see-through.")] public bool TransparentBackground = false; [Tooltip("LED cells across and down (32 x 8 is one classic panel, 64 x 16 is four).")] public int Columns = 32; public int Rows = 8; [Tooltip("Pixels per cell on the picture (made smaller when the board would be wider than 1024 pixels).")] public int CellWidth = 24; public int CellHeight = 32; [Tooltip("The LED character sprite sheet (all characters in one row, in the order of Characters).")] public TextureItem CharacterSheet; [Tooltip("Frames across and down in the sprite sheet.")] public int SheetColumns = 90; public int SheetRows = 1; [Tooltip("The character each frame of the sheet shows, in order (frame 0 first). Empty: the standard LED characters.")] public string Characters = ""; [Tooltip("Colour 0 to 9, as used by programs.")] public Color Colour0 = new Color(1f, 1f, 1f); public Color Colour1 = new Color(1f, 0f, 0f); public Color Colour2 = new Color(1f, 0.5f, 0f); public Color Colour3 = new Color(1f, 1f, 0f); public Color Colour4 = new Color(0f, 1f, 0f); public Color Colour5 = new Color(0f, 0f, 1f); public Color Colour6 = new Color(0.5f, 0.2f, 0.9f); public Color Colour7 = new Color(0f, 1f, 1f); public Color Colour8 = new Color(1f, 0.1f, 0.55f); public Color Colour9 = new Color(0.29f, 1f, 0.4f); [Tooltip("How brightly the whole board glows (0 = not at all, up to 10). Each cell's glow level 0-9 dims or brightens it.")] public float Glow = 2f; [Tooltip("Pause after every for() loop step, in seconds (on top of wait()).")] public float FrameDelay = 0.05f; [Tooltip("Board updates sent to everyone per second (1 to 20); updates that change nothing are not sent.")] public int UpdatesPerSecond = 20; [Tooltip("Programs shown in turn (AutoSwitchPrograms), or started by the matching ProgramEvents.")] public List Programs = new List { "background(stars),for(1,100,1,border(solid, \u2591\u2592\u2593\u2588\u2593\u2592\u2591, {r=1,9}, 9, true),wait(0.1))", "for(1,250,1,ti={ti}+({i} * 0.02) * (1 - {i}/100) + sin({i} * 0.05) * 2,sinewave(5, {i}*6, {ti}+4, X, {r=1,9}, 8,8))", "clear(),for(1,350,1,ti={ti}+({i} * 0.02) * (1 - {i}/50) + sin({i} * 0.05) * 2, sinewave(6, {i}*2, {ti}+3, *, {r=1,9}, {r=1,9},2)),wipe(vertical)", "spritedefine(diamond1, 9, 9,\" X X X X X X X X X X X X \"),spritedefine(diamond2, 9, 9,\" X X X X X X X X \"),spritedefine(diamond3, 9, 9,\" X X X X \"),clear(),for(1,50,1,newc = {rc}%10,newx = {rw}%50,newy = {rw}%10,for(1,1,1,paintsprite(diamond1,{newx},{newy},{newc},2,false)),for(1,1,1,paintsprite(diamond2,{newx},{newy},{newc},4,false)),for(1,1,1,paintsprite(diamond3,{newx},{newy},{newc},9,false))),clear()", "spritedefine(rotline1, 7, 7,\" * * * * * * * \"),spritedefine(rotline2, 7, 7,\"* * * * * * *\"),spritedefine(rotline3, 7, 7,\" ******* \"),spritedefine(rotline4, 7, 7,\" * * * * * * * \"),for(1,10,1,for(1,4,1,paintsprite(rotline{i},16,0,{r=0,9},9),wait(0.02))),clear()", "clear(),for(1,10,1,border(solid, \u2591\u2592\u2593\u2588\u2593\u2592\u2591, 4, 9,true),textdraw(HELLO,1,{i},0,9),wait(0.1)),for(1,15,1,border(solid, \u2591\u2592\u2593\u2588\u2593\u2592\u2591, 4, 9,true),textdraw(HELLO,2,10,2,9),wait(0.1)),for(1,100,1,border(solid, \u2591H\u2592E\u2593L\u2588L\u2593O\u2592\u2591, 4, 9,true),textdraw(HELLO,1,2,3,9),wait(0.1)),wipe(horizontal)", "background(stars),for(0,6,1,textdraw(WOW,2,{i} * 17,2,{r=1,9},9),wait(0.1)),for(1,8,1,border(solid, \u2591\u2592\u2593\u2588\u2593\u2592\u2591, {r=1,9}, 9, true),wait(0.1)),for(0,6,1,textdraw(WOW,2,{i} * 17,9,{r=1,9},9),wait(0.1)),for(1,8,1,border(solid, \u2591\u2592\u2593\u2588\u2593\u2592\u2591, {r=1,9}, 9, true),wait(0.1)),wipe(vertical)", "clear(),background(stars),for(1,50,1,border(solid,~$.DANCE.$~,{r=0,9},1,true,true),for(1, 4, 1,shape(rectangle,2,12,2,{r=0,7},\u2591,{r=0,9},9),shape(rectangle,2,12,{i}*15,{r=0,7},\u2591,{r=0,9},9)),wait(0.05)),wipe(horizontal)", "for(1,25,1,border(solid, XYZ, {r=1,9}, 9, true),wait(0.02)),for(1,300,1,shape(circle,3+{rw},3+{rh},{rw},{rh},X,{r=1,9},9), wait(0.02)),clear()" }; public bool AutoSwitchPrograms = true; [Tooltip("Seconds each program gets before the next one starts.")] public float AutoSwitchSeconds = 10f; [Tooltip("Script events that start a program: ProgramEvents[n] starts Programs[n].")] public List ProgramEvents = new List(); [Tooltip("Shown when a player arrives (if UseOnJoin is on); {lastuserjoined} is their name.")] public string OnJoinProgram = "clear(),for(1,40,1,border(solid, \u2591\u2592\u2593\u2588\u2593\u2592\u2591, {r=1,9}, 9, true),textdraw(HI,1,2,1,{r=1,9},9),textdraw({lastuserjoined},1,2,4,0,9),wait(0.1)),wipe(horizontal)"; public bool UseOnJoin = true; [Tooltip("Usernames allowed to use the chat commands (/ledeval, /ledstop, /ledreset, /board, ...).")] public List Moderators = new List(); [Tooltip("Writes program errors to the script log.")] public bool DebugMode = false; // ---------------------------------------------------------------- the board private const string LedCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()[];:',./?|-_=+~\u00A9\u00AE\u2593\u2592\u2591\u2588\u25B2\u25BC\u25C4\u2660\u2663\u2665\u2666\u266A\u266B\u25CA\u263A\u00A7 \u058D\u2E19\u263C\u203B\u221E\u038F\u2116\u2051\u04CD\\<>{}\u25BA"; private ScriptCanvas canvas; private CanvasTileGrid grid; private int width, height; private char[] cells; private int[] cellColour, cellGlow; private Color[] palette; private Coroutine running, switcher; private int programIndex = -1; private string lastUserJoined = ""; private readonly List usersJoined = new List(); public override void Start() { width = Mathf.Clamp(Columns, 1, 256); height = Mathf.Clamp(Rows, 1, 256); int cw = Mathf.Max(2, CellWidth), ch = Mathf.Max(2, CellHeight); float fit = Mathf.Min(1f, Mathf.Min(1024f / (width * cw), 1024f / (height * ch))); cw = Mathf.Max(1, Mathf.FloorToInt(cw * fit)); ch = Mathf.Max(1, Mathf.FloorToInt(ch * fit)); // Empty (or not readable): the standard set, which matches the LED sprite sheet. if (string.IsNullOrEmpty(Characters) || Characters.StartsWith("\"") || Characters.Contains("\\u")) Characters = LedCharacters; palette = new[] { Colour0, Colour1, Colour2, Colour3, Colour4, Colour5, Colour6, Colour7, Colour8, Colour9 }; cells = new char[width * height]; cellColour = new int[width * height]; cellGlow = new int[width * height]; canvas = IsValid(Board) ? CreateCanvas(Board, BoardMaterial, width * cw, height * ch, Color.black, transparentBackground: TransparentBackground) : CreateCanvas(BoardMaterial, width * cw, height * ch, Color.black, transparentBackground: TransparentBackground); canvas.SetUpdateRate(UpdatesPerSecond).SetEmission(Glow); grid = canvas.CreateTileGrid(CharacterSheet, SheetColumns, SheetRows, width, height, cw, ch); if (CharacterSheet == null || CharacterSheet.IsNull) LogWarning("LEDboard: put the LED character sprite sheet in CharacterSheet."); ClearBoard(); for (int i = 0; i < ProgramEvents.Count && i < Programs.Count; i++) { int index = i; if (!string.IsNullOrWhiteSpace(ProgramEvents[i])) ListenForScriptEvent(ProgramEvents[i].Trim(), (ScriptEventData data) => Run(Programs[index])); } RegisterChatCommand(OnChat); if (AutoSwitchPrograms) switcher = StartCoroutine(AutoSwitch()); } private bool IsValid(SceneObject o) => o != null && !o.IsNull; private void Trace(string message) { if (DebugMode) LogWarning("LEDboard: " + message); } // One cell: a character (from Characters), colour 0-9 and glow 0-9. private void SetCell(int x, int y, char c, int colour, int glow) { if (x < 0 || y < 0 || x >= width || y >= height) return; int frame = (Characters ?? "").IndexOf(c); if (frame < 0) return; int i = y * width + x; colour = Mathf.Clamp(colour, 0, 9); glow = Mathf.Clamp(glow, 0, 9); cells[i] = c; cellColour[i] = colour; cellGlow[i] = glow; Color p = palette[colour]; float bright = 0.45f + 0.55f * glow / 9f; grid.Set(x, y, frame, new Color(p.r * bright, p.g * bright, p.b * bright, 1f)); } private void ClearBoard() { for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) SetCell(x, y, ' ', 0, 1); } // Text with colour and glow codes: inverted ? + digit sets the colour of the next character, inverted ! + digit // its glow. Cleanslate: spaces are drawn too (otherwise they leave the cell as it is). private void SetDisplayText(string text, bool cleanslate) { int colour = 0, glow = 1, cell = 0; bool hasColour = false, hasGlow = false; for (int i = 0; i < text.Length && cell < width * height; i++) { char c = text[i]; if ((c == '\u00BF' || c == '\u00A1') && i + 1 < text.Length && char.IsDigit(text[i + 1])) { if (c == '\u00BF') { colour = text[i + 1] - '0'; hasColour = true; } else { glow = text[i + 1] - '0'; hasGlow = true; } i++; continue; } if (cleanslate || c != ' ') SetCell(cell % width, cell / width, c, hasColour ? colour : 0, hasGlow ? glow : 1); hasColour = hasGlow = false; cell++; } } // ---------------------------------------------------------------- running programs // Starts a program, stopping the one that was running. private void Run(string program) { if (string.IsNullOrWhiteSpace(program)) return; if (running != null) StopCoroutine(running); halt = false; running = StartCoroutine(Execute(program)); } private void Stop() { if (running != null) StopCoroutine(running); running = null; } private System.Collections.IEnumerator AutoSwitch() { yield return new WaitForSeconds(1f); while (AutoSwitchPrograms) { if (Programs.Count > 0) { programIndex = (programIndex + 1) % Programs.Count; Run(Programs[programIndex]); } yield return new WaitForSeconds(Mathf.Max(1f, AutoSwitchSeconds)); } switcher = null; } private System.Collections.IEnumerator Execute(string program) { System.Collections.IEnumerator steps = RunList(SplitTop(program)); while (true) { object step; try { if (!steps.MoveNext()) break; step = steps.Current; } catch (System.Exception error) { Trace("program stopped: " + error.Message); break; } yield return step; } running = null; } private System.Collections.IEnumerator RunList(List instructions) { foreach (string instruction in instructions) { if (halt) yield break; System.Collections.IEnumerator steps = RunCall(instruction); while (steps.MoveNext()) yield return steps.Current; } } public override void OnUserJoin(UserInfo user) { string name = (user?.Username ?? "").Trim(); if (name.Length == 0) return; lastUserJoined = name; if (!usersJoined.Contains(name)) usersJoined.Add(name); if (UseOnJoin && !string.IsNullOrWhiteSpace(OnJoinProgram)) Run(OnJoinProgram); } // ---------------------------------------------------------------- chat private bool IsModerator(string username) { foreach (string m in Moderators) if (string.Equals((m ?? "").Trim(), (username ?? "").Trim(), System.StringComparison.OrdinalIgnoreCase)) return true; return false; } private void OnChat(ChatCommandInfo info) { string command = (info.Command ?? "").ToLowerInvariant(); if (!command.StartsWith("/led") && command != "/board" && command != "/addmod" && command != "/removemod") return; if (!IsModerator(info.Username)) return; string full = info.FullText ?? ""; int space = full.IndexOf(' '); string rest = space >= 0 ? full.Substring(space + 1).Trim() : ""; string arg = rest.ToLowerInvariant(); switch (command) { case "/ledeval": Run(rest); break; case "/ledstop": Stop(); ClearBoard(); break; case "/ledreset": ClearBoard(); break; case "/board": if (rest.Length > 0) SetDisplayText(rest.ToUpperInvariant(), true); break; case "/ledframerate": { float value; if (float.TryParse(rest, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out value)) FrameDelay = Mathf.Clamp(value, 0f, 5f); break; } case "/ledautoswitch": AutoSwitchPrograms = arg == "on"; if (AutoSwitchPrograms && switcher == null) switcher = StartCoroutine(AutoSwitch()); if (!AutoSwitchPrograms && switcher != null) { StopCoroutine(switcher); switcher = null; } break; case "/ledonjoin": UseOnJoin = arg == "on"; break; case "/leddebug": DebugMode = arg == "on"; break; case "/addmod": if (rest.Length > 0 && !IsModerator(rest)) Moderators.Add(rest); break; case "/removemod": Moderators.RemoveAll(m => string.Equals((m ?? "").Trim(), rest, System.StringComparison.OrdinalIgnoreCase)); break; } } // ---------------------------------------------------------------- the program language private readonly Dictionary vars = new Dictionary(); private readonly Dictionary spritePatterns = new Dictionary(); private readonly Dictionary spriteWidths = new Dictionary(), spriteHeights = new Dictionary(); private readonly Dictionary sinewaveFrames = new Dictionary(); private int borderRotation, replaceIndex; private bool halt; // Splits "a(1,2), b, c(\"x,y\")" at the commas outside brackets and quotes. private static List SplitTop(string text) { List parts = new List(); System.Text.StringBuilder current = new System.Text.StringBuilder(); int depth = 0; bool quoted = false; foreach (char c in text ?? "") { if (c == '\r' || c == '\n') { if (quoted) continue; current.Append(' '); continue; } if (c == '"') quoted = !quoted; else if (!quoted && c == '(') depth++; else if (!quoted && c == ')') depth = Mathf.Max(0, depth - 1); if (c == ',' && depth == 0 && !quoted) { parts.Add(current.ToString().Trim()); current.Clear(); } else current.Append(c); } if (current.ToString().Trim().Length > 0) parts.Add(current.ToString().Trim()); return parts; } private static string Unquote(string s) { s = (s ?? "").Trim(); if (s.Length >= 2 && s[0] == '"' && s[s.Length - 1] == '"') return s.Substring(1, s.Length - 2); return s; } private static string Format(double v) { if (System.Math.Abs(v - System.Math.Round(v)) < 1e-9) return ((long)System.Math.Round(v)).ToString(System.Globalization.CultureInfo.InvariantCulture); return v.ToString("0.######", System.Globalization.CultureInfo.InvariantCulture); } private int RandomInt(int min, int max) => min + Mathf.Min(max - min, Mathf.FloorToInt(Random.value * (max - min + 1))); // {i} and other variables, {r=min,max} (a random whole number), {rw} / {rh} (a random column / row), {rc} (a random // colour), {lastuserjoined}. Unknown names stay as they are (they count as 0 in sums). private string Substitute(string text) { System.Text.StringBuilder output = new System.Text.StringBuilder(); int i = 0; while (i < text.Length) { int end = text[i] == '{' ? text.IndexOf('}', i) : -1; if (end < 0) { output.Append(text[i]); i++; continue; } string name = text.Substring(i + 1, end - i - 1).Trim(); string lower = name.ToLowerInvariant(); double value; if (lower.StartsWith("r=")) { string[] range = name.Substring(2).Split(','); int a = 0, b = 100; if (range.Length == 2) { int.TryParse(range[0].Trim(), out a); int.TryParse(range[1].Trim(), out b); } output.Append(RandomInt(Mathf.Min(a, b), Mathf.Max(a, b))); } else if (lower == "rw") output.Append(RandomInt(0, width - 1)); else if (lower == "rh") output.Append(RandomInt(0, height - 1)); else if (lower == "rc") output.Append(RandomInt(0, 8)); else if (lower == "lastuserjoined") output.Append(lastUserJoined.ToUpperInvariant()); else if (vars.TryGetValue(name, out value)) output.Append(Format(value)); else output.Append(text, i, end - i + 1); i = end + 1; } return output.ToString(); } private double Number(string expression) => new Expression(expression).Value(); private int Int(string expression) => (int)System.Math.Truncate(Number(expression)); private int IntArg(string[] a, int index, int fallback) => a.Length > index && a[index].Trim().Length > 0 ? Int(a[index]) : fallback; private static bool BoolArg(string[] a, int index, bool fallback) => a.Length > index && a[index].Trim().Length > 0 ? a[index].Trim().ToLowerInvariant() == "true" : fallback; private static char CharArg(string[] a, int index, char fallback) { string s = a.Length > index ? Unquote(a[index]) : ""; return s.Length > 0 ? s[0] : fallback; } // One instruction: a variable (name = expression), a for() loop, or a drawing call. private System.Collections.IEnumerator RunCall(string instruction) { instruction = (instruction ?? "").Trim(); if (instruction.Length == 0) yield break; int open = instruction.IndexOf('('), equals = instruction.IndexOf('='); if (equals > 0 && (open < 0 || equals < open) && IsName(instruction.Substring(0, equals).Trim())) { string name = instruction.Substring(0, equals).Trim(); if (name.ToLowerInvariant() != "r") { try { vars[name] = Number(Substitute(instruction.Substring(equals + 1))); } catch (System.Exception e) { Trace(e.Message); } } yield break; } int close = instruction.LastIndexOf(')'); if (open <= 0 || close < open) { Trace("not understood: " + instruction); yield break; } string func = instruction.Substring(0, open).Trim().ToLowerInvariant(); string inside = instruction.Substring(open + 1, close - open - 1); if (func == "for") { System.Collections.IEnumerator loop = ForLoop(SplitTop(inside)); while (loop.MoveNext()) yield return loop.Current; yield break; } string[] a = SplitTop(Substitute(inside)).ToArray(); if (func == "wait") { double t = a.Length > 0 ? Number(a[0]) : 1; yield return new WaitForSeconds((float)System.Math.Max(0.001, t)); yield break; } if (func == "wipe") { bool vertical = a.Length > 0 && a[0].Trim().ToLowerInvariant() == "vertical"; int lines = vertical ? height : width; for (int n = 0; n < lines; n++) { if (vertical) for (int x = 0; x < width; x++) SetCell(x, n, ' ', 1, 1); else for (int y = 0; y < height; y++) SetCell(n, y, ' ', 1, 1); yield return new WaitForSeconds(0.02f); } yield break; } if (func == "fade") { for (int level = 3; level >= 0; level--) { for (int i = 0; i < cells.Length; i++) SetCell(i % width, i / width, cells[i], cellColour[i], Mathf.Min(cellGlow[i], level)); yield return new WaitForSeconds(0.1f); } yield return new WaitForSeconds(0.25f); ClearBoard(); yield break; } if (func == "stop") { ClearBoard(); halt = true; yield break; } try { Draw(func, a); } catch (System.Exception e) { Trace(func + "(): " + e.Message); } } private static bool IsName(string s) { if (s.Length == 0 || !(char.IsLetter(s[0]) || s[0] == '_')) return false; foreach (char c in s) if (!char.IsLetterOrDigit(c) && c != '_') return false; return true; } // for(start, end, step, instructions...) or for(name, start, end, step, instructions...): runs the instructions // with {i} (or {name}) counting from start to end; FrameDelay seconds after every step. private System.Collections.IEnumerator ForLoop(List a) { string name = "i"; int offset = 0; if (a.Count > 0 && IsName(a[0].Trim())) { name = a[0].Trim(); offset = 1; } int from = a.Count > offset ? Int(Substitute(a[offset])) : 0; int to = a.Count > offset + 1 ? Int(Substitute(a[offset + 1])) : 10; int step = a.Count > offset + 2 ? Int(Substitute(a[offset + 2])) : 1; if (step == 0) step = 1; List body = a.Count > offset + 3 ? a.GetRange(offset + 3, a.Count - offset - 3) : new List(); double saved; bool hadSaved = vars.TryGetValue(name, out saved); int guard = 0; for (int i = from; step > 0 ? i <= to : i >= to; i += step) { if (++guard > 100000 || halt) break; vars[name] = i; foreach (string instruction in body) { if (halt) break; System.Collections.IEnumerator steps = RunCall(instruction); while (steps.MoveNext()) yield return steps.Current; vars[name] = i; // an inner loop with the same name ends: this loop's value again } yield return new WaitForSeconds(Mathf.Max(0.001f, FrameDelay)); } if (hadSaved) vars[name] = saved; else vars.Remove(name); } // The drawing calls (their arguments already have {variables} filled in). private void Draw(string func, string[] a) { switch (func) { case "clear": ClearBoard(); break; case "background": if (a.Length > 0 && a[0].Trim().ToLowerInvariant() == "stars") { for (int y = 0; y < height; y++) for (int x = 0; x < width; x++) SetCell(x, y, '.', 0, 1); } else ClearBoard(); break; case "textdraw": TextDraw(a); break; case "shape": Shape(a[0].Trim().ToLowerInvariant(), IntArg(a, 1, 5), IntArg(a, 2, 5), IntArg(a, 3, 0), IntArg(a, 4, 0), CharArg(a, 5, '*'), IntArg(a, 6, 1), IntArg(a, 7, 1)); break; case "border": Border(a.Length > 1 ? a[1].Trim() : "#", IntArg(a, 2, 0), IntArg(a, 3, 5), BoolArg(a, 4, true)); break; case "sinewave": Sinewave(a); break; case "spritedefine": if (a.Length >= 4) { string n = Unquote(a[0]); spritePatterns[n] = Unquote(a[3]); spriteWidths[n] = Mathf.Max(1, Int(a[1])); spriteHeights[n] = Mathf.Max(1, Int(a[2])); } break; case "paintsprite": if (a.Length >= 5) PaintSprite(Unquote(a[0]), Int(a[1]), Int(a[2]), Int(a[3]), Int(a[4]), BoolArg(a, 5, true), a.Length > 6 ? a[6].Trim() : "", IntArg(a, 7, 0)); break; case "joineduserslist": UsersList(IntArg(a, 0, width), IntArg(a, 1, height), IntArg(a, 2, 0), IntArg(a, 3, 0), IntArg(a, 4, 0), IntArg(a, 5, 1), BoolArg(a, 6, true)); break; case "bargraph": break; // not available default: Trace("unknown function: " + func); break; } } // textdraw(text, size, x, y, colour, glow); size 1 = one cell per character, 2 = 5 rows high, 4 = 8 rows high. // textdraw(text, x, y, size, colour, glow) works too, when only its fourth value is a size. private void TextDraw(string[] a) { string text = (a.Length > 0 ? Unquote(a[0]) : " ").ToUpperInvariant(); int v1 = IntArg(a, 1, 1), v2 = IntArg(a, 2, 0), v3 = IntArg(a, 3, 0); bool sizeFirst = IsSize(v1) || !IsSize(v3); int size = sizeFirst ? v1 : v3, x = sizeFirst ? v2 : v1, y = sizeFirst ? v3 : v2; DrawText(text, x, y, size, IntArg(a, 4, 0), IntArg(a, 5, 1)); } private static bool IsSize(int v) => v == 1 || v == 2 || v == 4; private void DrawText(string text, int x0, int y0, int size, int colour, int glow) { if (size != 2 && size != 4) { for (int i = 0; i < text.Length; i++) if (text[i] != ' ') SetCell(x0 + i, y0, text[i], colour, glow); return; } string[] font = size == 2 ? Font2 : Font4; int advance = size == 2 ? 6 : 9; for (int i = 0; i < text.Length; i++) { string[] rows = Glyph(font, text[i]); for (int r = 0; r < rows.Length; r++) for (int c = 0; c < rows[r].Length; c++) if (rows[r][c] == '#') SetCell(x0 + i * advance + c, y0 + r, '\u2588', colour, glow); } } private static string[] Glyph(string[] font, char c) { string space = null; for (int i = 0; i + 1 < font.Length; i += 2) { if (font[i][0] == c) return font[i + 1].Split('|'); if (font[i][0] == ' ') space = font[i + 1]; } return (space ?? "").Split('|'); } // shape(rectangle | circle | splatter | triangle, width, height, x, y, fill character, colour, glow) private void Shape(string type, int w, int h, int x0, int y0, char fill, int colour, int glow) { for (int y = 0; y < h; y++) { if (type == "triangle") { int row = (int)((float)y / Mathf.Max(1, h) * w), start = x0 + w / 2 - row / 2; for (int x = 0; x <= row; x++) SetCell(start + x, y0 + y, fill, colour, glow); continue; } for (int x = 0; x < w; x++) { bool inside; if (type == "circle") { float rx = w / 2f, ry = h / 2f, dx = (x - rx) / rx, dy = (y - ry) / ry; inside = dx * dx + dy * dy <= 1f; } else if (type == "splatter") { float r = w / 2f, dx = x - r, dy = y - r; inside = dx * dx + dy * dy <= r * r; } else inside = type == "rectangle"; if (inside) SetCell(x0 + x, y0 + y, fill, colour, glow); } } } // border(solid, characters, colour, glow, animated): the characters run round the edge (and move along each call). private void Border(string text, int colour, int glow, bool animated) { if (string.IsNullOrEmpty(text)) text = "#"; if (animated && text.Length > 1) { borderRotation = (borderRotation + 1) % text.Length; text = text.Substring(borderRotation) + text.Substring(0, borderRotation); } for (int y = 0; y < height; y++) { if (y == 0 || y == height - 1) { for (int x = 0; x < width; x++) SetCell(x, y, text[x % text.Length], colour, glow); } else { SetCell(0, y, text[y % text.Length], colour, glow); SetCell(width - 1, y, text[(y + width) % text.Length], colour, glow); } } } // sinewave(amplitude, frequency, speed, fill character, colour, glow, thickness): fills the board with a wave // (and clears the rest); the same call again moves it along. private void Sinewave(string[] a) { double amp = a.Length > 0 ? Number(a[0]) : 1, freq = a.Length > 1 ? Number(a[1]) : 0, speed = a.Length > 2 ? Number(a[2]) : 0; char fill = CharArg(a, 3, '#'); int colour = IntArg(a, 4, 0), glow = IntArg(a, 5, 1), thick = Mathf.Max(1, IntArg(a, 6, 1)); string key = string.Join(",", a).ToLowerInvariant(); int frame; sinewaveFrames.TryGetValue(key, out frame); if (sinewaveFrames.Count > 500) sinewaveFrames.Clear(); sinewaveFrames[key] = frame + 1; for (int x = 0; x < width; x++) { int centre = (int)(height / 2 + System.Math.Sin(x * freq + frame * speed * 0.01) * amp); for (int y = 0; y < height; y++) { bool on = y >= centre - thick / 2 && y <= centre + thick / 2; SetCell(x, y, on ? fill : ' ', on ? colour : 0, on ? glow : 0); } } } // paintsprite(name, x, y, colour, glow, transparent, replace characters, rotation degrees) private void PaintSprite(string name, int x0, int y0, int colour, int glow, bool transparent, string replace, int rotation) { string pattern; int w, h; if (!spritePatterns.TryGetValue(name, out pattern) || !spriteWidths.TryGetValue(name, out w) || !spriteHeights.TryGetValue(name, out h)) { Trace("no sprite called " + name); return; } float angle = rotation * Mathf.PI / 180f, cos = Mathf.Cos(angle), sin = Mathf.Sin(angle), cx = w / 2f, cy = h / 2f; for (int sy = 0; sy < h; sy++) for (int sx = 0; sx < w; sx++) { int index = sy * w + sx; if (index >= pattern.Length) return; char c = pattern[index]; if (transparent && c == ' ') continue; float dx = sx - cx, dy = sy - cy; int tx = (int)(x0 + cx + dx * cos - dy * sin), ty = (int)(y0 + cy + dx * sin + dy * cos); if (replace.Length > 0 && c != ' ') { c = replace[replaceIndex % replace.Length]; replaceIndex = (replaceIndex + 1) % 1000000; } SetCell(tx, ty, c, colour, glow); } } // joineduserslist(width, height, x, y, colour, glow, newest first): the names of players who came, in columns. private void UsersList(int w, int h, int x0, int y0, int colour, int glow, bool newestFirst) { int perRow = Mathf.Max(1, w / 8), shown = Mathf.Min(usersJoined.Count, perRow * h); for (int n = 0; n < shown; n++) { string user = usersJoined[newestFirst ? usersJoined.Count - 1 - n : n].ToUpperInvariant(); int x = x0 + (n % perRow) * 10, y = y0 + n / perRow; for (int c = 0; c < user.Length; c++) SetCell(x + c, y, user[c], colour, glow); } } // ---------------------------------------------------------------- sums: + - * / % ( ) and sin cos tan sqrt abs // log log10 floor ceil round pow(a, b) min(a, b) max(a, b) pi private sealed class Expression { private readonly string s; private int p; public Expression(string text) { s = text ?? ""; } public double Value() { p = 0; double v = Sum(); return double.IsNaN(v) || double.IsInfinity(v) ? 0 : v; } private void Skip() { while (p < s.Length && char.IsWhiteSpace(s[p])) p++; } private bool Take(char c) { Skip(); if (p < s.Length && s[p] == c) { p++; return true; } return false; } private double Sum() { double v = Product(); while (true) { if (Take('+')) v += Product(); else if (Take('-')) v -= Product(); else return v; } } private double Product() { double v = Unary(); while (true) { if (Take('*')) v *= Unary(); else if (Take('/')) { double d = Unary(); v = d == 0 ? 0 : v / d; } else if (Take('%')) { double d = Unary(); v = d == 0 ? 0 : v % d; } else return v; } } private double Unary() { if (Take('-')) return -Unary(); if (Take('+')) return Unary(); return Atom(); } private double Atom() { Skip(); if (p >= s.Length) return 0; if (Take('(')) { double v = Sum(); Take(')'); return v; } if (s[p] == '{') { int end = s.IndexOf('}', p); p = end < 0 ? s.Length : end + 1; return 0; } // an unknown {name} if (char.IsDigit(s[p]) || s[p] == '.') { int start = p; while (p < s.Length && (char.IsDigit(s[p]) || s[p] == '.')) p++; if (p < s.Length && (s[p] == 'e' || s[p] == 'E') && p + 1 < s.Length && (char.IsDigit(s[p + 1]) || s[p + 1] == '-' || s[p + 1] == '+')) { p += 2; while (p < s.Length && char.IsDigit(s[p])) p++; } double v; double.TryParse(s.Substring(start, p - start), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out v); return v; } if (char.IsLetter(s[p])) { int start = p; while (p < s.Length && char.IsLetterOrDigit(s[p])) p++; string name = s.Substring(start, p - start).ToLowerInvariant(); if (name == "pi") return System.Math.PI; List args = new List(); if (Take('(')) { if (!Take(')')) { do args.Add(Sum()); while (Take(',')); Take(')'); } } double x = args.Count > 0 ? args[0] : 0, y = args.Count > 1 ? args[1] : 0; switch (name) { case "sin": return System.Math.Sin(x); case "cos": return System.Math.Cos(x); case "tan": return System.Math.Tan(x); case "sqrt": return x < 0 ? 0 : System.Math.Sqrt(x); case "abs": return System.Math.Abs(x); case "log": return x <= 0 ? 0 : System.Math.Log(x); case "log10": return x <= 0 ? 0 : System.Math.Log10(x); case "floor": return System.Math.Floor(x); case "ceil": return System.Math.Ceiling(x); case "round": return System.Math.Round(x); case "pow": return System.Math.Pow(x, y); case "min": return System.Math.Min(x, y); case "max": return System.Math.Max(x, y); default: return 0; } } p++; // anything else is skipped return Atom(); } } // ---------------------------------------------------------------- fonts (# = a lit cell, | = next row) private static readonly string[] Font2 = { "A", " # | # # |#####|# #|# #", "B", "#### |# #|#### |# #|#### ", "C", " ### |# |# |# | ### ", "D", "#### |# #|# #|# #|#### ", "E", "#####|# |#### |# |#####", "F", "#####|# |#### |# |# ", "G", " ### |# |# ##|# #| ### ", "H", "# #|# #|#####|# #|# #", "I", " # | # | # | # | # ", "J", " # | # | # |# # | ### ", "K", "# #|# # |### |# # |# #", "L", "# |# |# |# |#####", "M", "# #|## ##|# # #|# #|# #", "N", "# #|## #|# # #|# ##|# #", "O", " ### |# #|# #|# #| ### ", "P", "#### |# #|#### |# |# ", "Q", " ### |# #|# #|# ##| ####", "R", "#### |# #|#### |# # |# #", "S", " ### |# | ### | #| ### ", "T", "#####| # | # | # | # ", "U", "# #|# #|# #|# #| ### ", "V", "# #|# #|# #| # # | # ", "W", "# #|# #|# # #|## ##|# #", "X", "# #| # # | # | # # |# #", "Y", "# #| # # | # | # | # ", "Z", "#####| #| # | # |#####", "0", "#####|# #|# #|# #|#####", "1", " # | ## | # | # |#####", "2", "#####| #|#####|# |#####", "3", "#####| #|#####| #|#####", "4", "# #|# #|#####| #| #", "5", "#####|# |#####| #|#####", "6", "#####|# |#####|# #|#####", "7", "#####| #| #| #| #", "8", "#####|# #|#####|# #|#####", "9", "#####|# #|#####| #|#####", "!", " # | # | # | | # ", "@", " ##### |# # |# # # |# # | ##### ", "#", "# #|#####|# #|#####|# #", "$", " ### |# #| ##### | # | #### ", "%", "# #|# #| # | # |# #", "^", " # | # # |#####| | ", "&", " #####|# #| ##### |# # #| ##### ", "*", " # # | # # |#####| # # | # # ", "(", " # | # | # | # | # ", ")", " # | # | # | # | # ", "-", " |#####| | | ", "=", " |#####| |#####| ", "+", " # | # |#####| # | # ", "[", "#### |# |# |# |#### ", "]", " ####| #| #| #| ####", "{", " #### |# |# |# | #### ", "}", " #### | #| #| #| #### ", "|", " # | # | # | # | # ", "\\", "# | # | # | # | #", ";", " # | # | # | # | # ", ":", " # | # | # | # | ", "'", " # | # | # | | ", "\"", " # # | # # | | | ", ",", " | | | # | # ", ".", " | | | # | # ", "<", " #| # | # | # | # ", ">", " # | # | # | # | #", "/", " # | # | # | # |# ", "?", " ### |# #| # | # | ### ", " ", " | | | | " }; private static readonly string[] Font4 = { "A", " # | # # | # # |#######|# #|# #|# #|# #", "B", "##### |# # |##### |# # |##### |# #|# #|##### ", "C", " ##### |# |# |# |# |# |# | ##### ", "D", "##### |# # |# #|# #|# #|# #|# # |##### ", "E", "#######|# |##### |# |# |# |#######| ", "F", "#######|# |##### |# |# |# |# | ", "G", " ##### |# |# |# # |# # |# # | ##### | ", "H", "# #|# #|#######|# #|# #|# #|# #|# #", "I", " ###### | # | # | # | # | # | # | ###### ", "J", " # | # | # | # |# # |# # | ###### | ", "K", "# #|# # |# # |##### |# # |# # |# #| ", "L", "# |# |# |# |# |# |####### | ", "M", "# # |## ## |# # # # |# # # |# # |# # |# # | ", "N", "# # |## # |# # # |# # # |# # # |# ## |# # | ", "O", " ##### |# #|# #|# #|# #|# #| ##### | ", "P", "##### |# # |##### |# |# |# |# | ", "Q", " ##### |# #|# #|# #|# #|# # #| ######| ", "R", "##### |# # |##### |# # |# # |# #|# #| ", "S", " ##### |# |##### | # | # |# # | ##### | ", "T", "#######| # | # | # | # | # | # | ", "U", "# #|# #|# #|# #|# #|# #| ##### | ", "V", "# #|# #|# #|# #| # # | # # | # | ", "W", "# # |# # |# # |# # # |# # # |## ## |# # | ", "X", "# #| # # | # # | # | # # | # # |# #| ", "Y", "# #| # # | # # | # | # | # | # | ", "Z", "#######| # | # | # | # | # |####### | ", "0", " ##### |# #|# #|# #|# #|# #| ##### | ", "1", " # | ## | # | # | # | # | # | ", "2", "#######| # | # |####### |# |# |########| ", "3", "#######| # | # |####### | # | # |####### | ", "4", "# #|# #|# #|########| # | # | # | ", "5", "########|# |# |######## | # | # |######## | ", "6", "#######|# |# |######## |# # |# # |######## | ", "7", "########| # | # | # | # | # |# | ", "8", "#######|# # |####### |# # |####### |# # |####### | ", "9", "#######|# # |####### | # | # | # |####### | ", "@", " ##### |# # |# # # |# ### # |# # # |# # | ##### | ", "#", "# # |########|# # |########|# # |########|# # | ", "$", " #### | # #| ##### | # # | # # | ##### | # #| #### ", "%", " ##### |# # | # | # | # | # |########| ", "&", " ##### |# # |# # # | ##### | # |# # | ##### | ", "*", " # | # # |########| # # | # | # # |########| ", "(", " # | # | # | # | # | # | # | ", ")", " # | # | # | # | # | # | # | ", "+", " # | # |########| # | # | # | # | ", "-", " | |######## | | | | | ", "=", "########| |########| |########| | | ", ":", " | # | | # | | # | | ", ";", " | # | | # | | # | # | ", ",", " | | | | # | # | # | ", ".", " | | | | # | | | ", "/", " #| # | # | # | # | # | # | ", "\\", "# | # | # | # | # | # | # | ", "?", " ###### |# # | # | # | # | | # | ", "!", " # | # | # | # | # | | # | ", " ", " | | | | | | | " }; }