Skip to content

Instantly share code, notes, and snippets.

@RobThree
Created October 17, 2017 08:42
Show Gist options
  • Save RobThree/f6af3c3b84173da183140344f3a325df to your computer and use it in GitHub Desktop.
Save RobThree/f6af3c3b84173da183140344f3a325df to your computer and use it in GitHub Desktop.
OneLoneCoder GameConsole inspired C# version; work in progress and not a 1:1 conversion. API differs, tried to stay more close to dotnet best practices.
using Microsoft.Win32.SafeHandles;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
namespace ConsoleEngine
{
public abstract class GameConsole : IDisposable
{
#region Windows API
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern short GetAsyncKeyState(int vKey);
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern SafeFileHandle CreateFile(
string fileName,
[MarshalAs(UnmanagedType.U4)] uint fileAccess,
[MarshalAs(UnmanagedType.U4)] uint fileShare,
IntPtr securityAttributes,
[MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
[MarshalAs(UnmanagedType.U4)] int flags,
IntPtr template
);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, EntryPoint = "WriteConsoleOutputW")]
private static extern bool WriteConsoleOutput(
SafeFileHandle hConsoleOutput,
CharInfo[] lpBuffer,
Coord dwBufferSize,
Coord dwBufferCoord,
ref SmallRect lpWriteRegion
);
[StructLayout(LayoutKind.Sequential)]
private struct Coord
{
public short X;
public short Y;
public Coord(short x, short y)
{
X = x;
Y = y;
}
};
[StructLayout(LayoutKind.Explicit)]
private struct CharUnion
{
[FieldOffset(0)] public char UnicodeChar;
[FieldOffset(0)] public byte AsciiChar;
}
[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto)]
private struct CharInfo
{
[FieldOffset(0)] public CharUnion Char;
[FieldOffset(2)] public short Attributes;
}
[StructLayout(LayoutKind.Sequential)]
private struct SmallRect
{
public short Left;
public short Top;
public short Right;
public short Bottom;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private class CONSOLE_FONT_INFOEX
{
private int cbSize;
public CONSOLE_FONT_INFOEX()
{
cbSize = Marshal.SizeOf(typeof(CONSOLE_FONT_INFOEX));
}
public int FontIndex;
public short FontWidth;
public short FontHeight;
public int FontFamily;
public int FontWeight;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string FaceName;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetCurrentConsoleFontEx
(
IntPtr ConsoleOutput,
bool MaximumWindow,
[In, Out] CONSOLE_FONT_INFOEX ConsoleCurrentFontEx
);
#endregion
public struct KeyState
{
public bool Pressed;
public bool Released;
public bool Held;
}
private readonly SafeFileHandle _consolehandle;
private readonly Plane<CharInfo> _screenbuf;
private readonly Coord _screencoord;
private readonly Coord _topleft = new Coord() { X = 0, Y = 0 };
private readonly Thread _gamethread;
private SmallRect _screenrect;
private const int KEYSTATES = 0XFF;
private readonly short[] _newkeystate = new short[KEYSTATES];
private readonly short[] _oldkeystate = new short[KEYSTATES];
public KeyState[] KeyStates { get; private set; }
public short Width { get; private set; }
public short Height { get; private set; }
public string Title { get { return Console.Title; } set { Console.Title = value ?? "GameConsole by RobIII"; } }
public enum PIXELS
{
PIXEL_NONE = '\0',
PIXEL_SOLID = (char)0xDB,
PIXEL_THREEQUARTERS = (char)0XB2,
PIXEL_HALF = (char)0XB1,
PIXEL_QUARTER = (char)0xB0
}
public enum COLOR
{
FG_BLACK = 0x0000,
FG_DARK_BLUE = 0x0001,
FG_DARK_GREEN = 0x0002,
FG_DARK_CYAN = 0x0003,
FG_DARK_RED = 0x0004,
FG_DARK_MAGENTA = 0x0005,
FG_DARK_YELLOW = 0x0006,
FG_GREY = 0x0007,
FG_DARK_GREY = 0x0008,
FG_BLUE = 0x0009,
FG_GREEN = 0x000A,
FG_CYAN = 0x000B,
FG_RED = 0x000C,
FG_MAGENTA = 0x000D,
FG_YELLOW = 0x000E,
FG_WHITE = 0x000F,
BG_BLACK = 0x0000,
BG_DARK_BLUE = 0x0010,
BG_DARK_GREEN = 0x0020,
BG_DARK_CYAN = 0x0030,
BG_DARK_RED = 0x0040,
BG_DARK_MAGENTA = 0x0050,
BG_DARK_YELLOW = 0x0060,
BG_GREY = 0x0070,
BG_DARK_GREY = 0x0080,
BG_BLUE = 0x0090,
BG_GREEN = 0x00A0,
BG_CYAN = 0x00B0,
BG_RED = 0x00C0,
BG_MAGENTA = 0x00D0,
BG_YELLOW = 0x00E0,
BG_WHITE = 0x00F0,
}
public GameConsole(short width, short height, string title = null, string font = "Consolas", short fontwidth = 8, short fontheight = 8)
{
Width = width;
Height = height;
Title = title;
KeyStates = new KeyState[KEYSTATES];
_consolehandle = CreateFile("CONOUT$", 0x40000000, 0x02, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);
if (!_consolehandle.IsInvalid)
{
_screenbuf = new Plane<CharInfo>(Width, Height);
_screenrect = new SmallRect() { Left = 0, Top = 0, Right = Width, Bottom = Height };
_screencoord = new Coord() { X = Width, Y = Height };
var cfi = new CONSOLE_FONT_INFOEX()
{
FaceName = font,
FontWidth = fontwidth,
FontHeight = fontheight,
FontFamily = 0, //FF_DONTCARE
FontWeight = 0x0190, //FW_NORMAL
FontIndex = 0
};
SetCurrentConsoleFontEx(_consolehandle.DangerousGetHandle(), false, cfi);
}
if (width > Console.LargestWindowWidth || height > Console.LargestWindowHeight)
throw new InvalidOperationException($"Unable to create console; maximum width/height are {Console.LargestWindowWidth} x {Console.LargestWindowHeight}");
Console.WindowWidth = width;
Console.WindowHeight = height;
Console.CursorVisible = false;
//Console.OutputEncoding = Encoding.Unicode;
_gamethread = new Thread(() =>
{
if (OnUserCreate())
{
var sw = Stopwatch.StartNew();
var cont = true;
while (cont)
{
GetKeyStates();
cont = OnUserUpdate(sw.Elapsed);
Paint();
};
}
});
}
public void DrawSprite(int x, int y, Sprite sprite, char alphaChar = '\0')
{
for (int py = 0; py < sprite.Height; py++)
{
for (int px = 0; px < sprite.Width; px++)
{
var c = sprite.GetChar(px, py);
if (c != alphaChar)
SetChar(x + px, y + py, sprite.GetChar(px, py), sprite.GetColor(px, py));
}
}
}
public void Clear()
{
Fill(0, 0, Width, Height, (char)PIXELS.PIXEL_NONE, (short)COLOR.BG_BLACK);
}
public void Fill(int x, int y, int width, int height, char c = (char)PIXELS.PIXEL_NONE, short attributes = (short)COLOR.BG_BLACK)
{
for (int xp = x; xp < width; xp++)
for (int yp = y; yp < height; yp++)
SetChar(xp, yp, (char)PIXELS.PIXEL_NONE, 0);
}
public void Print(int x, int y, string text, short attributes = (int)COLOR.FG_WHITE)
{
for (int i = 0; i < text.Length; ++i)
{
SetChar(x + i, y, text[i], attributes);
}
}
public void SetChar(int x, int y, char c, short attributes = (short)COLOR.FG_WHITE)
{
var offset = _screenbuf.GetOffset(x, y);
_screenbuf.Data[offset].Attributes = attributes;
_screenbuf.Data[offset].Char.UnicodeChar = c;
}
public char GetChar(int x, int y)
{
return _screenbuf.GetData(x, y).Char.UnicodeChar;
}
public void Start()
{
_gamethread.Start();
_gamethread.Join();
}
public KeyState GetKeyState(ConsoleKey key)
{
return KeyStates[(int)key];
}
public static int Clamp(int v, int min, int max)
{
return Math.Min(Math.Max(v, min), max);
}
public static double ClampF(double v, double min, double max)
{
return Math.Min(Math.Max(v, min), max);
}
public abstract bool OnUserCreate();
public abstract bool OnUserUpdate(TimeSpan elapsedTime);
private void Paint()
{
WriteConsoleOutput(_consolehandle, _screenbuf.Data, _screencoord, _topleft, ref _screenrect);
}
private void GetKeyStates()
{
for (int i = 0; i < KEYSTATES; i++)
{
_newkeystate[i] = GetAsyncKeyState(i);
KeyStates[i].Pressed = false;
KeyStates[i].Released = false;
if (_newkeystate[i] != _oldkeystate[i])
{
if ((_newkeystate[i] & 0x8000) != 0)
{
KeyStates[i].Pressed = !KeyStates[i].Held;
KeyStates[i].Held = true;
}
else
{
KeyStates[i].Released = true;
KeyStates[i].Held = false;
}
}
_oldkeystate[i] = _newkeystate[i];
}
}
#region IDisposable Support
private bool _disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed state (managed objects) here.
}
_consolehandle.Dispose();
_disposed = true;
}
}
~GameConsole()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(false);
}
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}
}
namespace ConsoleEngine
{
public class Plane<T>
{
private readonly T[] _data;
public T[] Data { get { return _data; } }
public int Width { get; private set; }
public int Height { get; private set; }
public Plane(int width, int height)
{
Width = width;
Height = height;
_data = new T[width * height];
}
public int GetOffset(int x, int y)
{
return GameConsole.Clamp(y, 0, Height - 1) * Width + GameConsole.Clamp(x, 0, Width - 1);
}
private int EnsureValidOffset(int offset)
{
return GameConsole.Clamp(offset, 0, _data.Length);
}
public void SetData(int offset, T data)
{
_data[EnsureValidOffset(offset)] = data;
}
public void SetData(int x, int y, T data)
{
_data[GetOffset(x, y)] = data;
}
public T GetData(int offset)
{
return _data[EnsureValidOffset(offset)];
}
public T GetData(int x, int y)
{
return _data[GetOffset(x, y)];
}
}
}
using System;
using System.Linq;
namespace ConsoleEngine
{
public class Sprite
{
private readonly Plane<char> _spritedata;
private readonly Plane<short> _spritecolors;
private readonly int _width;
private readonly int _height;
public int Width { get { return _width; } }
public int Height { get { return _height; } }
public Sprite(string[] spriteData, short[] spriteColors = null)
{
if (spriteData.Length == 0)
throw new ArgumentException(nameof(spriteData));
if (spriteData.Any(s => s.Length != spriteData[0].Length))
throw new ArgumentException(nameof(spriteData));
_width = spriteData[0].Length;
_height = spriteData.Length;
if (spriteColors != null && spriteColors.Length != _width * _height)
throw new ArgumentException(nameof(spriteColors));
_spritedata = new Plane<char>(_width, _height);
_spritecolors = new Plane<short>(_width, _height);
int i = 0;
foreach (var s in spriteData)
foreach (var c in s)
_spritedata.SetData(i++, c);
for (i = 0; i < _spritecolors.Data.Length; i++)
_spritecolors.SetData(i, spriteColors == null ? (short)GameConsole.COLOR.FG_GREY : spriteColors[i]);
}
public Sprite(string[] spriteData, GameConsole.COLOR[] spriteColors = null)
: this(spriteData, spriteColors?.Select(c => (short)c).ToArray())
{ }
public char GetChar(int x, int y)
{
return _spritedata.GetData(x, y);
}
public short GetColor(int x, int y)
{
return _spritecolors.GetData(x, y);
}
}
}
@RobThree
Copy link
Author

