diff --git a/BeefProj.toml b/BeefProj.toml index 90cd527..ee19c67 100644 --- a/BeefProj.toml +++ b/BeefProj.toml @@ -1,8 +1,7 @@ FileVersion = 1 -Dependencies = {corlib = "*", SDL2 = "*"} +Dependencies = {corlib = "*", SDL2 = "*", MiniZ = "*"} [Project] Name = "Strawberry" TargetType = "BeefLib" StartupObject = "Program" -DefaultNamespace = "Strawberry" diff --git a/src/Core/Game.bf b/src/Core/Game.bf index 6ae8032..0a52018 100644 --- a/src/Core/Game.bf +++ b/src/Core/Game.bf @@ -15,6 +15,7 @@ namespace Strawberry public class Game { + public readonly Dictionary Sprites; public readonly List VirtualInputs; public readonly String Title; public readonly int Width; @@ -42,7 +43,6 @@ namespace Strawberry : base() { Game = this; - VirtualInputs = new List(); Title = windowTitle; Width = width; @@ -81,8 +81,13 @@ namespace Strawberry SDLMixer.OpenAudio(44100, SDLMixer.MIX_DEFAULT_FORMAT, 2, 4096); SDLTTF.Init(); - BuildTypeLists(); + VirtualInputs = new List(); Input.[Friend]Init(gamepadLimit); + + Sprites = new Dictionary(); + //LoadSprites(); + + BuildTypeLists(); } public ~this() @@ -104,6 +109,8 @@ namespace Strawberry DisposeTypeLists(); Input.[Friend]Dispose(); + DisposeSprites(); + Sprite.[Friend]Dispose(); Game = null; } @@ -184,7 +191,7 @@ namespace Strawberry } } - public void Render() + private void Render() { SDL.SetRenderDrawColor(Renderer, ClearColor.R, ClearColor.G, ClearColor.B, ClearColor.A); SDL.RenderClear(Renderer); @@ -214,6 +221,51 @@ namespace Strawberry } } + // Load Images + + private void LoadSprites() + { + let root = scope String(ContentRoot); + root.Append(Path.DirectorySeparatorChar); + root.Append("Sprites"); + if (Directory.Exists(root)) + LoadSpritesDir(root); + else + Console.WriteLine("Content/Sprites folder does not exist!"); + } + + private void LoadSpritesDir(String directory) + { + for (let dir in Directory.EnumerateDirectories(directory)) + { + let path = scope String(); + dir.GetFilePath(path); + LoadSpritesDir(path); + } + + for (let file in Directory.EnumerateFiles(directory, "*.ase")) + { + let path = scope String(); + file.GetFilePath(path); + + let sprite = new [Friend]Sprite(path); + + path.Remove(0, ContentRoot.Length + 8); + Sprites.Add(new String(path), sprite); + } + } + + private void DisposeSprites() + { + for (let kv in Sprites) + { + delete kv.key; + delete kv.value; + } + + delete Sprites; + } + // Type assignable caching private void BuildTypeLists() diff --git a/src/Core/Sprite.bf b/src/Core/Sprite.bf new file mode 100644 index 0000000..b162d41 --- /dev/null +++ b/src/Core/Sprite.bf @@ -0,0 +1,604 @@ +using System; +using SDL2; +using System.IO; +using System.Text; +using System.Collections; +using MiniZ; + +namespace Strawberry +{ + public class Sprite + { + private enum Modes + { + Indexed = 1, + Grayscale = 2, + RGBA = 4, + } + + private enum Chunks + { + OldPaletteA = 0x0004, + OldPaletteB = 0x0011, + Layer = 0x2004, + Cel = 0x2005, + CelExtra = 0x2006, + Mask = 0x2016, + Path = 0x2017, + FrameTags = 0x2018, + Palette = 0x2019, + UserData = 0x2020, + Slice = 0x2022 + } + + public readonly String Path; + + private Frame[] frames; + private List layers; + private List tags; + private List slices; + private int width; + private int height; + private Modes mode; + + private this(String path) + { + Path = new String(path); + Load(); + } + + public ~this() + { + delete Path; + Unload(); + } + + private void Unload() + { + for (let f in frames) + delete f; + delete frames; + + for (let l in layers) + delete l; + delete layers; + + for (let t in tags) + delete t; + delete tags; + + for (let s in slices) + delete s; + delete slices; + } + + public void Reload() + { + Unload(); + Load(); + } + + private void Load() + { + /* + Aseprite file loading based on code from Noel Berry's Foster Framework here: + https://github.com/NoelFB/Foster/blob/master/Framework/Graphics/Images/Aseprite.cs + */ + + let stream = scope FileStream(); + stream.Open(Path, .Read, .Read); + + //Helpers to match ASE file format spec + uint8 BYTE() => stream.Read(); + uint16 WORD() => stream.Read(); + int16 SHORT() => stream.Read(); + uint32 DWORD() => stream.Read(); + int32 LONG() => stream.Read(); + void SEEK(int bytes) => stream.Position += bytes; + void BYTES(uint8[] into, int bytes) + { + for (let i < bytes) + into[i] = BYTE(); + } + String STRING(String into) + { + let len = WORD(); + let arr = scope uint8[len]; + for (let i < len) + arr[i] = BYTE(); + + Encoding.UTF8.DecodeToUTF8(arr, into); + return into; + } + + //Parse + { + //Header + { + //File Size + DWORD(); + + // Magic number + var magic = WORD(); + if (magic != 0xA5E0) + Runtime.FatalError("File is not in .ase format"); + + // Frame Count / Width / Height / Color Mode + frames = new Frame[WORD()]; + width = WORD(); + height = WORD(); + mode = (Modes)(WORD() / 8); + + // Other Info, Ignored + DWORD(); // Flags + WORD(); // Speed (deprecated) + DWORD(); // Set be 0 + DWORD(); // Set be 0 + BYTE(); // Palette entry + SEEK(3); // Ignore these bytes + WORD(); // Number of colors (0 means 256 for old sprites) + BYTE(); // Pixel width + BYTE(); // Pixel height + SEEK(92); // For Future + } + + layers = new List(); + tags = new List(); + slices = new List(); + + // Body + { + var temp = scope:: uint8[width * height * (int)mode]; + let palette = scope:: Color[256]; + HasUserData last = null; + + for (int i = 0; i < frames.Count; i++) + { + let frame = new Frame(width, height); + frames[i] = frame; + + int64 frameStart, frameEnd; + int chunkCount; + + // frame header + { + frameStart = stream.Position; + frameEnd = frameStart + DWORD(); + WORD(); // Magic number (always 0xF1FA) + chunkCount = WORD(); // Number of "chunks" in this frame + frame.Duration = WORD(); // Frame duration (in milliseconds) + SEEK(6); // For future (set to zero) + } + + // chunks + for (int j = 0; j < chunkCount; j++) + { + int64 chunkStart, chunkEnd; + Chunks chunkType; + + // chunk header + { + chunkStart = stream.Position; + chunkEnd = chunkStart + DWORD(); + chunkType = (Chunks)WORD(); + } + + // LAYER CHUNK + if (chunkType == Chunks.Layer) + { + // create layer + var layer = new Layer(); + + // get layer data + layer.Flag = (Layer.Flags)WORD(); + layer.Type = (Layer.Types)WORD(); + layer.ChildLevel = WORD(); + WORD(); // width (unused) + WORD(); // height (unused) + layer.BlendMode = WORD(); + layer.Alpha = (BYTE() / 255f); + SEEK(3); // for future + STRING(layer.Name); + + layers.Add(layer); + } + // CEL CHUNK + else if (chunkType == Chunks.Cel) Cel: + { + var layer = layers[WORD()]; + var x = SHORT(); + var y = SHORT(); + var alpha = BYTE() / 255f; + var celType = WORD(); + var width = 0; + var height = 0; + Color[] pixels = null; + Cel link = null; + + SEEK(7); + + // RAW or DEFLATE + if (celType == 0 || celType == 2) + { + width = WORD(); + height = WORD(); + + var count = width * height * (int)mode; + if (count > temp.Count) + temp = scope:: uint8[count]; + + // RAW + if (celType == 0) + { + BYTES(temp, count); + } + // DEFLATE + else + { + //TODO: Figure this out to enable aseprite loading + + Runtime.FatalError("Decompression not yet implemented."); + + /* + Noel's C#: + SEEK(2); + + using var deflate = new DeflateStream(reader.BaseStream, CompressionMode.Decompress, true); + deflate.Read(temp, 0, count); + */ + } + + // get pixel data + pixels = scope:Cel Color[width * height]; + BytesToPixels(temp, pixels, mode, palette); + } + // REFERENCE + else if (celType == 1) + { + var linkFrame = frames[WORD()]; + var linkCel = linkFrame.Cels[frame.Cels.Count]; + + width = linkCel.Width; + height = linkCel.Height; + pixels = linkCel.Pixels; + link = linkCel; + } + else + Runtime.FatalError("Cel type not yet implemented"); + + var cel = new Cel(layer, pixels); + cel.X = x; + cel.Y = y; + cel.Width = width; + cel.Height = height; + cel.Alpha = alpha; + cel.Link = link; + + // draw to frame if visible + if (cel.Layer.Visible) + CelToFrame(frame, cel); + + frame.Cels.Add(cel); + } + // PALETTE CHUNK + else if (chunkType == Chunks.Palette) + { + DWORD(); //size (unused) + var start = DWORD(); + var end = DWORD(); + SEEK(8); // for future + + for (int p = 0; p < (end - start) + 1; p++) + { + var hasName = WORD(); + palette[start + p] = Color(BYTE(), BYTE(), BYTE(), BYTE()); + + if (Calc.BitCheck(hasName, 0)) + STRING(scope String()); + } + } + // USERDATA + else if (chunkType == Chunks.UserData) + { + if (last != null) + { + var flags = (int)DWORD(); + + // has text + if (Calc.BitCheck(flags, 0)) + STRING(last.UserDataText); + + // has color + if (Calc.BitCheck(flags, 1)) + last.UserDataColor = Color(BYTE(), BYTE(), BYTE(), BYTE()); + } + } + // TAG + else if (chunkType == Chunks.FrameTags) + { + var count = WORD(); + SEEK(8); + + for (int t = 0; t < count; t++) + { + var tag = new Tag(); + tag.From = WORD(); + tag.To = WORD(); + tag.LoopDirection = (Tag.LoopDirections)BYTE(); + SEEK(8); + tag.Color = Color(BYTE(), BYTE(), BYTE(), (uint8)255); + SEEK(1); + STRING(tag.Name); + tags.Add(tag); + } + } + // SLICE + else if (chunkType == Chunks.Slice) + { + var count = DWORD(); + var flags = (int)DWORD(); + DWORD(); // reserved + var name = STRING(scope String()); + + for (int s = 0; s < count; s++) + { + var slice = new Slice(); + slice.Name.Set(name); + slice.Frame = (int)DWORD(); + slice.OriginX = (int)LONG(); + slice.OriginY = (int)LONG(); + slice.Width = (int)DWORD(); + slice.Height = (int)DWORD(); + + // 9 slice (ignored atm) + if (Calc.BitCheck(flags, 0)) + { + slice.NineSlice = Rect( + (int)LONG(), + (int)LONG(), + (int)DWORD(), + (int)DWORD()); + } + + // pivot point + if (Calc.BitCheck(flags, 1)) + slice.Pivot = Point((int)DWORD(), (int)DWORD()); + + slices.Add(slice); + } + } + + stream.Position = chunkEnd; + } + + stream.Position = frameEnd; + frame.FinishLoad(); + } + } + } + + stream.Close(); + } + + private void BytesToPixels(uint8[] bytes, Color[] pixels, Modes mode, Color[] palette) + { + int len = pixels.Count; + if (mode == Modes.RGBA) + { + for (int p = 0, int b = 0; p < len; p++, b += 4) + { + pixels[p].R = (uint8)(bytes[b + 0] * bytes[b + 3] / 255); + pixels[p].G = (uint8)(bytes[b + 1] * bytes[b + 3] / 255); + pixels[p].B = (uint8)(bytes[b + 2] * bytes[b + 3] / 255); + pixels[p].A = bytes[b + 3]; + } + } + else if (mode == Modes.Grayscale) + { + for (int p = 0, int b = 0; p < len; p++, b += 2) + { + pixels[p].R = pixels[p].G = pixels[p].B = (uint8)(bytes[b + 0] * bytes[b + 1] / 255); + pixels[p].A = bytes[b + 1]; + } + } + else if (mode == Modes.Indexed) + { + for (int p = 0; p < len; p++) + pixels[p] = palette[bytes[p]]; + } + } + + private void CelToFrame(Frame frame, Cel cel) + { + let opacity = (uint8)((cel.Alpha * cel.Layer.Alpha) * 255); + var pxLen = frame.PixelsLength; + + var blend = BlendModes[0]; + if (cel.Layer.BlendMode < BlendModes.Count) + blend = BlendModes[cel.Layer.BlendMode]; + + for (int sx = Math.Max(0, -cel.X), int right = Math.Min(cel.Width, width - cel.X); sx < right; sx++) + { + int dx = cel.X + sx; + int dy = cel.Y * width; + + for (int sy = Math.Max(0, -cel.Y), int bottom = Math.Min(cel.Height, height - cel.Y); sy < bottom; sy++, dy += width) + { + if (dx + dy >= 0 && dx + dy < pxLen) + blend(frame.Pixels, (dx + dy) * 4, cel.Pixels[sx + sy * cel.Width], opacity); + } + } + } + + // Data + + private class Frame + { + public SDL.Texture* Texture; + public float Duration; + public List Cels = new List() ~ delete _; + public uint8* Pixels; + public int32 PixelsLength; + + public this(int w, int h) + { + Texture = SDL.CreateTexture(Game.Renderer, (uint32)SDL.PIXELFORMAT_ARGB8888, (int32)SDL.TextureAccess.Static, (int32)w, (int32)h); + + void* ptr; + SDL.LockTexture(Texture, null, out ptr, out PixelsLength); + Pixels = (uint8*)ptr; + } + + public ~this() + { + SDL.DestroyTexture(Texture); + } + + public void FinishLoad() + { + SDL.UnlockTexture(Texture); + Pixels = null; + } + } + + public class Tag + { + public enum LoopDirections + { + Forward = 0, + Reverse = 1, + PingPong = 2 + } + + public String Name = new String() ~ delete _; + public LoopDirections LoopDirection; + public int From; + public int To; + public Color Color; + } + + public class HasUserData + { + public String UserDataText = new String("") ~ delete _; + public Color UserDataColor; + } + + private class Slice : HasUserData + { + public int Frame; + public String Name = new String() ~ delete _; + public int OriginX; + public int OriginY; + public int Width; + public int Height; + public Point? Pivot; + public Rect? NineSlice; + } + + private class Cel : HasUserData + { + public Layer Layer; + public Color[] Pixels; + public Cel Link; + + public int X; + public int Y; + public int Width; + public int Height; + public float Alpha; + + public this(Layer layer, Color[] pixels) + { + Layer = layer; + Pixels = pixels; + } + } + + private class Layer : HasUserData + { + public enum Flags + { + Visible = 1, + Editable = 2, + LockMovement = 4, + Background = 8, + PreferLinkedCels = 16, + Collapsed = 32, + Reference = 64 + } + + public enum Types + { + Normal = 0, + Group = 1 + } + + public Flags Flag; + public Types Type; + public String Name = new String() ~ delete _; + public int ChildLevel; + public int BlendMode; + public float Alpha; + public bool Visible { get { return Flag.HasFlag(Flags.Visible); } } + } + + // Blend Modes + + // More or less copied from Aseprite's source code: + // https://github.com/aseprite/aseprite/blob/master/src/doc/blend_funcs.cpp + + private delegate void Blend(uint8* dest, int index, Color src, uint8 opacity); + + private static void Dispose() + { + for (let b in BlendModes) + delete b; + delete BlendModes; + } + + private static readonly Blend[] BlendModes = new Blend[] + { + // 0 - NORMAL + new (dest, index, src, opacity) => + { + if (src.A != 0) + { + uint8 a = dest[index]; + uint8 r = dest[index + 1]; + uint8 g = dest[index + 2]; + uint8 b = dest[index + 3]; + + if (dest[index] == 0) + { + a = src.A; + r = src.R; + g = src.G; + b = src.B; + } + else + { + var sa = MUL_UN8(src.A, opacity); + var ra = a + sa - MUL_UN8(a, sa); + + a = (uint8)ra; + r = (uint8)(r + (src.R - r) * sa / ra); + g = (uint8)(g + (src.G - g) * sa / ra); + b = (uint8)(b + (src.B - b) * sa / ra); + } + + dest[index] = a; + dest[index + 1] = r; + dest[index + 2] = g; + dest[index + 3] = b; + } + } + }; + + [Inline] + private static int MUL_UN8(int a, int b) + { + var t = (a * b) + 0x80; + return (((t >> 8) + t) >> 8); + } + } +} diff --git a/src/Static/Calc.bf b/src/Static/Calc.bf index 8f5eb9b..a5046e3 100644 --- a/src/Static/Calc.bf +++ b/src/Static/Calc.bf @@ -5,6 +5,12 @@ namespace Strawberry { static public class Calc { + [Inline] + static public bool BitCheck(int bits, int pos) + { + return (bits & (1 << pos)) != 0; + } + //Move toward a target value without crossing it [Inline] static public float Approach(float value, float target, float maxDelta)