NotNotTech / Raylib-CsLo

autogen bindings to Raylib 4.x and convenience wrappers on top. Requires use of `unsafe`
Mozilla Public License 2.0
115 stars 10 forks source link

Would it be possible to create c# bindings for rres resource packer system? #19

Closed SoloByte closed 2 years ago

SoloByte commented 2 years ago

Issue description

Dealing with exported resources in raylib with c# is complicated right now.

The default way is to copy the resource folder to the output directory but when distributing your game/app all resources are visible to the customer. (among many other problems with that way)

The problem is that loading resources from anywhere else than the disk is very complicated or not possible. (to my knowledge with raylib c# bindings) So my only solution would be to compress/encrypt the resources when building and then uncompress/decrypt them when the game/app is started but then the resources are visible again to the user while the game/app is running.

Raysan already implemented a solution for raylib called rres but it is only available for c right now. The resource packer can already be used to pack all the resources into a rres file but there is no way to load that rres file and transform it into raylib types like image, font, wave, etc. right now.

It would be really nice if there would be separate bindings for rres so it can be used with c# and it would make raylib with c# much more robust.

PS: At least it would be nice to know if you are interested in doing that at all. Because I don´t need it right now and if you would like to do it I am not going to look for another solution.

I will also ask ChrisDill with raylib-cs bindings.

Thanks!

jasonswearingen commented 2 years ago

Hi, could you tell me the steps currently needed to use this? I just briefly skimmed, and it looks like I would need to make a wrapper over rres.h from here: https://github.com/raysan5/rres

This is totally doable, and very easy to do so by anybody using the scripts found in CsLo. However I am currently remote for the next MONTH so not near my gamedev machine where I do this sort of work. Could you please confirm that it's rres.h that needs to be wrapped, and any other details? I can take a look when I get back (End of Aug) or in the meantime, if you want to checkin rres.h and modify the build script to include it in the CsLo output I'd be happy to merge. If this seems interesting, see https://github.com/NotNotTech/Raylib-CsLo/tree/main/sub-modules

SoloByte commented 2 years ago

Hi! Thanks for getting back to me. As I understand it "rres.h" and "rres-raylib.h" need to be wrapped. Rres-raylib.h is needed to convert the packed data into usably data structures for raylib. It is described here and here.

It is not urgent so take your time. I found a temporary solution that works for me until rres is ready. Unfortunately, I don´t know much about the process of creating bindings, so I am afraid I can´t help you there.

SoloByte commented 2 years ago

This is my temporary solution for packing resources into 1 file to make the distribution of the build easier. For anyone looking for something like this feel free to use it any way you want.

Limitations

How it Works

I always set this in my project file: <RunWorkingDirectory>$(MSBuildThisFileDirectory)</RunWorkingDirectory> to make the paths relative to the project.

You need a single folder containing all the resources you want to pack. I mostly just have a resources folder with a structure like that in my project:

resources -gfx --exampe-texture.png -audio --sfx ---example-sound.wav --music ---example-music.ogg -fonts --example-font.ttf -shaders --example-shader.fs

To pack your resources you just have to call:

string sourcePath = "resources"; //because it is relative to the project that is enough
string outputPath = ""; //empty path puts it in the root of your project and the file is always called resources.txt
ResourceManager.Generate(sourcePath, outputPath);

And when you run the game in visual studio the file is created.

When you build the game and you want to launch it from the .exe file make sure to comment out the ResourceManager.Generate(sourcePath, outputPath) Function Call!

Additionally, you can right-click on the newly created resources.txt in visual studio, go to properties, and set "Copy To Output Directory" to "Always Copy" so the file is copied to the output directory when you build the game.

You could make a separate console app with the source code and use the .exe file in a pre-build event in visual studio to automate the process of packing the resources.

Loading Resources

You need to create an instance of the ResourceManager Class and give it the path to the resources.txt file. After that just call any of the Load Functions in the resource manager instance to load resources (Image, Texture, Font, Sound, Music, Shader, Json String).

var resourceManager = new ResourceManager(""); //because file is in the root of the project "" is enough
var texture = resourceManager.LoadTexture("example-texture");
var sound = resourceManager.LoadSound("example-sound");
var music = resourceManager.LoadMusic("example-music");
var font = resourceManager.LoadFont("example-font", 100);
var shader = resourceManager.LoadFragmentShader("example-shader");

Source Code

using Raylib_CsLo;
using System.Text;
using System.IO.Compression;

namespace Resources
{
    //shader loading does not work yet
    internal struct ResourceInfo
    {
        public string extension;
        public byte[] data;

        public ResourceInfo(string extension, byte[] data) { this.extension = extension; this.data = data; }
    }
    public static class ResourceManager
    {
        private static Dictionary<string, ResourceInfo> resources = new();

        public static void Initialize(string path)
        {
            resources = LoadResources(path);
        }

        public static void Generate(string sourcePath, string outputPath)
        {
            string filename = "resources.txt";
            string[] files = Directory.GetFiles(sourcePath, "", SearchOption.AllDirectories);
            List<string> lines = new List<string>();
            foreach (var file in files)
            {
                lines.Add(Path.GetFileName(file));
                var d = File.ReadAllBytes(file);
                lines.Add(Convert.ToBase64String(Compress(d)));
            }
            File.WriteAllLines(outputPath + filename, lines);
        }

        public static Texture LoadTexture(string name)
        {
            return Raylib.LoadTextureFromImage(LoadImage(name));
        }
        public static Image LoadImage(string name)
        {
            unsafe
            {
                var data = resources[name].data;
                var extension = resources[name].extension;
                fixed (byte* ptr = data)
                {
                    return Raylib.LoadImageFromMemory(extension, ptr, data.Length);
                }
            }
        }
        public static Font LoadFont(string name, int fontSize = 100)
        {
            unsafe
            {
                var data = resources[name].data;
                var extension = resources[name].extension;
                fixed (byte* ptr = data)
                {
                    return Raylib.LoadFontFromMemory(extension, ptr, data.Length, fontSize, (int*)0, 300);
                }
            }
        }
        public static Wave LoadWave(string name)
        {
            unsafe
            {
                var data = resources[name].data;
                var extension = resources[name].extension;
                fixed (byte* ptr = data)
                {
                    return Raylib.LoadWaveFromMemory(extension, ptr, data.Length);
                }
            }
        }
        public static Sound LoadSound(string name)
        {
            return Raylib.LoadSoundFromWave(LoadWave(name));
        }
        public static Music LoadMusic(string name)
        {
            unsafe
            {
                var data = resources[name].data;
                var extension = resources[name].extension;
                fixed (byte* ptr = data)
                {
                    return Raylib.LoadMusicStreamFromMemory(extension, ptr, data.Length);
                }
            }
        }
        public static Shader LoadFragmentShader(string name)
        {
            string file = Encoding.Default.GetString(resources[name].data);
            return Raylib.LoadShaderFromMemory(null, file);
        }
        public static Shader LoadVertexShader(string name)
        {
            string file = Encoding.Default.GetString(resources[name].data);
            return Raylib.LoadShaderFromMemory(null, file);
        }
        public static string LoadJsonData(string name)
        {
            return Encoding.Default.GetString(resources[name].data);
        }

        private static Dictionary<string, ResourceInfo> LoadResources(string path)
        {
            Dictionary<string, ResourceInfo> result = new();
            var lines = File.ReadAllLines(path + "resources.txt");
            for (int i = 0; i < lines.Length; i += 2)
            {
                string filenName = lines[i];
                string name = Path.GetFileNameWithoutExtension(filenName);
                string extension = Path.GetExtension(filenName);
                string dataText = lines[i + 1];
                var data = Convert.FromBase64String(dataText);
                result.Add(name, new(extension, Decompress(data)));
            }
            return result;
        }
        private static byte[] Compress(byte[] data)
        {
            MemoryStream output = new MemoryStream();
            using (DeflateStream dstream = new DeflateStream(output, CompressionLevel.Optimal))
            {
                dstream.Write(data, 0, data.Length);
            }
            return output.ToArray();
        }
        private static byte[] Decompress(byte[] data)
        {
            MemoryStream input = new MemoryStream(data);
            MemoryStream output = new MemoryStream();
            using (DeflateStream dstream = new DeflateStream(input, CompressionMode.Decompress))
            {
                dstream.CopyTo(output);
            }
            return output.ToArray();
        }

    }

}
jasonswearingen commented 2 years ago

sorry for the delay, I will try to create bindings for everything with the update to 4.2, this comming weekend.

SoloByte commented 2 years ago

No problem! Thank you very much ;)

jasonswearingen commented 2 years ago

I included basic (unsafe) bindings to RRes, but no tests for it. please let me know how, or if it works. will be in the alpha1 nuget in a few minutes.

SoloByte commented 2 years ago

@jasonswearingen Thank you very much! I will test it as soon as I can and let you know if everything works ;)