Example: Snake

File: program.cs

using System.Threading;

namespace ConsoleEngine
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var f = new Snek())
                f.Start();
        }
    }
}

File: snek.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleEngine
{
    public class Snek : GameConsole
    {
        private List<SP> _snake;
        private M _lastmovement;
        private TimeSpan _lasttick;
        private int _score;
        private bool _dead;
        private readonly Random _rng = new Random();
        private readonly TimeSpan _frameinterval = TimeSpan.FromMilliseconds(50);
        private const int STARTLEN = 15;

        private const char PIXEL_FOOD = 'O';

        public Snek()
            : base(120, 75, "SNEK!", fontwidth: 10, fontheight: 10)
        { }

        public override bool OnUserCreate()
        {
            _lastmovement = new M { DX = 1, DY = 0 };
            _lasttick = TimeSpan.MinValue;
            _snake = new List<SP>(Enumerable.Range(1, STARTLEN).Select(i => new SP { X = STARTLEN - i, Y = 10 }));
            _score = 0;
            _dead = false;

            // Draw food
            for (int i = 0; i < 50; i++)
                Draw(GetFreeSpot(), PIXEL_FOOD, (int)COLOR.FG_GREEN);

            // Draw snake
            for (int i = 0; i < _snake.Count; i++)
                SetChar(_snake[_snake.Count - i - 1].X, _snake[_snake.Count - i - 1].Y, GetSnakePart(i), (short)COLOR.FG_WHITE);

            return true;
        }

        public override bool OnUserUpdate(TimeSpan elapsedTime)
        {
            Title = $"SNEK! SCORE: {_score} {elapsedTime}";

            // New frame?
            if (_lasttick  + _frameinterval <= elapsedTime)
            {
                // Find out which way to move
                var move = new M { DX = 0, DY = 0 };
                if (GetKeyState(ConsoleKey.LeftArrow).Held)
                    move.DX = -1;
                if (GetKeyState(ConsoleKey.RightArrow).Held)
                    move.DX = 1;
                if (GetKeyState(ConsoleKey.UpArrow).Held)
                    move.DY = -1;
                if (GetKeyState(ConsoleKey.DownArrow).Held)
                    move.DY = 1;
                if (move.DX != 0)
                    move.DY = 0;

                if (!_dead)
                {
                    // Ensure we always move SOME way
                    if (move.DX == 0 && move.DY == 0)
                        move = _lastmovement;

                    // Update snake
                    var head = _snake[0];
                    var tail = _snake[_snake.Count - 1];

                    // Erase last pixel from tail
                    SetChar(tail.X, tail.Y, (char)PIXELS.PIXEL_NONE);
                    for (int i = _snake.Count - 1; i > 0; i--)
                    {
                        _snake[i].X = _snake[i - 1].X;
                        _snake[i].Y = _snake[i - 1].Y;
                    }

                    if (head.X + move.DX < 0 || head.X + move.DX > Width - 1 || head.Y + move.DY < 0 || head.Y + move.DY > Height - 1)
                    {
                        _dead = true;
                    }
                    else
                    {
                        head.X += move.DX;
                        head.Y += move.DY;
                    }

                    switch (GetPixel(head))
                    {
                        case PIXEL_FOOD:
                            _snake.Add(new SP { X = tail.X, Y = tail.Y });
                            Draw(GetFreeSpot(), PIXEL_FOOD, (int)COLOR.FG_GREEN);
                            _score += 10;
                            break;
                        case (char)PIXELS.PIXEL_NONE:
                            break;
                        default:
                            _dead = true;
                            break;
                    }

                    // Draw snake
                    Draw(head, (char)PIXELS.PIXEL_SOLID);
                    for (int i = 0; i < 3; i++)
                        Draw(_snake[_snake.Count - i - 1], GetSnakePart(i), (short)COLOR.FG_WHITE);

                    // Remember last move
                    _lastmovement = move;
                    _lasttick = elapsedTime;
                }
                else
                {
                    var s = "You died! SCORE: " + _score;

                    Print((Width - s.Length) / 2, Height / 2, s, (short)COLOR.FG_RED);
                }
            }

            return !KeyStates[(int)ConsoleKey.Escape].Pressed;
        }

        private void Draw(SP pos, char c, short attributes = 15)
        {
            SetChar(pos.X, pos.Y, c, attributes);
        }

        private char GetPixel(SP pos)
        {
            return GetChar(pos.X, pos.Y);
        }

        private SP GetFreeSpot()
        {
            SP sp;
            do
            {
                sp = new SP
                {
                    X = _rng.Next(Width),
                    Y = _rng.Next(Height)
                };
            } while (GetPixel(sp) != (char)PIXELS.PIXEL_NONE);
            return sp;
        }

        private static char GetSnakePart(int index)
        {
            switch (index)
            {
                case 0:
                    return (char)PIXELS.PIXEL_QUARTER;
                case 1:
                    return (char)PIXELS.PIXEL_HALF;
                case 2:
                    return (char)PIXELS.PIXEL_THREEQUARTERS;
                default:
                    return (char)PIXELS.PIXEL_SOLID;

            }
        }

        class SP
        {
            public int X { get; set; }
            public int Y { get; set; }
        }

        class M
        {
            public int DX { get; set; }
            public int DY { get; set; }
        }
    }
}

