MonoGame / MonoGame

One framework for creating powerful cross-platform games.
http://www.monogame.net
Other
11.49k stars 2.92k forks source link

Add headless runtime for unit testing in CI/CD and running dedicated servers #7121

Open janfokke opened 4 years ago

janfokke commented 4 years ago

Currently, there are no runtimes that don't require a graphics card.

Adding a headless runtime would enable:

Currently, I use the following hack to run MonoGame headless, but I rather use a build-in runtime

public static class HeadlessRuntimeHelper
    {
        private static ContentManager _contentManager;

        public static void Initialize()
        {
            var cache = (Dictionary<Type, ContentTypeReader>) typeof(ContentTypeReaderManager).GetField(
                    "_contentReadersCache",
                    BindingFlags.Static | BindingFlags.NonPublic)
                .GetValue(null);

            Type readerType = typeof(Game).Assembly.GetType("Microsoft.Xna.Framework.Content.Texture2DReader");
            cache.Add(readerType, new HeadlessTexture2DReader());
        }

        public static ContentManager GetContentManager(string contentPath = "Content")
        {
            return _contentManager ??= new ContentManager(new GameServiceContainer(), contentPath);
        }

        private class HeadlessTexture2DReader : ContentTypeReader<Texture2D>
        {
            private readonly FieldInfo _heightFieldInfo =
                typeof(Texture2D).GetField("height", BindingFlags.Instance | BindingFlags.NonPublic);
            private readonly FieldInfo _widthFieldInfo =
                typeof(Texture2D).GetField("width", BindingFlags.Instance | BindingFlags.NonPublic);

            protected override Texture2D Read(ContentReader reader, Texture2D existingInstance)
            {
                reader.ReadInt32();
                var width = reader.ReadInt32();
                var height = reader.ReadInt32();
                var levelCount = reader.ReadInt32();
                for (var level = 0; level < levelCount; level++)
                {
                    var levelDataSizeInBytes = reader.ReadInt32();
                    reader.BaseStream.Position += levelDataSizeInBytes;
                }

                Texture2D texture = existingInstance;
                if (texture == null)
                {
                    texture = (Texture2D) FormatterServices.GetUninitializedObject(typeof(Texture2D));
                    _widthFieldInfo.SetValue(texture, width);
                    _heightFieldInfo.SetValue(texture, height);
                }
                return texture;
            }
        }
    }
Jjagg commented 4 years ago

I don't think this is worth it for us to maintain in the main repo.

For servers you really don't want to use graphics classes at all. IMO the best thing we can do for headless MG game servers is provide a separate assembly with math types. See #6850.

For unit testing, I think no-op implementations of graphics classes are of very limited value because you can't test the most important parts.

janfokke commented 4 years ago

I don't want to test Graphics and I don't want to render on a server.

The Game class and packages like MonoGame.Extended Tiled, require a window/graphics and therefore I am currently running my server in a custom update loop and using a ContentManager with a hack.

redthing1 commented 4 years ago

@janfokke could you explain how you are running a custom update loop?

janfokke commented 4 years ago

@xdrie I don't use the Game class, because that initializes SDL stuff. I copied the MonoGame update loop code and use the code above to fix ContentManager Texture2D loading.