@RobThree
Copy link
Author

Example: SpriteTest

File: program.cs

using System.Threading;

namespace ConsoleEngine
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var f = new SpriteTest())
                f.Start();
        }
    }
}

File: spritetest.cs

using System;

namespace ConsoleEngine
{
    public class SpriteTest : GameConsole
    {
        private readonly Sprite[] _sprites = new[] {
            new Sprite(new[] {
                "XXX   XXX",
                " XXX XXX ",
                "  XXXXX  ",
                " XXX XXX ",
                "XXX   XXX",
            }, new short[] {

                12, 12, 12, 0, 0, 0, 12, 12, 12,
                0, 12, 12, 12, 0, 12, 12, 12, 0,
                0, 0, 12, 12, 12, 12, 12, 0, 0,
                0, 12, 12, 12, 0, 12, 12, 12, 0,
                12, 12, 12, 0, 0, 0, 12, 12, 12,
            }),
            new Sprite(new[] {
                "XXX   XXX",
                " XXX XXX ",
                " XXX XXX ",
                " XXX XXX ",
                "XXX   XXX",
            }, new short[] {

                12, 12, 12, 0, 0, 0, 12, 12, 12,
                0, 12, 12, 12, 0, 12, 12, 12, 0,
                0, 12, 12, 12, 0, 12, 12, 12, 0,
                0, 12, 12, 12, 0, 12, 12, 12, 0,
                12, 12, 12, 0, 0, 0, 12, 12, 12,
            }),
            new Sprite(new[] {
                "XXX   XXX",
                " XXX XXX ",
                "XXX   XXX",
                " XXX XXX ",
                "XXX   XXX",
            }, new short[] {

                12, 12, 12, 0, 0, 0, 12, 12, 12,
                0, 12, 12, 12, 0, 12, 12, 12, 0,
                12, 12, 12, 0, 0, 0, 12, 12, 12,
                0, 12, 12, 12, 0, 12, 12, 12, 0,
                12, 12, 12, 0, 0, 0, 12, 12, 12,
            }),
            new Sprite(new[] {
                "XXX   XXX",
                " XXX XXX ",
                " XXX XXX ",
                " XXX XXX ",
                "XXX   XXX",
            }, new short[] {

                12, 12, 12, 0, 0, 0, 12, 12, 12,
                0, 12, 12, 12, 0, 12, 12, 12, 0,
                0, 12, 12, 12, 0, 12, 12, 12, 0,
                0, 12, 12, 12, 0, 12, 12, 12, 0,
                12, 12, 12, 0, 0, 0, 12, 12, 12,
            }),
        };


        public SpriteTest()
            : base(160, 100)
        {

        }

        public override bool OnUserCreate()
        {
            return true;
        }

        public override bool OnUserUpdate(TimeSpan elapsedTime)
        {
            Clear();

            var i = (int)((elapsedTime.TotalSeconds * 3) % _sprites.Length);

            DrawSprite(30 + (int)(Math.Round(Math.Cos(elapsedTime.TotalSeconds * 2) * 20)), 30 + (int)Math.Round(Math.Sin(elapsedTime.TotalSeconds * 2) * 20), _sprites[i], ' ');

            DrawSprite(40 + (int)(Math.Round(Math.Sin(elapsedTime.TotalSeconds * 2) * 20)), 40 + (int)Math.Round(Math.Cos(elapsedTime.TotalSeconds * 2) * 20), _sprites[i], ' ');

            DrawSprite(50 + (int)(Math.Round(Math.Cos(elapsedTime.TotalSeconds * 2) * 20)), 40 + (int)Math.Round(Math.Sin(elapsedTime.TotalSeconds * 2 + Math.PI) * 20), _sprites[i], ' ');


            DrawSprite(110 + (int)(Math.Round(Math.Cos(elapsedTime.TotalSeconds * 2) * 20)), 50 + (int)Math.Round(Math.Sin(elapsedTime.TotalSeconds * 2 + Math.PI * 1.8) * 20), _sprites[i], ' ');

            DrawSprite(90 + (int)(Math.Round(Math.Sin(elapsedTime.TotalSeconds * 2) * 20)), 20 + (int)Math.Round(Math.Cos(elapsedTime.TotalSeconds * 2 + Math.PI * .7) * 20), _sprites[i], ' ');

            DrawSprite(130 + (int)(Math.Round(Math.Cos(elapsedTime.TotalSeconds * 2) * 20)), 60 + (int)Math.Round(Math.Sin(elapsedTime.TotalSeconds * 2 + Math.PI * 1.3) * 20), _sprites[i], ' ');

            return !KeyStates[(int)ConsoleKey.Escape].Pressed;
        }
    }
}

@ZoBoRf
Copy link

ZoBoRf commented Oct 13, 2018

To Support Unicode-Characters I had to change the type of UnicdeChar in CharUnion from char to short.

using Microsoft.Win32.SafeHandles;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;


namespace ConsoleEngine
{
    public abstract class GameConsole : IDisposable
    {
        #region Windows API
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        private static extern short GetAsyncKeyState(int vKey);

        [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
        private static extern SafeFileHandle CreateFile(
            string fileName,
            [MarshalAs(UnmanagedType.U4)] uint fileAccess,
            [MarshalAs(UnmanagedType.U4)] uint fileShare,
            IntPtr securityAttributes,
            [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
            [MarshalAs(UnmanagedType.U4)] int flags,
            IntPtr template
        );

        [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto, EntryPoint = "WriteConsoleOutputW")]
        private static extern bool WriteConsoleOutput(
            SafeFileHandle hConsoleOutput,
            CharInfo[] lpBuffer,
            Coord dwBufferSize,
            Coord dwBufferCoord,
            ref SmallRect lpWriteRegion
        );



        [StructLayout(LayoutKind.Sequential)]
        private struct Coord
        {
            public short X;
            public short Y;

            public Coord(short x, short y)
            {
                X = x;
                Y = y;
            }
        };

        [StructLayout(LayoutKind.Explicit)]
        private struct CharUnion
        {
            [FieldOffset(0)] public short UnicodeChar;
            [FieldOffset(0)] public byte AsciiChar;
        }

        [StructLayout(LayoutKind.Explicit, CharSet = CharSet.Auto)]
        private struct CharInfo
        {
            [FieldOffset(0)] public CharUnion Char;
            [FieldOffset(2)] public short Attributes;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct SmallRect
        {
            public short Left;
            public short Top;
            public short Right;
            public short Bottom;
        }

        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
        private class CONSOLE_FONT_INFOEX
        {
            private int cbSize;
            public CONSOLE_FONT_INFOEX()
            {
                cbSize = Marshal.SizeOf(typeof(CONSOLE_FONT_INFOEX));
            }

            public int FontIndex;
            public short FontWidth;
            public short FontHeight;
            public int FontFamily;
            public int FontWeight;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
            public string FaceName;
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern bool SetCurrentConsoleFontEx
          (
          IntPtr ConsoleOutput,
          bool MaximumWindow,
          [In, Out] CONSOLE_FONT_INFOEX ConsoleCurrentFontEx
        );
        #endregion

        public struct KeyState
        {
            public bool Pressed;
            public bool Released;
            public bool Held;
        }

        private readonly SafeFileHandle _consolehandle;
        private readonly Plane<CharInfo> _screenbuf;
        private readonly Coord _screencoord;
        private readonly Coord _topleft = new Coord() { X = 0, Y = 0 };
        private readonly Thread _gamethread;
        private SmallRect _screenrect;

        private const int KEYSTATES = 0XFF;
        private readonly short[] _newkeystate = new short[KEYSTATES];
        private readonly short[] _oldkeystate = new short[KEYSTATES];

        public KeyState[] KeyStates { get; private set; }
        public short Width { get; private set; }
        public short Height { get; private set; }
        public string Title { get { return Console.Title; } set { Console.Title = value ?? "GameConsole by RobIII"; } }

        public enum PIXELS
        {
            PIXEL_NONE = '\0',
            PIXEL_SOLID = 0x2588,
            PIXEL_THREEQUARTERS = 0x2593,
            PIXEL_HALF = 0x2592,
            PIXEL_QUARTER = 0x2591
        }

        public enum COLOR
        {
            FG_BLACK = 0x0000,
            FG_DARK_BLUE = 0x0001,
            FG_DARK_GREEN = 0x0002,
            FG_DARK_CYAN = 0x0003,
            FG_DARK_RED = 0x0004,
            FG_DARK_MAGENTA = 0x0005,
            FG_DARK_YELLOW = 0x0006,
            FG_GREY = 0x0007,
            FG_DARK_GREY = 0x0008,
            FG_BLUE = 0x0009,
            FG_GREEN = 0x000A,
            FG_CYAN = 0x000B,
            FG_RED = 0x000C,
            FG_MAGENTA = 0x000D,
            FG_YELLOW = 0x000E,
            FG_WHITE = 0x000F,
            BG_BLACK = 0x0000,
            BG_DARK_BLUE = 0x0010,
            BG_DARK_GREEN = 0x0020,
            BG_DARK_CYAN = 0x0030,
            BG_DARK_RED = 0x0040,
            BG_DARK_MAGENTA = 0x0050,
            BG_DARK_YELLOW = 0x0060,
            BG_GREY = 0x0070,
            BG_DARK_GREY = 0x0080,
            BG_BLUE = 0x0090,
            BG_GREEN = 0x00A0,
            BG_CYAN = 0x00B0,
            BG_RED = 0x00C0,
            BG_MAGENTA = 0x00D0,
            BG_YELLOW = 0x00E0,
            BG_WHITE = 0x00F0,
        }

        public GameConsole(short width, short height, string title = null, string font = "Consolas", short fontwidth = 8, short fontheight = 8)
        {
            Width = width;
            Height = height;
            Title = title;

            KeyStates = new KeyState[KEYSTATES];

            _consolehandle = CreateFile("CONOUT$", 0x40000000, 0x02, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero);

            if (!_consolehandle.IsInvalid)
            {
                _screenbuf = new Plane<CharInfo>(Width, Height);
                _screenrect = new SmallRect() { Left = 0, Top = 0, Right = Width, Bottom = Height };
                _screencoord = new Coord() { X = Width, Y = Height };

                var cfi = new CONSOLE_FONT_INFOEX()
                {
                    FaceName = font,
                    FontWidth = fontwidth,
                    FontHeight = fontheight,
                    FontFamily = 0,            //FF_DONTCARE
                    FontWeight = 0x0190,       //FW_NORMAL
                    FontIndex = 0
                };

                SetCurrentConsoleFontEx(_consolehandle.DangerousGetHandle(), false, cfi);
            }

            if (width > Console.LargestWindowWidth || height > Console.LargestWindowHeight)
                throw new InvalidOperationException($"Unable to create console; maximum width/height are {Console.LargestWindowWidth} x {Console.LargestWindowHeight}");

            Console.WindowWidth = width;
            Console.WindowHeight = height;
            Console.CursorVisible = false;

            _gamethread = new Thread(() =>
            {
                if (OnUserCreate())
                {
                    var sw = Stopwatch.StartNew();
                    var cont = true;

                    while (cont)
                    {
                        GetKeyStates();

                        cont = OnUserUpdate(sw.Elapsed);
                        Paint();
                    };

                }
            });
        }

        public void DrawSprite(int x, int y, Sprite sprite, char alphaChar = '\0')
        {
            for (int py = 0; py < sprite.Height; py++)
            {
                for (int px = 0; px < sprite.Width; px++)
                {
                    var c = sprite.GetChar(px, py);
                    if (c != alphaChar)
                        SetChar(x + px, y + py, sprite.GetChar(px, py), sprite.GetColor(px, py));
                }
            }
        }

        public void Clear()
        {
            Fill(0, 0, Width, Height, (char)PIXELS.PIXEL_NONE, (short)COLOR.BG_BLACK);
        }

        public void Fill(int x, int y, int width, int height, char c = (char)PIXELS.PIXEL_NONE, short attributes = (short)COLOR.BG_BLACK)
        {
            for (int xp = x; xp < width; xp++)
                for (int yp = y; yp < height; yp++)
                    SetChar(xp, yp, (char)PIXELS.PIXEL_NONE, 0);
        }

        public void Print(int x, int y, string text, short attributes = (int)COLOR.FG_WHITE)
        {
            for (int i = 0; i < text.Length; ++i)
            {
                SetChar(x + i, y, text[i], attributes);
            }
        }

        public void SetChar(int x, int y, char c, short attributes = (short)COLOR.FG_WHITE)
        {
            var offset = _screenbuf.GetOffset(x, y);
            _screenbuf.Data[offset].Attributes = attributes;
            _screenbuf.Data[offset].Char.UnicodeChar = (short)c;
        }

        public char GetChar(int x, int y)
        {
            return (char)_screenbuf.GetData(x, y).Char.UnicodeChar;
        }

        public void Start()
        {

            _gamethread.Start();
            _gamethread.Join();
        }

        public KeyState GetKeyState(ConsoleKey key)
        {
            return KeyStates[(int)key];
        }

        public static int Clamp(int v, int min, int max)
        {
            return Math.Min(Math.Max(v, min), max);
        }

        public static double ClampF(double v, double min, double max)
        {
            return Math.Min(Math.Max(v, min), max);
        }

        public abstract bool OnUserCreate();

        public abstract bool OnUserUpdate(TimeSpan elapsedTime);

        private void Paint()
        {
            WriteConsoleOutput(_consolehandle, _screenbuf.Data, _screencoord, _topleft, ref _screenrect);
        }

        private void GetKeyStates()
        {
            for (int i = 0; i < KEYSTATES; i++)
            {

                _newkeystate[i] = GetAsyncKeyState(i);

                KeyStates[i].Pressed = false;
                KeyStates[i].Released = false;

                if (_newkeystate[i] != _oldkeystate[i])
                {
                    if ((_newkeystate[i] & 0x8000) != 0)
                    {
                        KeyStates[i].Pressed = !KeyStates[i].Held;
                        KeyStates[i].Held = true;
                    }
                    else
                    {
                        KeyStates[i].Released = true;
                        KeyStates[i].Held = false;
                    }
                }
                _oldkeystate[i] = _newkeystate[i];
            }
        }

        #region IDisposable Support
        private bool _disposed = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    // Dispose managed state (managed objects) here.
                }

                _consolehandle.Dispose();

                _disposed = true;
            }
        }

        ~GameConsole()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(false);
        }

        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        #endregion
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment