aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.compiled-res/player-ase_frames.qoibin0 -> 3581 bytes
-rw-r--r--.compiled-res/player-sheet.qoibin0 -> 4094 bytes
-rw-r--r--.compiled-res/player.qoibin0 -> 3581 bytes
-rw-r--r--.compiled-res/tilesets.qoi (renamed from res/tilesets.qoi)bin2998 -> 2998 bytes
-rw-r--r--README2
-rw-r--r--res/ase/player.asebin6589 -> 0 bytes
-rw-r--r--res/img/player.asebin0 -> 5482 bytes
-rw-r--r--res/img/tilesets.png (renamed from res/tilesets.png)bin2904 -> 2904 bytes
-rw-r--r--res/player.json226
-rw-r--r--res/player.qoibin4226 -> 0 bytes
-rwxr-xr-xrun.sh3
-rwxr-xr-xrun_debug.sh1
-rwxr-xr-xrun_release.sh1
-rwxr-xr-xsrc/assets.odin78
-rw-r--r--src/resources.odin27
-rw-r--r--tools/compile_assets/aseprite/LICENSE22
-rw-r--r--tools/compile_assets/aseprite/README.md106
-rw-r--r--tools/compile_assets/aseprite/ase.odin5
-rw-r--r--tools/compile_assets/aseprite/common.odin256
-rw-r--r--tools/compile_assets/aseprite/marshal.odin549
-rw-r--r--tools/compile_assets/aseprite/mod.pkg8
-rw-r--r--tools/compile_assets/aseprite/read.odin324
-rw-r--r--tools/compile_assets/aseprite/read_chunk.odin423
-rw-r--r--tools/compile_assets/aseprite/types.odin539
-rw-r--r--tools/compile_assets/aseprite/unmarshal.odin444
-rw-r--r--tools/compile_assets/aseprite/util.odin149
-rw-r--r--tools/compile_assets/aseprite/utils/animation.odin181
-rw-r--r--tools/compile_assets/aseprite/utils/blend.odin727
-rw-r--r--tools/compile_assets/aseprite/utils/common.odin303
-rw-r--r--tools/compile_assets/aseprite/utils/extract.odin613
-rw-r--r--tools/compile_assets/aseprite/utils/image.odin443
-rw-r--r--tools/compile_assets/aseprite/utils/sprite_sheet.odin382
-rw-r--r--tools/compile_assets/aseprite/utils/types.odin306
-rw-r--r--tools/compile_assets/aseprite/utils/utils_internal.odin110
-rw-r--r--tools/compile_assets/aseprite/write.odin304
-rw-r--r--tools/compile_assets/loaders.odin199
-rw-r--r--tools/compile_assets/main.odin328
37 files changed, 6830 insertions, 229 deletions
diff --git a/.compiled-res/player-ase_frames.qoi b/.compiled-res/player-ase_frames.qoi
new file mode 100644
index 0000000..1b2b8f0
--- /dev/null
+++ b/.compiled-res/player-ase_frames.qoi
Binary files differ
diff --git a/.compiled-res/player-sheet.qoi b/.compiled-res/player-sheet.qoi
new file mode 100644
index 0000000..4c91f6f
--- /dev/null
+++ b/.compiled-res/player-sheet.qoi
Binary files differ
diff --git a/.compiled-res/player.qoi b/.compiled-res/player.qoi
new file mode 100644
index 0000000..1b2b8f0
--- /dev/null
+++ b/.compiled-res/player.qoi
Binary files differ
diff --git a/res/tilesets.qoi b/.compiled-res/tilesets.qoi
index ecc869f..ecc869f 100644
--- a/res/tilesets.qoi
+++ b/.compiled-res/tilesets.qoi
Binary files differ
diff --git a/README b/README
index 3d5e6a5..df07ffc 100644
--- a/README
+++ b/README
@@ -1 +1 @@
-Will need sokol. Place it inside the shared directory given by Odin.
+A metroidvania
diff --git a/res/ase/player.ase b/res/ase/player.ase
deleted file mode 100644
index 4f637ae..0000000
--- a/res/ase/player.ase
+++ /dev/null
Binary files differ
diff --git a/res/img/player.ase b/res/img/player.ase
new file mode 100644
index 0000000..47a7dfa
--- /dev/null
+++ b/res/img/player.ase
Binary files differ
diff --git a/res/tilesets.png b/res/img/tilesets.png
index 4a431fe..4a431fe 100644
--- a/res/tilesets.png
+++ b/res/img/tilesets.png
Binary files differ
diff --git a/res/player.json b/res/player.json
deleted file mode 100644
index 4974248..0000000
--- a/res/player.json
+++ /dev/null
@@ -1,226 +0,0 @@
-{ "frames": [
- {
- "filename": "player 0.ase",
- "frame": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 200
- },
- {
- "filename": "player 1.ase",
- "frame": { "x": 16, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 2.ase",
- "frame": { "x": 32, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 3.ase",
- "frame": { "x": 48, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 4.ase",
- "frame": { "x": 64, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 5.ase",
- "frame": { "x": 80, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 6.ase",
- "frame": { "x": 96, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 75
- },
- {
- "filename": "player 7.ase",
- "frame": { "x": 112, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 75
- },
- {
- "filename": "player 8.ase",
- "frame": { "x": 128, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 75
- },
- {
- "filename": "player 9.ase",
- "frame": { "x": 144, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 75
- },
- {
- "filename": "player 10.ase",
- "frame": { "x": 160, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 75
- },
- {
- "filename": "player 11.ase",
- "frame": { "x": 176, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 75
- },
- {
- "filename": "player 12.ase",
- "frame": { "x": 192, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 75
- },
- {
- "filename": "player 13.ase",
- "frame": { "x": 208, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 75
- },
- {
- "filename": "player 14.ase",
- "frame": { "x": 224, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 15.ase",
- "frame": { "x": 240, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 16.ase",
- "frame": { "x": 256, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 17.ase",
- "frame": { "x": 272, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 18.ase",
- "frame": { "x": 288, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 19.ase",
- "frame": { "x": 304, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 20.ase",
- "frame": { "x": 320, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 21.ase",
- "frame": { "x": 336, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- },
- {
- "filename": "player 22.ase",
- "frame": { "x": 352, "y": 0, "w": 16, "h": 17 },
- "rotated": false,
- "trimmed": false,
- "spriteSourceSize": { "x": 0, "y": 0, "w": 16, "h": 17 },
- "sourceSize": { "w": 16, "h": 17 },
- "duration": 100
- }
- ],
- "meta": {
- "app": "http://www.aseprite.org/",
- "version": "1.x-dev",
- "image": "player.qoi",
- "format": "RGBA8888",
- "size": { "w": 368, "h": 17 },
- "scale": "1",
- "frameTags": [
- { "name": "idle", "from": 0, "to": 5, "direction": "forward", "color": "#fe5b59ff" },
- { "name": "run", "from": 6, "to": 13, "direction": "forward", "color": "#f7a547ff" },
- { "name": "jump_up", "from": 14, "to": 15, "direction": "forward", "color": "#6acd5bff" },
- { "name": "jump_trans", "from": 16, "to": 16, "direction": "forward", "color": "#6acd5bff" },
- { "name": "jump_down", "from": 17, "to": 18, "direction": "forward", "color": "#6acd5bff" },
- { "name": "sleep", "from": 19, "to": 22, "direction": "forward", "color": "#57b9f2ff" }
- ]
- }
-}
diff --git a/res/player.qoi b/res/player.qoi
deleted file mode 100644
index 11ebb7b..0000000
--- a/res/player.qoi
+++ /dev/null
Binary files differ
diff --git a/run.sh b/run.sh
index 508b340..d54b1d0 100755
--- a/run.sh
+++ b/run.sh
@@ -1 +1,2 @@
-odin run src -disallow-do -debug -sanitize:address
+odin run ./tools/compile_assets -- res src/assets.odin &&
+odin run src -disallow-do $1
diff --git a/run_debug.sh b/run_debug.sh
new file mode 100755
index 0000000..c56ad41
--- /dev/null
+++ b/run_debug.sh
@@ -0,0 +1 @@
+./run.sh "-debug -sanitize:address"
diff --git a/run_release.sh b/run_release.sh
deleted file mode 100755
index 06e8a5f..0000000
--- a/run_release.sh
+++ /dev/null
@@ -1 +0,0 @@
-odin run . -vet -disallow-do -o:speed
diff --git a/src/assets.odin b/src/assets.odin
new file mode 100755
index 0000000..e74dcf8
--- /dev/null
+++ b/src/assets.odin
@@ -0,0 +1,78 @@
+#+feature dynamic-literals
+package demonchime
+
+// DO NOT EDIT
+//
+// This file is autogenerated by tools/compile_assets
+// All resource types are defined in 'src/resources.odin'.
+
+import rl "vendor:raylib"
+
+Image_Id :: enum {
+ TILESETS,
+ PLAYER,
+}
+
+Animation_Id :: enum {
+ PLAYER,
+}
+
+Map_Id :: enum {
+ ROOM_BEGIN_1,
+ ROOM_BEGIN,
+}
+
+Tileset_Id :: enum {
+ TILESET,
+}
+
+Resource_Id :: union {
+ Image_Id,
+ Animation_Id,
+ Map_Id,
+ Tileset_Id,
+}
+
+images: [Image_Id]Image_Resource
+animations: [Animation_Id]Animation_Resource
+maps: [Map_Id]Map_Resource
+tilesets: [Tileset_Id]Tileset_Resource
+
+path_to_id: map[string]Resource_Id
+
+load_resources :: proc() {
+ load_images()
+ load_anims()
+ load_maps()
+ load_tilesets()
+
+ // Allow conversion from paths to a resource id, since it's a better way to
+ // reference resources in other resources (JSON is a good example).
+ path_to_id["res/tileset.tsj"] = Tileset_Id.TILESET
+ path_to_id["res/img/player.ase"] = Image_Id.PLAYER
+ path_to_id["res/img/tilesets.png"] = Image_Id.TILESETS
+ path_to_id["res/room_begin.tmj"] = Map_Id.ROOM_BEGIN
+ path_to_id["res/room_begin_1.tmj"] = Map_Id.ROOM_BEGIN_1
+}
+
+@(private="file")
+load_images :: proc() {
+ images[.TILESETS] = {data = #load("/home/xswan/demonchime/.compiled-res/tilesets.qoi")}
+ images[.PLAYER] = {data = #load("/home/xswan/demonchime/.compiled-res/player-sheet.qoi")}
+}
+
+@(private="file")
+load_anims :: proc() {
+ animations[.PLAYER] = {frame_count = 23, frame_durations = {100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100}, tags = {"jump_trans"={from = 16, to = 16}, "jump_down"={from = 17, to = 18}, "idle"={from = 0, to = 5}, "sleep"={from = 19, to = 22}, "jump_up"={from = 14, to = 15}, "run"={from = 6, to = 13}}}
+}
+
+@(private="file")
+load_maps :: proc() {
+ maps[.ROOM_BEGIN_1] = #load("/home/xswan/demonchime/res/room_begin_1.tmj")
+ maps[.ROOM_BEGIN] = #load("/home/xswan/demonchime/res/room_begin.tmj")
+}
+
+@(private="file")
+load_tilesets :: proc() {
+ tilesets[.TILESET] = #load("/home/xswan/demonchime/res/tileset.tsj")
+}
diff --git a/src/resources.odin b/src/resources.odin
new file mode 100644
index 0000000..6120812
--- /dev/null
+++ b/src/resources.odin
@@ -0,0 +1,27 @@
+package demonchime
+
+import rl "vendor:raylib"
+
+Image_Resource :: struct {
+ texture: rl.Texture2D,
+ anim: Animation_Id,
+ data: []u8,
+}
+
+Tag_Resource :: struct {
+ from: i32,
+ to: i32,
+}
+
+Animation_Resource :: struct {
+ frame_count: i32,
+ frame_durations: []i32,
+ tags: map[string]Tag_Resource,
+}
+
+Map_Resource :: struct {
+}
+
+Tileset_Resource :: struct {
+}
+
diff --git a/tools/compile_assets/aseprite/LICENSE b/tools/compile_assets/aseprite/LICENSE
new file mode 100644
index 0000000..70dab4e
--- /dev/null
+++ b/tools/compile_assets/aseprite/LICENSE
@@ -0,0 +1,22 @@
+zlib/libpng License
+
+Copyright (c) 2025 blob1807
+
+This software is provided ‘as-is’, without any express or implied
+warranty. In no event will the authors be held liable for any damages
+arising from the use of this software.
+
+Permission is granted to anyone to use this software for any purpose,
+including commercial applications, and to alter it and redistribute it
+freely, subject to the following restrictions:
+
+1. The origin of this software must not be misrepresented; you must not
+claim that you wrote the original software. If you use this software
+in a product, an acknowledgment in the product documentation would be
+appreciated but is not required.
+
+2. Altered source versions must be plainly marked as such, and must not be
+misrepresented as being the original software.
+
+3. This notice may not be removed or altered from any source
+distribution.
diff --git a/tools/compile_assets/aseprite/README.md b/tools/compile_assets/aseprite/README.md
new file mode 100644
index 0000000..a43b1f1
--- /dev/null
+++ b/tools/compile_assets/aseprite/README.md
@@ -0,0 +1,106 @@
+# Odin Aseprite
+Handler for Aseprite's `.ase`/`.aseprite`, `.aseprite-extension` & extended `.gpl` files writen in Odin.
+
+* `.\`: Main un/marshaler for `.ase`
+* `.\utils`: Creates Images, Animations & Sprite Sheets from Documents
+* `.\raw`: un/marshals `.ase` exactly as given by the spec
+* `.\gpl`: extended & normal .gpl
+* `.\extensions`: .aseprite-extension. WIP
+* `.\tests`: test files
+
+## Examples
+### aseprite
+```odin
+package main
+
+import "core:fmt"
+import ase "odin-aseprite"
+
+main :: proc() {
+ data := #load("geralt.aseprite")
+
+ doc: ase.Document
+ defer ase.destroy_doc(&doc)
+
+ umerr := ase.unmarshal(data[:], &doc)
+ if umerr != nil {
+ fmt.println(umerr)
+ return
+ }
+
+ buf: [dynamic]byte
+ defer delete(buf)
+
+ written, merr := ase.marshal(&buf, &doc)
+ if merr != nil {
+ fmt.println(merr)
+ return
+ }
+}
+```
+
+### utils
+```odin
+package main
+
+import "core:fmt"
+import ase "odin-aseprite"
+import "odin-aseprite/utils"
+
+main :: proc() {
+ data := #load("geralt.aseprite")
+
+ doc: ase.Document
+ defer ase.destroy_doc(&doc)
+
+ umerr := ase.unmarshal(data[:], &doc)
+ if umerr != nil {
+ fmt.println(umerr)
+ return
+ }
+
+ imgs, imgs_err := utils.get_all_images(&doc)
+ defer utils.destroy(imgs)
+
+ if imgs_err != nil {
+ fmt.println(imgs_err)
+ return
+ }
+}
+```
+
+### gpl
+```odin
+package main
+
+import "core:fmt"
+import "odin-aseprite/gpl"
+
+main :: proc() {
+ data := #load("geralt.gpl")
+
+ palette, err := gpl.parse(data[:])
+ if err != nil {
+ fmt.println(err)
+ return
+ }
+ defer destroy_gpl(&palette)
+
+ buf, err2 := gpl.to_bytes(palette)
+ if err2 != nil {
+ fmt.println(err2)
+ return
+ }
+ defer delete(buf)
+}
+```
+
+
+## Warnings
+ICC Colour Profiles aren't supported. The raw data will be saved to doc.
+
+## Errors
+Any errors please make an issue or DM them to me, `blob1807`, on the [Odin Discord](https://discord.com/invite/sVBPHEv).
+If you DM me please include the offending file/s.
+
+If you want to test your own files for errors. Add them to a new folder in `./tests` and run `odin test .` in the `./tests` directory.
diff --git a/tools/compile_assets/aseprite/ase.odin b/tools/compile_assets/aseprite/ase.odin
new file mode 100644
index 0000000..d0de0d0
--- /dev/null
+++ b/tools/compile_assets/aseprite/ase.odin
@@ -0,0 +1,5 @@
+package aseprite_file_handler
+
+// https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md
+// https://github.com/alpine-alpaca/asefile
+// https://github.com/AristurtleDev/AsepriteDotNet/blob/main/source/AsepriteDotNet/Document/UserData.cs
diff --git a/tools/compile_assets/aseprite/common.odin b/tools/compile_assets/aseprite/common.odin
new file mode 100644
index 0000000..ef32bba
--- /dev/null
+++ b/tools/compile_assets/aseprite/common.odin
@@ -0,0 +1,256 @@
+package aseprite_file_handler
+
+import "base:runtime"
+import "core:mem/virtual"
+
+@(require) import "core:log"
+
+
+ASE_DEBUG_MODE :: #config(ASE_DEBUG, ODIN_DEBUG)
+
+
+@(private)
+destroy_value :: proc(p: ^Property_Value, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ context.allocator = alloc
+ #partial switch &val in p {
+ case string:
+ delete(val) or_return
+
+ case UD_Vec:
+ for &v in val {
+ destroy_value(&v) or_return
+ }
+ delete(val) or_return
+
+ case Properties:
+ for _, &v in val {
+ destroy_value(&v) or_return
+ }
+ delete(val) or_return
+ }
+
+ return
+}
+
+destroy_doc :: proc(doc: ^Document) {
+ virtual.arena_destroy(&doc.arena)
+}
+
+
+destroy_doc_alloc :: proc(doc: ^Document, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ context.allocator = alloc
+ for &frame in doc.frames {
+ for &chunk in frame.chunks {
+ #partial switch &v in chunk {
+ case Old_Palette_256_Chunk:
+ for pack in v {
+ delete(pack.colors)
+ }
+ delete(v)
+
+ case Old_Palette_64_Chunk:
+ for pack in v {
+ delete(pack.colors)
+ }
+ delete(v)
+ case Layer_Chunk:
+ delete(v.name)
+
+ case Cel_Chunk:
+ switch &cel in v.cel {
+ case Linked_Cel:
+ case Raw_Cel:
+ delete(cel.pixels)
+ case Com_Image_Cel:
+ delete(cel.pixels)
+ case Com_Tilemap_Cel:
+ delete(cel.tiles)
+ }
+
+ case Color_Profile_Chunk:
+ switch icc in v.icc {
+ case ICC_Profile:
+ delete(icc)
+ }
+
+ case External_Files_Chunk:
+ for &e in v {
+ delete(e.file_name_or_id)
+ }
+ delete(v)
+
+ case Mask_Chunk:
+ delete(v.name)
+ delete(v.bit_map_data)
+
+ case Tags_Chunk:
+ for &t in v {
+ delete(t.name)
+ }
+ delete(v)
+
+ case Palette_Chunk:
+ for &e in v.entries {
+ switch &s in e.name {
+ case string:
+ delete(s)
+ }
+ }
+ delete(v.entries)
+
+ case User_Data_Chunk:
+ switch &s in v.text {
+ case string:
+ delete(s)
+ }
+
+ switch &m in v.maps {
+ case Properties_Map:
+ for _, &val in m {
+ destroy_value(&val)
+ }
+ delete_map(m)
+ }
+
+ case Slice_Chunk:
+ delete(v.name)
+ delete(v.keys)
+
+ case Tileset_Chunk:
+ delete(v.name)
+ switch &c in v.compressed {
+ case Tileset_Compressed:
+ delete(c)
+ }
+ }
+ }
+ delete(frame.chunks)
+ }
+ return delete(doc.frames)
+}
+
+
+destroy_chunk :: proc {
+ _destroy_old_256, _destroy_old_64, _destroy_layer, _destroy_cel,
+ _destroy_cel_extra, _destroy_color_profile, _destroy_external_files,
+ _destroy_mask, _destroy_path, _destroy_tags, _destroy_palette,
+ _destroy_user_data, _destroy_slice, _destroy_tileset,
+}
+
+@(private)
+_destroy_old_256 :: proc(c: Old_Palette_256_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ for pack in c {
+ delete(pack.colors, alloc) or_return
+ }
+ return delete(c, alloc)
+}
+
+@(private)
+_destroy_old_64 :: proc(c: Old_Palette_64_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ for pack in c {
+ delete(pack.colors, alloc) or_return
+ }
+ return delete(c, alloc)
+}
+
+@(private)
+_destroy_layer :: proc(c: Layer_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ return delete(c.name, alloc)
+}
+
+@(private)
+_destroy_cel :: proc(c: Cel_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ switch cel in c.cel {
+ case Linked_Cel:
+ case Raw_Cel:
+ delete(cel.pixels, alloc) or_return
+ case Com_Image_Cel:
+ delete(cel.pixels, alloc) or_return
+ case Com_Tilemap_Cel:
+ delete(cel.tiles, alloc) or_return
+ }
+ return
+}
+
+@(private)
+_destroy_cel_extra :: proc(c: Cel_Extra_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ return
+}
+
+@(private)
+_destroy_color_profile :: proc(c: Color_Profile_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ switch icc in c.icc {
+ case ICC_Profile:
+ delete(icc, alloc) or_return
+ }
+ return
+}
+
+@(private)
+_destroy_external_files :: proc(c: External_Files_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ for e in c {
+ delete(e.file_name_or_id, alloc) or_return
+ }
+ return delete(c, alloc)
+}
+
+@(private)
+_destroy_mask :: proc(c: Mask_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ delete(c.name, alloc) or_return
+ return delete(c.bit_map_data, alloc)
+}
+
+@(private)
+_destroy_path :: proc(c: Path_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ return
+}
+
+@(private)
+_destroy_tags :: proc(c: Tags_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ for t in c {
+ delete(t.name, alloc) or_return
+ }
+ return delete(c, alloc)
+}
+
+@(private)
+_destroy_palette :: proc(c: Palette_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ for e in c.entries {
+ switch s in e.name {
+ case string: delete(s, alloc) or_return
+ }
+ }
+ return delete(c.entries, alloc)
+}
+
+@(private)
+_destroy_user_data :: proc(c: User_Data_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ switch &s in c.text {
+ case string:
+ delete(s, alloc) or_return
+ }
+
+ switch &m in c.maps {
+ case Properties_Map:
+ for _, &val in m {
+ destroy_value(&val, alloc) or_return
+ }
+ delete_map(m) or_return
+ }
+ return
+}
+
+@(private)
+_destroy_slice :: proc(c: Slice_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ delete(c.name, alloc) or_return
+ return delete(c.keys, alloc)
+}
+
+@(private)
+_destroy_tileset :: proc(c: Tileset_Chunk, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ switch v in c.compressed {
+ case Tileset_Compressed:
+ delete(v, alloc) or_return
+ }
+ return delete(c.name, alloc)
+}
diff --git a/tools/compile_assets/aseprite/marshal.odin b/tools/compile_assets/aseprite/marshal.odin
new file mode 100644
index 0000000..1b56de1
--- /dev/null
+++ b/tools/compile_assets/aseprite/marshal.odin
@@ -0,0 +1,549 @@
+package aseprite_file_handler
+
+import "core:io"
+import "core:os"
+import "core:log"
+import "core:bytes"
+import "core:bufio"
+import "vendor:zlib"
+
+
+marshal_to_bytes_buff :: proc(doc: ^Document, b: ^bytes.Buffer, allocator := context.allocator)-> (file_size: int, err: Marshal_Error) {
+ w, ok := io.to_writer(bytes.buffer_to_stream(b))
+ if !ok {
+ return file_size, .Unable_Make_Writer
+ }
+ return marshal(doc, w, allocator)
+}
+
+marshal_to_handle :: proc(doc: ^Document, h: os.Handle, allocator := context.allocator)-> (file_size: int, err: Marshal_Error) {
+ w, ok := io.to_writer(os.stream_from_handle(h))
+ if !ok {
+ return file_size, .Unable_Make_Writer
+ }
+ return marshal(doc, w, allocator)
+}
+
+marshal_to_slice :: proc(doc: ^Document, b: []byte, allocator := context.allocator)-> (file_size: int, err: Marshal_Error) {
+ buf: bytes.Buffer
+ defer bytes.buffer_destroy(&buf)
+ file_size = marshal(doc, &buf, allocator) or_return
+ if len(b) < len(buf.buf[buf.off:]) {
+ return file_size, Marshal_Errors.Buffer_Not_Big_Enough
+ }
+ copy_slice(b[:], buf.buf[buf.off:])
+ return
+}
+
+marshal_to_dynamic :: proc(doc: ^Document, b: ^[dynamic]byte, allocator := context.allocator)-> (file_size: int, err: Marshal_Error) {
+ buf: bytes.Buffer
+ defer bytes.buffer_destroy(&buf)
+ file_size = marshal(doc, &buf, allocator) or_return
+ append(b, ..buf.buf[:])
+ return
+}
+
+marshal_to_bufio :: proc(doc: ^Document, w: ^bufio.Writer, allocator := context.allocator) -> (file_size: int, err: Marshal_Error) {
+ ww, ok := io.to_writer(bufio.writer_to_stream(w))
+ if !ok {
+ return file_size, .Unable_Make_Writer
+ }
+ return marshal(doc, ww, allocator)
+}
+
+marshal :: proc{
+ marshal_to_bytes_buff, marshal_to_slice, marshal_to_handle,
+ marshal_to_dynamic, marshal_to_bufio, marshal_to_writer,
+}
+
+marshal_to_writer :: proc(doc: ^Document, ww: io.Writer, allocator := context.allocator) -> (file_size: int, err: Marshal_Error) {
+ ud_map_warn: bool
+ s := &file_size
+ b: bytes.Buffer
+ defer bytes.buffer_destroy(&b)
+
+ w, ok := io.to_writer(bytes.buffer_to_stream(&b))
+ if !ok {
+ return file_size, .Unable_Make_Writer
+ }
+
+ write(w, FILE_MAGIC_NUM, s) or_return
+ write(w, WORD(len(doc.frames)), s) or_return
+ write(w, doc.header.width, s) or_return
+ write(w, doc.header.height, s) or_return
+ write(w, WORD(doc.header.color_depth), s) or_return
+ write(w, transmute(DWORD)doc.header.flags, s) or_return
+ write(w, doc.header.speed, s) or_return
+ write_skip(w, 8, s) or_return
+ write(w, doc.header.transparent_index, s) or_return
+ write_skip(w, 3, s) or_return
+ write(w, doc.header.num_of_colors, s) or_return
+ write(w, doc.header.ratio_width, s) or_return
+ write(w, doc.header.ratio_height, s) or_return
+ write(w, doc.header.x, s) or_return
+ write(w, doc.header.y, s) or_return
+ write(w, doc.header.grid_width, s) or_return
+ write(w, doc.header.grid_height, s) or_return
+ write_skip(w, 84, s) or_return
+
+ for frame in doc.frames {
+ fb: bytes.Buffer
+ defer bytes.buffer_destroy(&fb)
+
+ fw, ok2 := io.to_writer(bytes.buffer_to_stream(&fb))
+ if !ok2 {
+ return file_size, .Unable_Make_Writer
+ }
+
+ frame_size: int
+ fs := &frame_size
+
+ write(fw, FRAME_MAGIC_NUM, fs) or_return
+ write(fw, frame.header.old_num_of_chunks, fs) or_return
+ write(fw, frame.header.duration, fs) or_return
+ write_skip(fw, 2, fs) or_return
+ write(fw, frame.header.num_of_chunks, fs) or_return
+
+ for chunk in frame.chunks {
+ cb: bytes.Buffer
+ defer bytes.buffer_destroy(&cb)
+
+ cw, ok3 := io.to_writer(bytes.buffer_to_stream(&cb))
+ if !ok3 {
+ return file_size, .Unable_Make_Writer
+ }
+
+ chunk_size: int
+ cs := &chunk_size
+
+ chunk_type := get_chunk_type(chunk) or_return
+ write(cw, chunk_type, cs) or_return
+
+ switch val in chunk {
+ case Old_Palette_256_Chunk:
+ write(cw, WORD(len(val)), cs) or_return
+
+ for p in val {
+ write(cw, p.entries_to_skip, cs) or_return
+ if len(p.colors) > 256 {
+ return file_size, .Invalid_Old_Palette
+
+ } else if len(p.colors) == 256 {
+ write_byte(cw, 0, cs) or_return
+
+ } else {
+ write(cw, p.num_colors, cs) or_return
+ }
+
+ for c in p.colors {
+ write(cw, c[2], cs) or_return
+ write(cw, c[1], cs) or_return
+ write(cw, c[0], cs) or_return
+ }
+ }
+
+ case Old_Palette_64_Chunk:
+ write(cw, WORD(len(val)), cs) or_return
+
+ for p in val {
+ write(cw, p.entries_to_skip, cs) or_return
+
+ if len(p.colors) > 256 {
+ return file_size, .Invalid_Old_Palette
+
+ } else if len(p.colors) == 256 {
+ write_byte(cw, 0, cs) or_return
+
+ } else {
+ write(cw, p.num_colors, cs) or_return
+ }
+
+ for c in p.colors {
+ write(cw, c[2], cs) or_return
+ write(cw, c[1], cs) or_return
+ write(cw, c[0], cs) or_return
+ }
+ }
+ case Layer_Chunk:
+ write(cw, transmute(WORD)val.flags, cs) or_return
+ write(cw, WORD(val.type), cs) or_return
+ write(cw, val.child_level, cs) or_return
+ write(cw, val.default_width, cs) or_return
+ write(cw, val.default_height, cs) or_return
+ write(cw, WORD(val.blend_mode), cs) or_return
+ write(cw, val.opacity, cs) or_return
+ write_skip(cw, 3, cs) or_return
+ write(cw, val.name, cs) or_return
+
+ if val.type == .Tilemap {
+ write(cw, val.tileset_index, cs) or_return
+ }
+
+ case Cel_Chunk:
+ write(cw, val.layer_index, cs) or_return
+ write(cw, val.x, cs) or_return
+ write(cw, val.y, cs) or_return
+ write(cw, val.opacity_level, cs) or_return
+
+ cel_type := get_cel_type(val.cel) or_return
+ write(cw, cel_type, cs) or_return
+ write(cw, val.z_index, cs) or_return
+ write_skip(cw, 5, cs) or_return
+
+ switch cel in val.cel {
+ case Raw_Cel:
+ write(cw, cel.width, cs) or_return
+ write(cw, cel.height, cs) or_return
+ write(cw, cel.pixels[:], cs) or_return
+
+ case Linked_Cel:
+ write(cw, WORD(cel), cs) or_return
+
+ case Com_Image_Cel:
+ write(cw, cel.width, cs) or_return
+ write(cw, cel.height, cs) or_return
+
+ com_buf := make([]byte, len(cel.pixels)+64, allocator) or_return
+ defer delete(com_buf)
+
+ data_rd: [^]u8 = raw_data(cel.pixels[:])
+ com_buf_rd: [^]u8 = raw_data(com_buf[:])
+
+ config := zlib.z_stream {
+ avail_in=zlib.uInt(len(cel.pixels)),
+ next_in=&data_rd[0],
+ avail_out=zlib.uInt(len(com_buf)),
+ next_out=&com_buf_rd[0],
+ }
+
+ en := zlib.deflateInit(&config, zlib.DEFAULT_COMPRESSION)
+ if en < zlib.OK {
+ return file_size, ZLIB_Errors(en)
+ }
+
+ en = zlib.deflate(&config, zlib.FINISH)
+ if en < zlib.OK {
+ return file_size, ZLIB_Errors(en)
+ }
+
+ en = zlib.deflateEnd(&config)
+ if en < zlib.OK {
+ return file_size, ZLIB_Errors(en)
+ }
+
+ write(cw, com_buf[:int(config.total_out)], cs) or_return
+
+ case Com_Tilemap_Cel:
+ write(cw, cel.width, cs) or_return
+ write(cw, cel.height, cs) or_return
+ write(cw, cel.bits_per_tile, cs) or_return
+ write(cw, DWORD(cel.bitmask_id), cs) or_return
+ write(cw, cel.bitmask_x, cs) or_return
+ write(cw, cel.bitmask_y, cs) or_return
+ write(cw, cel.bitmask_diagonal, cs) or_return
+ write_skip(cw, 10, cs) or_return
+
+ buf := make([]u8, len(cel.tiles)*4, allocator) or_return
+ defer delete(buf)
+
+ n := tiles_to_u8(cel.tiles[:], buf[:]) or_return
+ com_buf := make([]byte, n+64, allocator) or_return
+ defer delete(com_buf)
+
+ data_rd: [^]u8 = raw_data(buf[:n])
+ com_buf_rd: [^]u8 = raw_data(com_buf[:])
+
+ config := zlib.z_stream {
+ avail_in=zlib.uInt(n),
+ next_in=&data_rd[0],
+ avail_out=zlib.uInt(len(com_buf)),
+ next_out=&com_buf_rd[0],
+ }
+
+ en := zlib.deflateInit(&config, zlib.DEFAULT_COMPRESSION)
+ if en < zlib.OK {
+ return file_size, ZLIB_Errors(en)
+ }
+
+ en = zlib.deflate(&config, zlib.FINISH)
+ if en < zlib.OK {
+ return file_size, ZLIB_Errors(en)
+ }
+
+ en = zlib.deflateEnd(&config)
+ if en < zlib.OK {
+ return file_size, ZLIB_Errors(en)
+ }
+
+ write(cw, com_buf[:int(config.total_out)], cs) or_return
+
+ case:
+ return file_size, .Invalid_Cel_Type
+ }
+
+ case Cel_Extra_Chunk:
+ write(cw, transmute(WORD)val.flags, cs) or_return
+ write(cw, val.x, cs) or_return
+ write(cw, val.y, cs) or_return
+ write(cw, val.width, cs) or_return
+ write(cw, val.width, cs) or_return
+ write_skip(cw, 16, cs) or_return
+
+ case Color_Profile_Chunk:
+ if val.icc != nil {
+ write(cw, WORD(Color_Profile_Type.ICC), cs) or_return
+ } else {
+ write(cw, WORD(val.type), cs) or_return
+ }
+
+ write(cw, transmute(WORD)val.flags, cs) or_return
+ write(cw, val.fixed_gamma, cs) or_return
+ write_skip(cw, 8, cs) or_return
+
+ #partial switch v in val.icc {
+ case ICC_Profile:
+ write(cw, DWORD(len(v)), cs) or_return
+ write(cw, cast([]u8)v[:], cs) or_return
+ }
+
+ case External_Files_Chunk:
+ write(cw, DWORD(len(val)), cs) or_return
+ write_skip(cw, 8, cs) or_return
+
+ for file in val {
+ write(cw, file.id, cs) or_return
+ write(cw, BYTE(file.type), cs) or_return
+ write_skip(cw, 7, cs) or_return
+ write(cw, file.file_name_or_id, cs) or_return
+ }
+
+ case Mask_Chunk:
+ write(cw, val.x, cs) or_return
+ write(cw, val.y, cs) or_return
+ write(cw, val.width, cs) or_return
+ write(cw, val.height, cs) or_return
+ write_skip(cw, 8, cs) or_return
+ write(cw, val.name, cs) or_return
+ write(cw, val.bit_map_data[:], cs) or_return
+
+ case Path_Chunk:
+
+ case Tags_Chunk:
+ write(cw, WORD(len(val)), cs) or_return
+ write_skip(cw, 8, cs) or_return
+
+ for tag in val {
+ write(cw, tag.from_frame, cs) or_return
+ write(cw, tag.to_frame, cs) or_return
+ write(cw, BYTE(tag.loop_direction), cs) or_return
+ write(cw, tag.repeat, cs) or_return
+ write_skip(cw, 6, cs) or_return
+ write(cw, tag.tag_color[2], cs) or_return
+ write(cw, tag.tag_color[1], cs) or_return
+ write(cw, tag.tag_color[0], cs) or_return
+ write(cw, BYTE(0), cs) or_return
+ write(cw, tag.name, cs) or_return
+ }
+
+ case Palette_Chunk:
+ write(cw, DWORD(len(val.entries)), cs) or_return
+ write(cw, val.first_index, cs) or_return
+ write(cw, val.last_index, cs) or_return
+ write_skip(cw, 8, cs) or_return
+
+ for entry in val.entries {
+ if entry.name != nil {
+ write(cw, WORD(1), cs) or_return
+ } else {
+ write(cw, WORD(0), cs) or_return
+ }
+
+ write(cw, entry.color[3], cs) or_return
+ write(cw, entry.color[2], cs) or_return
+ write(cw, entry.color[1], cs) or_return
+ write(cw, entry.color[0], cs) or_return
+
+ #partial switch v in entry.name {
+ case string:
+ write(cw, v, cs) or_return
+ }
+ }
+
+ case User_Data_Chunk:
+ flags: UD_Flags
+ if val.text != nil { flags += {.Text} }
+ if val.color != nil { flags += {.Color} }
+ if val.maps != nil { flags += {.Properties} }
+
+ write(cw, transmute(DWORD)flags, cs) or_return
+
+ switch v in val.text {
+ case string:
+ write(cw, v, cs) or_return
+ }
+
+ switch v in val.color {
+ case Color_RGBA:
+ write(cw, v[3], cs) or_return
+ write(cw, v[2], cs) or_return
+ write(cw, v[1], cs) or_return
+ write(cw, v[0], cs) or_return
+ }
+
+ switch m in val.maps {
+ case Properties_Map:
+ if !ud_map_warn {
+ log.warn("Writing User Data Maps may still have bugs.")
+ ud_map_warn = true
+ }
+
+ mb: bytes.Buffer
+ defer bytes.buffer_destroy(&mb)
+ mw, ok4 := io.to_writer(bytes.buffer_to_stream(&mb))
+ if !ok4 {
+ return file_size, .Unable_Make_Writer
+ }
+ map_size: int
+ ms := &map_size
+ write(mw, DWORD(len(m)), ms) or_return
+
+ for key, val in m {
+ write(mw, key, ms) or_return
+ val := val.(Properties)
+ write(mw, DWORD(len(val)), ms)
+ for name, type in val {
+ _, err = write(mw, name, ms)
+ if err != nil {
+ log.error("Failed to write key", key)
+ return
+ }
+ write(mw, get_property_type(type) or_return, ms)
+ write(mw, type, ms) or_return
+ }
+ }
+
+ map_size += 4
+ write(cw, DWORD(map_size), cs) or_return
+ write(cw, mb.buf[:map_size-4], cs) or_return
+ }
+
+ case Slice_Chunk:
+ write(cw, DWORD(len(val.keys)), cs) or_return
+
+ flags := val.flags
+ if len(val.keys) != 0 {
+ key := val.keys[0]
+ if key.center != nil {
+ flags += {.Patched_slice}
+ }
+ if key.pivot != nil {
+ flags += {.Pivot_Information}
+ }
+ }
+ write(cw, transmute(DWORD)flags, cs) or_return
+
+ write(cw, DWORD(0), cs) or_return
+ write(cw, val.name, cs) or_return
+
+ for key in val.keys {
+ write(cw, key.frame_num, cs) or_return
+ write(cw, key.x, cs) or_return
+ write(cw, key.y, cs) or_return
+ write(cw, key.width, cs) or_return
+ write(cw, key.height, cs) or_return
+
+ #partial switch v in key.center {
+ case Slice_Center:
+ write(cw, v.x, cs) or_return
+ write(cw, v.y, cs) or_return
+ write(cw, v.width, cs) or_return
+ write(cw, v.height, cs) or_return
+ }
+
+ #partial switch v in key.pivot {
+ case Slice_Pivot:
+ write(cw, v.x, cs) or_return
+ write(cw, v.y, cs) or_return
+ }
+ }
+
+ case Tileset_Chunk:
+ write(cw, val.id, cs) or_return
+
+ flags := val.flags
+ if val.compressed != nil {
+ flags += {.Include_Tiles_Inside_This_File}
+ }
+ if val.external != nil {
+ flags += {.Include_Link_To_External_File}
+ }
+ write(cw, transmute(DWORD)flags, cs) or_return
+
+ write(cw, val.num_of_tiles, cs) or_return
+ write(cw, val.width, cs) or_return
+ write(cw, val.height, cs) or_return
+ write(cw, val.base_index, cs) or_return
+ write_skip(cw, 14, cs) or_return
+ write(cw, val.name, cs) or_return
+
+ #partial switch v in val.external {
+ case Tileset_External:
+ write(cw, v.file_id, cs) or_return
+ write(cw, v.tileset_id, cs) or_return
+ }
+
+ #partial switch v in val.compressed {
+ case Tileset_Compressed:
+ com_buf := make([]byte, len(v), allocator) or_return
+ defer delete(com_buf)
+ data_rd: [^]u8 = raw_data(v[:])
+ com_buf_rd: [^]u8 = raw_data(com_buf[:])
+
+ config := zlib.z_stream {
+ avail_in = zlib.uInt(len(v)),
+ next_in = &data_rd[0],
+ avail_out = zlib.uInt(len(v)),
+ next_out = &com_buf_rd[0],
+ }
+
+ en := zlib.deflateInit(&config, zlib.DEFAULT_COMPRESSION)
+ if en < zlib.OK {
+ return file_size, ZLIB_Errors(en)
+ }
+
+ en = zlib.deflate(&config, zlib.FINISH)
+ if en < zlib.OK {
+ return file_size, ZLIB_Errors(en)
+ }
+
+ en = zlib.deflateEnd(&config)
+ if en < zlib.OK {
+ return file_size, ZLIB_Errors(en)
+ }
+
+ write(cw, DWORD(config.total_out), cs) or_return
+ write(cw, com_buf[:int(config.total_out)], cs) or_return
+ }
+
+ case:
+ return file_size, .Invalid_Chunk_Type
+ }
+
+ write(fw, DWORD(chunk_size + 4), fs) or_return
+ write(fw, cb.buf[:chunk_size], fs) or_return
+ }
+
+ write(w, DWORD(frame_size + 4), s) or_return
+ write(w, fb.buf[:frame_size], s) or_return
+ }
+
+ written: int
+ file_size += 4
+ write(ww, DWORD(file_size), &written) or_return
+ write(ww, b.buf[:file_size-4], &written) or_return
+
+ if written != file_size {
+ return file_size, Marshal_Errors.Wrong_Write_Size
+ }
+ return
+} \ No newline at end of file
diff --git a/tools/compile_assets/aseprite/mod.pkg b/tools/compile_assets/aseprite/mod.pkg
new file mode 100644
index 0000000..c030f04
--- /dev/null
+++ b/tools/compile_assets/aseprite/mod.pkg
@@ -0,0 +1,8 @@
+{
+ "version": "0.0.1",
+ "description": "Handler for Aseprite's file formats.",
+ "url": "https://github.com/blob1807/odin-aseprite",
+ "readme": "README.md",
+ "license": "BSD 3-Clause",
+ "keywords": ["Aseprite", "ase"]
+}
diff --git a/tools/compile_assets/aseprite/read.odin b/tools/compile_assets/aseprite/read.odin
new file mode 100644
index 0000000..68864f7
--- /dev/null
+++ b/tools/compile_assets/aseprite/read.odin
@@ -0,0 +1,324 @@
+package aseprite_file_handler
+
+import "core:io"
+import "core:log"
+import "core:encoding/endian"
+
+read_bool :: proc(r: io.Reader, n: ^int) -> (data: bool, err: Read_Error) {
+ return bool(read_byte(r, n) or_return), nil
+}
+
+read_i8 :: proc(r: io.Reader, n: ^int) -> (data: i8, err: Read_Error) {
+ return i8(read_byte(r, n) or_return), nil
+}
+
+read_byte :: proc(r: io.Reader, n: ^int) -> (data: BYTE, err: Read_Error) {
+ data, err = io.read_byte(r, n)
+ if err != nil {
+ log.error("Failed to read byte/i8/bool", n^)
+ }
+ return
+}
+
+read_word :: proc(r: io.Reader, n: ^int) -> (data: WORD, err: Read_Error) {
+ buf: [2]byte
+ s := io.read(r, buf[:], n) or_return
+ if s != 2 {
+ log.error("Failed to read word", s, n^)
+ return 0, .Wrong_Read_Size
+ }
+
+ v, ok := endian.get_u16(buf[:], .Little)
+ if !ok {
+ err = .Unable_To_Decode_Data
+ }
+ return v, err
+}
+
+read_short :: proc(r: io.Reader, n: ^int) -> (data: SHORT, err: Read_Error) {
+ buf: [2]byte
+ s := io.read(r, buf[:], n) or_return
+ if s != 2 {
+ log.error("Failed to read short", s, n^)
+ return 0, .Wrong_Read_Size
+ }
+
+ v, ok := endian.get_i16(buf[:], .Little)
+ if !ok {
+ err = .Unable_To_Decode_Data
+ }
+ return v, err
+}
+
+read_dword :: proc(r: io.Reader, n: ^int) -> (data: DWORD, err: Read_Error) {
+ buf: [4]byte
+ s := io.read(r, buf[:], n) or_return
+ if s != 4 {
+ log.error("Failed to read dword", s, n^)
+ return 0, .Wrong_Read_Size
+ }
+
+ v, ok := endian.get_u32(buf[:], .Little)
+ if !ok {
+ err = .Unable_To_Decode_Data
+ } return v, err
+}
+
+read_long :: proc(r: io.Reader, n: ^int) -> (data: LONG, err: Read_Error) {
+ buf: [4]byte
+ s := io.read(r, buf[:], n) or_return
+ if s != 4 {
+ log.error("Failed to read long", s, n^)
+ return 0, .Wrong_Read_Size
+ }
+
+ v, ok := endian.get_i32(buf[:], .Little)
+ if !ok {
+ err = .Unable_To_Decode_Data
+ }
+ return v, err
+}
+
+read_fixed :: proc(r: io.Reader, n: ^int) -> (data: FIXED, err: Read_Error) {
+ buf: [4]byte
+ s := io.read(r, buf[:], n) or_return
+ if s != 4 {
+ log.error("Failed to read fixed", s, n^)
+ return data, .Wrong_Read_Size
+ }
+
+ /*vi, ok_i := endian.get_i16(buf[:2], .Little)
+ if !ok_i {
+ err = .Unable_To_Decode_Data
+ return
+ }
+
+ vf, ok_f := endian.get_i16(buf[2:], .Little)
+ if !ok_f {
+ err = .Unable_To_Decode_Data
+ return
+ }
+ fixed.init_from_parts(&data, i32(vi), i32(vf))*/
+
+ v, ok := endian.get_i32(buf[:], .Little)
+ if !ok {
+ err = .Unable_To_Decode_Data
+ return
+ }
+ data.i = v
+ return
+}
+
+read_float :: proc(r: io.Reader, n: ^int) -> (data: FLOAT, err: Read_Error) {
+ buf: [4]byte
+ s := io.read(r, buf[:], n) or_return
+ if s != 42 {
+ log.error("Failed to read float", s, n^)
+ return 0, .Wrong_Read_Size
+ }
+
+ v, ok := endian.get_f32(buf[:], .Little)
+ if !ok {
+ err = .Unable_To_Decode_Data
+ }
+ return v, err
+}
+
+read_double :: proc(r: io.Reader, n: ^int) -> (data: DOUBLE, err: Read_Error) {
+ buf: [8]byte
+ s := io.read(r, buf[:], n) or_return
+ if s != 8 {
+ log.error("Failed to read double", s, n^)
+ return 0, .Wrong_Read_Size
+ }
+
+ v, ok := endian.get_f64(buf[:], .Little)
+ if !ok {
+ err = .Unable_To_Decode_Data
+ }
+ return v, err
+}
+
+read_qword :: proc(r: io.Reader, n: ^int) -> (data: QWORD, err: Read_Error) {
+ buf: [8]byte
+ s := io.read(r, buf[:], n) or_return
+ if s != 8 {
+ log.error("Failed to read qword", s, n^)
+ return 0, .Wrong_Read_Size
+ }
+
+ v, ok := endian.get_u64(buf[:], .Little)
+ if !ok {
+ err = .Unable_To_Decode_Data
+ }
+ return v, err
+}
+
+read_long64 :: proc(r: io.Reader, n: ^int) -> (data: LONG64, err: Read_Error) {
+ buf: [8]byte
+ s := io.read(r, buf[:], n) or_return
+ if s != 8 {
+ log.error("Failed to read long64", s, n^)
+ return 0, .Wrong_Read_Size
+ }
+
+ v, ok := endian.get_i64(buf[:], .Little)
+ if !ok {
+ err = .Unable_To_Decode_Data
+ }
+ return v, err
+}
+
+read_string :: proc(r: io.Reader, n: ^int, alloc := context.allocator, loc := #caller_location) -> (data: STRING, err: Read_Error) {
+ size := int(read_word(r, n) or_return)
+ if size == 0 {
+ return
+ }
+
+ buf := make([]byte, size, alloc) or_return
+ s: int
+ s, err = io.read(r, buf, n)
+ if err != nil {
+ log.error("Failed to read string", size, err, n^, loc)
+ return
+ }
+ if s != size {
+ log.error("Unexpected string size", size, s, n^, loc)
+ err = .Wrong_Read_Size
+ return
+ }
+
+ data = string(buf)
+ return
+}
+
+read_point :: proc(r: io.Reader, n: ^int) -> (data: POINT, err: Read_Error) {
+ data.x = read_long(r, n) or_return
+ data.y = read_long(r, n) or_return
+ return
+}
+
+read_size :: proc(r: io.Reader, n: ^int) -> (data: SIZE, err: Read_Error) {
+ data.w = read_long(r, n) or_return
+ data.h = read_long(r, n) or_return
+ return
+}
+
+read_rect :: proc(r: io.Reader, n: ^int) -> (data: RECT, err: Read_Error) {
+ data.origin = read_point(r, n) or_return
+ data.size = read_size(r, n) or_return
+ return
+}
+
+read_uuid:: proc(r: io.Reader, n: ^int) -> (data: UUID, err: Read_Error) {
+ s := io.read(r, data[:], n) or_return
+ if s != 16 {
+ log.error("Failed to read UUID", s, data, n^)
+ err = .Wrong_Read_Size
+ }
+ return
+}
+
+read_pixel :: proc(r: io.Reader, n: ^int) -> (data: PIXEL, err: Read_Error) {
+ return read_byte(r, n)
+}
+
+read_pixels :: proc(r: io.Reader, data: []PIXEL, n: ^int) -> (err: Read_Error) {
+ return read_bytes(r, data[:], n)
+}
+
+read_tile :: proc(r: io.Reader, type: Tile_ID, n: ^int) -> (data: TILE, err: Read_Error) {
+ switch type {
+ case .byte: data = read_byte(r, n) or_return
+ case .word: data = read_word(r, n) or_return
+ case .dword: data = read_dword(r, n) or_return
+ }
+ return
+}
+
+read_tiles :: proc(r: io.Reader, data: []TILE, type: Tile_ID, n: ^int) -> (err: Read_Error) {
+ size := len(data)
+ if len(data) == 0 {
+ return
+ }
+
+ for i in 0..<size {
+ data[i] = read_tile(r, type, n) or_return
+ }
+ return
+}
+
+read_bytes :: proc(r: io.Reader, data: []byte, n: ^int) -> (err: Read_Error) {
+ s := io.read(r, data, n) or_return
+ if s != len(data) {
+ log.error("Could read all the bytes asked.", s, len(data))
+ err = .Wrong_Read_Size
+ }
+ return
+}
+
+read_skip :: proc(r: io.Reader, to_skip: int, n: ^int) -> (err: Read_Error) {
+ for _ in 0..<to_skip {
+ io.read_byte(r, n) or_return
+ }
+ return
+}
+
+read_ud_value :: proc(r: io.Reader, type: Property_Type, n: ^int, alloc := context.allocator) -> (val: Property_Value, err: Unmarshal_Error) {
+ context.allocator = alloc
+
+ switch type {
+ case .Null: return nil, nil
+ case .Bool: return read_bool(r, n)
+ case .I8: return read_i8(r, n)
+ case .U8: return read_byte(r, n)
+ case .I16: return read_short(r, n)
+ case .U16: return read_word(r, n)
+ case .I32: return read_long(r, n)
+ case .U32: return read_dword(r, n)
+ case .I64: return read_long64(r, n)
+ case .U64: return read_qword(r, n)
+ case .Fixed: return read_fixed(r, n)
+ case .F32: return read_float(r, n)
+ case .F64: return read_double(r, n)
+ case .String: return read_string(r, n)
+ case .Point: return read_point(r, n)
+ case .Size: return read_size(r, n)
+ case .Rect: return read_rect(r, n)
+ case .UUID: return read_uuid (r, n)
+
+ case .Vector:
+ num := int(read_dword(r, n) or_return)
+ val = make(UD_Vec, num) or_return
+ vec_type := Property_Type(read_word(r, n) or_return)
+
+ if vec_type == .Null {
+ for i in 0..<num {
+ prop_type := Property_Type(read_word(r, n) or_return)
+ val.(UD_Vec)[i] = read_ud_value(r, prop_type, n) or_return
+ }
+
+ } else {
+ for i in 0..<num {
+ val.(UD_Vec)[i] = read_ud_value(r, vec_type, n) or_return
+ }
+ }
+
+ case .Properties:
+ size := read_dword(r, n) or_return
+ val = make(Properties, size) or_return
+
+ #partial switch &v in val {
+ case Properties:
+ for _ in 0..<size {
+ key := read_string(r, n) or_return
+ defer delete(key)
+ prop_type := Property_Type(read_word(r, n) or_return)
+ v[key] = read_ud_value(r, prop_type, n) or_return
+ }
+ }
+ }
+
+ return
+}
+
diff --git a/tools/compile_assets/aseprite/read_chunk.odin b/tools/compile_assets/aseprite/read_chunk.odin
new file mode 100644
index 0000000..9f7839e
--- /dev/null
+++ b/tools/compile_assets/aseprite/read_chunk.odin
@@ -0,0 +1,423 @@
+package aseprite_file_handler
+
+import "base:intrinsics"
+import "core:io"
+import "core:log"
+import "core:bytes"
+import "core:compress/zlib"
+
+
+read_file_header :: proc(r: io.Reader, rt: ^int) -> (h: File_Header, err: Unmarshal_Error) {
+ h.size = read_dword(r, rt) or_return
+
+ if io.Stream_Mode.Size in io.query(r) {
+ stream_size := io.size(r) or_return
+ if stream_size != i64(h.size) {
+ return {}, .Data_Size_Not_Equal_To_Header
+ }
+ }
+
+ magic := read_word(r, rt) or_return
+ if magic != FILE_MAGIC_NUM {
+ return {}, .Bad_File_Magic_Number
+ }
+
+ h.frames = read_word(r, rt) or_return
+
+ h.width = read_word(r, rt) or_return
+ h.height = read_word(r, rt) or_return
+ h.color_depth = Color_Depth(read_word(r, rt) or_return)
+
+ h.flags = transmute(File_Flags)read_dword(r, rt) or_return
+ h.speed = read_word(r, rt) or_return
+ read_skip(r, 4+4, rt) or_return
+
+ h.transparent_index = read_byte(r, rt) or_return
+ read_skip(r, 3, rt) or_return
+
+ h.num_of_colors = read_word(r, rt) or_return
+ h.ratio_width = read_byte(r, rt) or_return
+ h.ratio_height = read_byte(r, rt) or_return
+
+ h.x = read_short(r, rt) or_return
+ h.y = read_short(r, rt) or_return
+
+ h.grid_width = read_word(r, rt) or_return
+ h.grid_height = read_word(r, rt) or_return
+ read_skip(r, 84, rt) or_return
+
+ return
+}
+
+
+read_old_palette_256 :: proc(r: io.Reader, rt: ^int, allocator := context.allocator) -> (chunk: Old_Palette_256_Chunk, err: Unmarshal_Error) {
+ op_size := cast(int)read_word(r, rt) or_return
+ chunk = make(Old_Palette_256_Chunk, op_size, allocator) or_return
+
+ for &packet in chunk {
+ packet.entries_to_skip = read_byte(r, rt) or_return
+ packet.num_colors = read_byte(r, rt) or_return
+ count := int(packet.num_colors)
+ if count == 0 {
+ count = 256
+ }
+
+ packet.colors = make([]Color_RGB, count, allocator) or_return
+ for &c in packet.colors {
+ read_bytes(r, c[:], rt) or_return
+ }
+ }
+ return
+}
+
+read_old_palette_64 :: proc(r: io.Reader, rt: ^int, allocator := context.allocator) -> (chunk: Old_Palette_64_Chunk, err: Unmarshal_Error) {
+ op_size := cast(int)read_word(r, rt) or_return
+ chunk = make(Old_Palette_64_Chunk, op_size, allocator) or_return
+
+ for &packet in chunk {
+ packet.entries_to_skip = read_byte(r, rt) or_return
+ packet.num_colors = read_byte(r, rt) or_return
+ count := int(packet.num_colors)
+ if count == 0 {
+ count = 256
+ }
+
+ packet.colors = make([]Color_RGB, count, allocator) or_return
+ for &c in packet.colors {
+ read_bytes(r, c[:], rt) or_return
+ }
+ }
+ return
+}
+
+read_layer :: proc(r: io.Reader, rt: ^int, has_uuid: bool, allocator := context.allocator) -> (chunk: Layer_Chunk, err: Unmarshal_Error) {
+ chunk.flags = transmute(Layer_Chunk_Flags)read_word(r, rt) or_return
+ chunk.type = Layer_Types(read_word(r, rt) or_return)
+ chunk.child_level = read_word(r, rt) or_return
+ chunk.default_width = read_word(r, rt) or_return
+ chunk.default_height = read_word(r, rt) or_return
+ chunk.blend_mode = Layer_Blend_Mode(read_word(r, rt) or_return)
+ chunk.opacity = read_byte(r, rt) or_return
+ read_skip(r, 3, rt) or_return
+ chunk.name = read_string(r, rt, allocator) or_return
+
+ if chunk.type == .Tilemap {
+ chunk.tileset_index = read_dword(r, rt) or_return
+ }
+ if has_uuid {
+ chunk.uuid = read_uuid(r, rt) or_return
+ }
+
+ return
+}
+
+read_cel :: proc(r: io.Reader, rt: ^int, color_depth: int, c_size: int, allocator := context.allocator) -> (chunk: Cel_Chunk, err: Unmarshal_Error) {
+ context.allocator = allocator
+ chunk.layer_index = read_word(r, rt) or_return
+ chunk.x = read_short(r, rt) or_return
+ chunk.y = read_short(r, rt) or_return
+ chunk.opacity_level = read_byte(r, rt) or_return
+ chunk.type = Cel_Types(read_word(r, rt) or_return)
+ chunk.z_index = read_short(r, rt) or_return
+ read_skip(r, 5, rt) or_return
+
+ switch chunk.type {
+ case .Raw:
+ cel: Raw_Cel
+ cel.width = read_word(r, rt) or_return
+ cel.height = read_word(r, rt) or_return
+ cel.pixels = make([]PIXEL, int(cel.width * cel.height)) or_return
+ read_bytes(r, cel.pixels[:], rt) or_return
+ chunk.cel = cel
+
+ case .Linked_Cel:
+ chunk.cel = Linked_Cel(read_word(r, rt) or_return)
+
+ case .Compressed_Image:
+ cel: Com_Image_Cel
+ cel.width = read_word(r, rt) or_return
+ cel.height = read_word(r, rt) or_return
+
+ com_size := c_size-26
+ if com_size <= 0 {
+ err = .Invalid_Compression_Size
+ return
+ }
+
+ buf: bytes.Buffer
+ defer bytes.buffer_destroy(&buf)
+
+ data := make([]byte, com_size) or_return
+ defer delete(data)
+
+ read_bytes(r, data[:], rt) or_return
+
+ exp_size := color_depth / 8 * int(cel.height) * int(cel.width)
+ zlib.inflate(data[:], &buf, expected_output_size=exp_size) or_return
+
+ cel.pixels = make([]byte, exp_size) or_return
+ copy(cel.pixels[:], buf.buf[:])
+
+ chunk.cel = cel
+
+ case .Compressed_Tilemap:
+ cel: Com_Tilemap_Cel
+ cel.width = read_word(r, rt) or_return
+ cel.height = read_word(r, rt) or_return
+ cel.bits_per_tile = read_word(r, rt) or_return
+ cel.bitmask_id = Tile_ID(read_dword(r, rt) or_return)
+ cel.bitmask_x = read_dword(r, rt) or_return
+ cel.bitmask_y = read_dword(r, rt) or_return
+ cel.bitmask_diagonal = read_dword(r, rt) or_return
+ read_skip(r, 10, rt) or_return
+
+ buf: bytes.Buffer
+ defer bytes.buffer_destroy(&buf)
+ // size_of(DWORD*5, WORD*6, SHORT*3, BYTE, SKIPED*15)-1
+ com_size := c_size-54
+ if com_size <= 0 {
+ err = .Invalid_Compression_Size
+ return
+ }
+
+ data := make([]byte, com_size) or_return
+ defer delete(data)
+ read_bytes(r, data[:], rt) or_return
+ exp_size := color_depth / 8 * int(cel.height) * int(cel.width)
+ zlib.inflate(data[:], &buf, expected_output_size=exp_size) or_return
+
+ br: bytes.Reader
+ bytes.reader_init(&br, buf.buf[:])
+ rr, ok := io.to_reader(bytes.reader_to_stream(&br))
+ if !ok { err = .Unable_Make_Reader; return }
+
+ cel.tiles = make([]TILE, cel.height * cel.width) or_return
+ read_tiles(rr, cel.tiles[:], cel.bitmask_id, rt) or_return
+
+ chunk.cel = cel
+
+ case:
+ err = .Invalid_Cel_Type
+ return
+ }
+ return
+}
+
+read_cel_extra :: proc(r: io.Reader, rt: ^int) -> (chunk: Cel_Extra_Chunk, err: Unmarshal_Error) {
+ chunk.flags = transmute(Cel_Extra_Flags)read_word(r, rt) or_return
+ chunk.x = read_fixed(r, rt) or_return
+ chunk.y = read_fixed(r, rt) or_return
+ chunk.width = read_fixed(r, rt) or_return
+ chunk.height = read_fixed(r, rt) or_return
+ return
+}
+
+read_color_profile :: proc(r: io.Reader, rt: ^int, warned: ^bool, allocator := context.allocator) -> (chunk: Color_Profile_Chunk, err: Unmarshal_Error) {
+ chunk.type = Color_Profile_Type(read_word(r, rt) or_return)
+ chunk.flags = transmute(Color_Profile_Flags)read_word(r, rt) or_return
+ chunk.fixed_gamma = read_fixed(r, rt) or_return
+ read_skip(r, 8, rt) or_return
+
+ if chunk.type == .ICC {
+ icc_size := cast(int)read_dword(r, rt) or_return
+ chunk.icc = make(ICC_Profile, icc_size, allocator) or_return
+ read_bytes(r, cast([]u8)chunk.icc.(ICC_Profile)[:], rt) or_return
+ if !warned^ {
+ log.warn("Embedded ICC Color Profiles are currently not supported.")
+ warned^ = true
+ }
+ }
+ return
+}
+
+read_external_files :: proc(r: io.Reader, rt: ^int, allocator := context.allocator) -> (chunk: External_Files_Chunk, err: Unmarshal_Error) {
+ entries := read_dword(r, rt) or_return
+ chunk = make([]External_Files_Entry, entries, allocator) or_return
+ read_skip(r, 8, rt) or_return
+
+ for &entry in chunk {
+ entry.id = read_dword(r, rt) or_return
+ entry.type = ExF_Entry_Type(read_byte(r, rt) or_return)
+ read_skip(r, 7, rt) or_return
+ entry.file_name_or_id = read_string(r, rt, allocator) or_return
+ }
+ return
+}
+
+read_mask :: proc(r: io.Reader, rt: ^int, allocator := context.allocator) -> (chunk: Mask_Chunk, err: Unmarshal_Error) {
+ chunk.x = read_short(r, rt) or_return
+ chunk.y = read_short(r, rt) or_return
+ chunk.width = read_word(r, rt) or_return
+ chunk.height = read_word(r, rt) or_return
+ read_skip(r, 8, rt) or_return
+ chunk.name = read_string(r, rt, allocator) or_return
+
+ size := int(chunk.height) * ((int(chunk.width) + 7) / 8)
+ chunk.bit_map_data = make([]BYTE, size, allocator) or_return
+ read_bytes(r, chunk.bit_map_data[:], rt) or_return
+ return
+}
+
+read_path :: proc() -> (chunk: Path_Chunk) {
+ return
+}
+
+read_tags :: proc(r: io.Reader, rt: ^int, allocator := context.allocator) -> (chunk: Tags_Chunk, err: Unmarshal_Error) {
+ size := cast(int)read_word(r, rt) or_return
+ chunk = make([]Tag, size, allocator) or_return
+ read_skip(r, 8, rt) or_return
+
+ for &tag in chunk {
+ tag.from_frame = read_word(r, rt) or_return
+ tag.to_frame = read_word(r, rt) or_return
+ tag.loop_direction = Tag_Loop_Dir(read_byte(r, rt) or_return)
+ tag.repeat = read_word(r, rt) or_return
+
+ read_skip(r, 6, rt) or_return
+ read_bytes(r, tag.tag_color[:], rt) or_return
+ read_byte(r, rt) or_return
+ tag.name = read_string(r, rt, allocator) or_return
+ }
+
+ return
+}
+
+read_palette :: proc(r: io.Reader, rt: ^int, allocator := context.allocator) -> (chunk: Palette_Chunk, err: Unmarshal_Error) {
+ chunk.size = read_dword(r, rt) or_return
+ chunk.first_index = read_dword(r, rt) or_return
+ chunk.last_index = read_dword(r, rt) or_return
+ size := int(chunk.last_index - chunk.first_index + 1)
+ chunk.entries = make([]Palette_Entry, size, allocator) or_return
+ read_skip(r, 8, rt) or_return
+
+ for &entry in chunk.entries {
+ pf := transmute(Pal_Flags)read_word(r, rt) or_return
+ read_bytes(r, entry.color[:], rt) or_return
+
+ if .Has_Name in pf {
+ entry.name = read_string(r, rt, allocator) or_return
+ }
+ }
+
+ return
+}
+
+read_user_data :: proc(r: io.Reader, rt: ^int, allocator := context.allocator) -> (chunk: User_Data_Chunk, err: Unmarshal_Error) {
+ flags := transmute(UD_Flags)read_dword(r, rt) or_return
+
+ if .Text in flags {
+ chunk.text = read_string(r, rt) or_return
+ }
+
+ if .Color in flags {
+ colour: Color_RGBA
+ read_bytes(r, colour[:], rt) or_return
+ chunk.color = colour
+ }
+
+ if .Properties in flags {
+ //map_size := read_dword(r, rt) or_return
+ read_skip(r, 4, rt) or_return
+ map_num := read_dword(r, rt) or_return
+ maps := make(Properties_Map, map_num) or_return
+
+ for _ in 0..<map_num {
+ key := read_dword(r, rt) or_return
+
+ prop_num := int(read_dword(r, rt) or_return)
+ val := make(Properties, prop_num) or_return
+
+ for _ in 0..<prop_num {
+ name := read_string(r, rt) or_return
+ defer delete(name)
+ type := Property_Type(read_word(r, rt) or_return)
+ val[name] = read_ud_value(r, type, rt) or_return
+ }
+
+ maps[key] = val
+ }
+ chunk.maps = maps
+ }
+
+ return
+}
+
+read_slice :: proc(r: io.Reader, rt: ^int, alloc := context.allocator) -> (chunk: Slice_Chunk, err: Unmarshal_Error) {
+ context.allocator = alloc
+ keys := int(read_dword(r, rt) or_return)
+ chunk.flags = transmute(Slice_Flags)read_dword(r, rt) or_return
+ read_dword(r, rt) or_return
+ chunk.name = read_string(r, rt) or_return
+ chunk.keys = make([]Slice_Key, keys) or_return
+
+ for &key in chunk.keys {
+ key.frame_num = read_dword(r, rt) or_return
+ key.x = read_long(r, rt) or_return
+ key.y = read_long(r, rt) or_return
+ key.width = read_dword(r, rt) or_return
+ key.height = read_dword(r, rt) or_return
+
+ if .Patched_slice in chunk.flags {
+ cen: Slice_Center
+ cen.x = read_long(r, rt) or_return
+ cen.y = read_long(r, rt) or_return
+ cen.width = read_dword(r, rt) or_return
+ cen.height = read_dword(r, rt) or_return
+ key.center = cen
+ }
+
+ if .Pivot_Information in chunk.flags {
+ p: Slice_Pivot
+ p.x = read_long(r, rt) or_return
+ p.y = read_long(r, rt) or_return
+ key.pivot = p
+ }
+ }
+
+ return
+}
+
+read_tileset :: proc(r: io.Reader, rt: ^int, allocator := context.allocator) -> (chunk: Tileset_Chunk, err: Unmarshal_Error) {
+ chunk.id = read_dword(r, rt) or_return
+ chunk.flags = transmute(Tileset_Flags)read_dword(r, rt) or_return
+ chunk.num_of_tiles = read_dword(r, rt) or_return
+ chunk.width = read_word(r, rt) or_return
+ chunk.height = read_word(r, rt) or_return
+ chunk.base_index = read_short(r, rt) or_return
+ read_skip(r, 14, rt)
+ chunk.name = read_string(r, rt) or_return
+
+ if .Include_Link_To_External_File in chunk.flags {
+ ex: Tileset_External
+ ex.file_id = read_dword(r, rt) or_return
+ ex.tileset_id = read_dword(r, rt) or_return
+ chunk.external = ex
+ }
+
+ if .Include_Tiles_Inside_This_File in chunk.flags {
+ size := int(read_dword(r, rt) or_return)
+
+ buf: bytes.Buffer
+
+ data := make([]byte, size, allocator) or_return
+ defer delete(data)
+
+ read_bytes(r, data, rt) or_return
+
+ zlib.inflate_from_byte_array(data, &buf) or_return
+
+ res := len(buf.buf)-buf.off
+ exp_size := int(chunk.width) * int(chunk.height) * int(chunk.num_of_tiles)
+
+ if (res != exp_size) && (res != exp_size*2) && (res != exp_size*4) {
+ fast_log(.Error, "Expected size not equal to uncompressed size")
+ return chunk, Read_Error(Read_Errors.Comp_Tileset_Not_Expected_Size)
+ }
+
+ chunk.compressed = (Tileset_Compressed)(buf.buf[buf.off:])
+
+ }
+
+ return
+}
+
diff --git a/tools/compile_assets/aseprite/types.odin b/tools/compile_assets/aseprite/types.odin
new file mode 100644
index 0000000..7052f81
--- /dev/null
+++ b/tools/compile_assets/aseprite/types.odin
@@ -0,0 +1,539 @@
+package aseprite_file_handler
+
+
+import "base:runtime"
+import "core:io"
+import "core:os"
+import "core:math/fixed"
+import "core:mem/virtual"
+import "core:compress/zlib"
+import "core:encoding/uuid"
+
+
+import vzlib "vendor:zlib"
+
+//https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md
+
+Unmarshal_Errors :: enum {
+ None,
+ Unable_Make_Reader,
+ Bad_File_Magic_Number,
+ Bad_Frame_Magic_Number,
+ Bad_User_Data_Type,
+ Data_Size_Not_Equal_To_Header,
+ Invalid_Chunk_Type,
+ Invalid_Cel_Type,
+ Invalid_Compression_Size,
+
+ User_Data_Maps_Not_Supported,
+}
+Unmarshal_Error :: union #shared_nil {
+ Unmarshal_Errors,
+ Read_Error,
+ ZLIB_Errors,
+ runtime.Allocator_Error,
+ zlib.Error,
+ io.Error,
+ os.Error,
+}
+
+Read_Errors :: enum {
+ None,
+ Unable_To_Decode_Data,
+ Wrong_Read_Size,
+ Array_To_Small,
+ Unable_Make_Seeker,
+ Comp_Tileset_Not_Expected_Size,
+}
+Read_Error :: union #shared_nil {Read_Errors, io.Error, runtime.Allocator_Error}
+
+Marshal_Errors :: enum {
+ None,
+ Unable_Make_Writer,
+ Buffer_Not_Big_Enough,
+ Invalid_Chunk_Type,
+ Wrong_Write_Size,
+ Invalid_Old_Palette,
+ Invalid_Cel_Type,
+ Invalid_Property_Type,
+}
+Marshal_Error :: union #shared_nil {
+ Marshal_Errors,
+ Write_Error,
+ ZLIB_Errors,
+ io.Error,
+ runtime.Allocator_Error,
+}
+
+Write_Errors :: enum {
+ None,
+ Unable_To_Encode_Data,
+ Wrong_Write_Size,
+ Array_To_Small,
+ Unable_Make_Seeker,
+}
+Write_Error :: union #shared_nil {
+ Write_Errors,
+ io.Error,
+ runtime.Allocator_Error,
+}
+
+ZLIB_Errors :: enum(i32) {
+ ERRNO = vzlib.ERRNO,
+ STREAM_ERROR = vzlib.STREAM_ERROR,
+ DATA_ERROR = vzlib.DATA_ERROR,
+ MEM_ERROR = vzlib.MEM_ERROR,
+ BUF_ERROR = vzlib.BUF_ERROR,
+ VERSION_ERROR = vzlib.VERSION_ERROR,
+}
+
+// all writen in le
+BYTE :: u8
+WORD :: u16
+SHORT :: i16
+DWORD :: u32
+LONG :: i32
+FIXED :: fixed.Fixed16_16
+FLOAT :: f32
+DOUBLE :: f64
+QWORD :: u64
+LONG64 :: i64
+
+BYTE_N :: [dynamic]BYTE
+
+
+STRING :: string
+POINT :: struct {
+ x: LONG,
+ y: LONG,
+}
+SIZE :: struct {
+ w: LONG,
+ h: LONG,
+}
+RECT :: struct {
+ origin: POINT,
+ size: SIZE,
+}
+
+PIXEL_RGBA :: [4]BYTE
+PIXEL_GRAYSCALE :: [2]BYTE
+PIXEL_INDEXED :: BYTE
+
+// PIXEL :: union {PIXEL_RGBA, PIXEL_GRAYSCALE, PIXEL_INDEXED}
+PIXEL :: u8
+TILE :: union {BYTE, WORD, DWORD}
+
+// of size 16
+UUID :: uuid.Identifier
+
+Color_RGB :: [3]BYTE
+Color_RGBA :: [4]BYTE
+
+Document :: struct {
+ header: File_Header,
+ frames: []Frame,
+ arena: virtual.Arena `fmt:"-"`,
+}
+
+
+Frame :: struct {
+ header: Frame_Header,
+ chunks: []Chunk,
+}
+
+Chunk :: union {
+ Old_Palette_256_Chunk, Old_Palette_64_Chunk, Layer_Chunk, Cel_Chunk,
+ Cel_Extra_Chunk, Color_Profile_Chunk, External_Files_Chunk, Mask_Chunk,
+ Path_Chunk, Tags_Chunk, Palette_Chunk, User_Data_Chunk, Slice_Chunk,
+ Tileset_Chunk,
+}
+
+
+FILE_MAGIC_NUM : WORD : 0xA5E0
+FILE_HEADER_SIZE :: 128
+
+
+Color_Depth :: enum(WORD){
+ Indexed = 8,
+ Grayscale = 16,
+ RGBA = 32,
+}
+
+File_Flag :: enum(DWORD) {
+ Layer_Opacity,
+ Layer_Blend_Opacity_for_Groups,
+ Has_UUID,
+}
+File_Flags :: bit_set[File_Flag; DWORD]
+
+File_Header :: struct {
+ size: DWORD,
+ frames: WORD,
+ width: WORD,
+ height: WORD,
+ color_depth: Color_Depth,
+ flags: File_Flags, // 1=Layer opacity has valid value
+ speed: WORD, // Not longer in use
+ transparent_index: BYTE, // for Indexed sprites only
+ num_of_colors: WORD, // 0 == 256 for old sprites
+ ratio_width: BYTE, // "pixel width/pixel height" if 0 ratio == 1:1
+ ratio_height: BYTE, // "pixel width/pixel height" if 0 ratio == 1:1
+ x: SHORT,
+ y: SHORT,
+ grid_width: WORD, // 0 if no grid
+ grid_height: WORD, // 0 if no grid
+}
+
+
+FRAME_HEADER_SIZE :: 16
+FRAME_MAGIC_NUM : WORD : 0xF1FA
+
+Frame_Header :: struct {
+ old_num_of_chunks: WORD, // if \xFFFF use new
+ duration: WORD, // in milliseconds
+ num_of_chunks: DWORD, // if 0 use old
+}
+
+
+Chunk_Types :: enum(WORD) {
+ none,
+ old_palette_256 = 0x0004,
+ old_palette_64 = 0x0011,
+ layer = 0x2004,
+ cel = 0x2005,
+ cel_extra = 0x2006,
+ color_profile = 0x2007,
+ external_files = 0x2008,
+ mask = 0x2016, // no longer in use
+ path = 0x2017, // not in use
+ tags = 0x2018,
+ palette = 0x2019,
+ user_data = 0x2020,
+ slice = 0x2022,
+ tileset = 0x2023,
+}
+
+Chunk_Types_Set :: enum {
+ old_palette_256,
+ old_palette_64,
+ layer,
+ cel,
+ cel_extra,
+ color_profile,
+ external_files,
+ mask, // no longer in use
+ path, // not in use
+ tags,
+ palette,
+ user_data,
+ slice,
+ tileset,
+}
+
+Chunk_Set :: bit_set[Chunk_Types_Set]
+
+
+Old_Palette_Packet :: struct {
+ entries_to_skip: BYTE, // start from 0
+ num_colors: BYTE, // 0 == 256
+ colors: []Color_RGB,
+}
+
+Old_Palette_256_Chunk :: distinct []Old_Palette_Packet
+Old_Palette_64_Chunk :: distinct []Old_Palette_Packet
+
+
+Layer_Chunk_Flag :: enum(WORD) {
+ Visiable,
+ Editable,
+ Lock_Movement,
+ Background,
+ Prefer_Linked_Cels,
+ Group_Collapsed,
+ Ref_Layer,
+}
+Layer_Chunk_Flags :: bit_set [Layer_Chunk_Flag; WORD]
+
+Layer_Types :: enum(WORD) {
+ Normal, // image
+ Group,
+ Tilemap,
+}
+
+Layer_Blend_Mode :: enum(WORD) {
+ Normal,
+ Multiply,
+ Screen,
+ Overlay,
+ Darken,
+ Lighten,
+ Color_Dodge,
+ Color_Burn,
+ Hard_Light,
+ Soft_Light,
+ Difference,
+ Exclusion,
+ Hue,
+ Saturation,
+ Color,
+ Luminosity,
+ Addition,
+ Subtract,
+ Divide,
+}
+
+Layer_Chunk :: struct {
+ flags: Layer_Chunk_Flags,
+ type: Layer_Types,
+ child_level: WORD,
+ default_width: WORD, // Ignored
+ default_height: WORD, // Ignored
+ blend_mode: Layer_Blend_Mode,
+ opacity: BYTE, // valid when header flag is 1
+ name: string,
+ tileset_index: DWORD, // set if type == Tilemap
+ uuid: Maybe(UUID),
+}
+
+
+Raw_Cel :: struct{
+ width: WORD,
+ height: WORD,
+ pixels: []PIXEL,
+}
+
+Linked_Cel :: distinct WORD
+
+// raw cel ZLIB compressed
+Com_Image_Cel :: struct{
+ width: WORD,
+ height: WORD,
+ pixels: []PIXEL,
+}
+
+Tile_ID :: enum(DWORD) {
+ byte = 0xfffffff1,
+ word = 0xffff1fff,
+ dword = 0x1fffffff,
+}
+
+Com_Tilemap_Cel :: struct{
+ width, height: WORD,
+ bits_per_tile: WORD, // always 32
+ bitmask_id: Tile_ID,
+ bitmask_x: DWORD `fmt:"b"`,
+ bitmask_y: DWORD `fmt:"b"`,
+ bitmask_diagonal: DWORD `fmt:"b"`,
+ tiles: []TILE, // ZLIB compressed
+}
+
+Cel_Types :: enum(WORD){
+ Raw,
+ Linked_Cel,
+ Compressed_Image,
+ Compressed_Tilemap,
+}
+
+Cel_Type :: union{ Raw_Cel, Linked_Cel, Com_Image_Cel, Com_Tilemap_Cel }
+
+Cel_Chunk :: struct {
+ layer_index: WORD,
+ x: SHORT,
+ y: SHORT,
+ opacity_level: BYTE,
+ type: Cel_Types,
+ z_index: SHORT, // 0=default, pos=show n layers later, neg=back
+ cel: Cel_Type,
+}
+
+
+Cel_Extra_Flag :: enum(WORD){ Precise }
+Cel_Extra_Flags :: bit_set[Cel_Extra_Flag; WORD]
+
+Cel_Extra_Chunk :: struct {
+ flags: Cel_Extra_Flags,
+ x: FIXED,
+ y: FIXED,
+ width: FIXED,
+ height: FIXED,
+}
+
+
+ICC_Profile :: distinct []byte
+
+Color_Profile_Flag :: enum(WORD){ Special_Fixed_Gamma }
+Color_Profile_Flags :: bit_set[Color_Profile_Flag; WORD]
+
+Color_Profile_Type :: enum(WORD) {
+ None,
+ sRGB,
+ ICC,
+}
+
+Color_Profile_Chunk :: struct {
+ type: Color_Profile_Type,
+ flags: Color_Profile_Flags,
+ fixed_gamma: FIXED,
+ // TODO: Yay more libs to make, https://www.color.org/icc1v42.pdf
+ icc: Maybe(ICC_Profile),
+}
+
+
+ExF_Entry_Type :: enum(BYTE) {
+ Palette,
+ Tileset,
+ Properties_Name,
+ Tile_Manegment_Name,
+}
+
+External_Files_Entry :: struct {
+ id: DWORD,
+ type: ExF_Entry_Type,
+ file_name_or_id: STRING,
+}
+
+External_Files_Chunk :: []External_Files_Entry
+
+
+Mask_Chunk :: struct {
+ x: SHORT,
+ y: SHORT,
+ width: WORD,
+ height: WORD,
+ name: string,
+ bit_map_data: []BYTE, //size = height*((width+7)/8)
+}
+
+
+Path_Chunk :: struct { } // never used
+
+
+Tag_Loop_Dir :: enum(BYTE) {
+ Forward,
+ Reverse,
+ Ping_Pong,
+ Ping_Pong_Reverse,
+}
+
+Tag :: struct{
+ from_frame: WORD,
+ to_frame: WORD,
+ loop_direction: Tag_Loop_Dir,
+ repeat: WORD,
+ tag_color: Color_RGB,
+ name: string,
+}
+
+Tags_Chunk :: []Tag
+
+
+Pal_Flag :: enum(WORD){ Has_Name }
+Pal_Flags :: bit_set[Pal_Flag; WORD]
+
+Palette_Entry :: struct {
+ color: Color_RGBA,
+ name: Maybe(string),
+}
+
+Palette_Chunk :: struct {
+ size: DWORD,
+ first_index: DWORD,
+ last_index: DWORD,
+ entries: []Palette_Entry,
+}
+
+
+// Vec_Diff :: struct{type: WORD, data: UD_Property_Value}
+// UD_Vec :: union {[]UD_Property_Value, []Vec_Diff}
+UD_Vec :: []Property_Value
+
+Property_Type :: enum(WORD) {
+ Null, Bool, I8, U8, I16, U16, I32, U32, I64, U64,
+ Fixed, F32, F64, String, Point, Size, Rect,
+ Vector, Properties, UUID,
+}
+
+Property_Value :: union {
+ bool, i8, BYTE, SHORT, WORD, LONG, DWORD, LONG64, QWORD, FIXED, FLOAT,
+ DOUBLE, STRING, POINT, SIZE, RECT, UUID,
+ UD_Vec, Properties,
+}
+
+Properties :: map[string]Property_Value
+Properties_Map :: map[DWORD]Property_Value
+
+UD_Flag :: enum(DWORD) {
+ Text,
+ Color,
+ Properties,
+}
+UD_Flags :: bit_set[UD_Flag; DWORD]
+
+User_Data_Chunk :: struct {
+ text: Maybe(string),
+ color: Maybe(Color_RGBA),
+ maps: Maybe(Properties_Map),
+}
+
+
+Slice_Center :: struct {
+ x: LONG,
+ y: LONG,
+ width: DWORD,
+ height: DWORD,
+}
+
+Slice_Pivot :: distinct POINT
+
+Slice_Key :: struct {
+ frame_num: DWORD,
+ x: LONG,
+ y: LONG,
+ width: DWORD,
+ height: DWORD,
+ center: Maybe(Slice_Center),
+ pivot: Maybe(Slice_Pivot),
+}
+
+Slice_Flag :: enum(DWORD) {
+ Patched_slice,
+ Pivot_Information,
+}
+Slice_Flags :: bit_set[Slice_Flag; DWORD]
+
+Slice_Chunk :: struct {
+ flags: Slice_Flags,
+ name: string,
+ keys: []Slice_Key,
+}
+
+
+Tileset_Flag :: enum(DWORD) {
+ Include_Link_To_External_File,
+ Include_Tiles_Inside_This_File,
+ Tile_ID_Is_0,
+ Auto_Mode_X_Flip_Match,
+ Auto_Mode_Y_Flip_Match,
+ Auto_Mode_Diagonal_Flip_Match,
+}
+Tileset_Flags :: bit_set[Tileset_Flag; DWORD]
+
+Tileset_External :: struct {
+ file_id: DWORD,
+ tileset_id: DWORD,
+}
+
+Tileset_Compressed :: distinct []PIXEL
+
+Tileset_Chunk :: struct {
+ id: DWORD,
+ flags: Tileset_Flags,
+ num_of_tiles: DWORD,
+ width: WORD,
+ height: WORD,
+ base_index: SHORT,
+ name: string,
+ external: Maybe(Tileset_External),
+ compressed: Maybe(Tileset_Compressed),
+}
diff --git a/tools/compile_assets/aseprite/unmarshal.odin b/tools/compile_assets/aseprite/unmarshal.odin
new file mode 100644
index 0000000..5e5295c
--- /dev/null
+++ b/tools/compile_assets/aseprite/unmarshal.odin
@@ -0,0 +1,444 @@
+package aseprite_file_handler
+
+import "base:runtime"
+import "base:intrinsics"
+import "core:io"
+import "core:os"
+import "core:log"
+import "core:bytes"
+import "core:bufio"
+import "core:mem/virtual"
+
+
+unmarshal_from_bytes_buff :: proc(doc: ^Document, r: ^bytes.Reader, alloc: runtime.Allocator = {}) -> (err: Unmarshal_Error) {
+ rr, ok := io.to_reader(bytes.reader_to_stream(r))
+ if !ok {
+ return .Unable_Make_Reader
+ }
+ return unmarshal(doc, rr, alloc)
+}
+
+unmarshal_from_bufio :: proc(doc: ^Document, r: ^bufio.Reader, alloc: runtime.Allocator = {}) -> (err: Unmarshal_Error) {
+ rr, ok := io.to_reader(bufio.reader_to_stream(r))
+ if !ok {
+ return .Unable_Make_Reader
+ }
+ return unmarshal(doc, rr, alloc)
+}
+
+unmarshal_from_filename :: proc(doc: ^Document, name: string, alloc: runtime.Allocator = {}) -> (err: Unmarshal_Error) {
+ fd, fd_err := os.open(name, os.O_RDONLY, 0)
+ if fd_err != nil {
+ log.error("Unable to read because of:", fd_err)
+ return fd_err
+ }
+ defer os.close(fd)
+ return unmarshal(doc, fd, alloc)
+}
+
+unmarshal_from_handle :: proc(doc: ^Document, h: os.Handle, alloc: runtime.Allocator = {}) -> (err: Unmarshal_Error) {
+ rr, ok := io.to_reader(os.stream_from_handle(h))
+ if !ok {
+ return .Unable_Make_Reader
+ }
+ return unmarshal(doc, rr, alloc)
+}
+
+unmarshal_from_slice :: proc(doc: ^Document, b: []byte, alloc: runtime.Allocator = {}) -> (err: Unmarshal_Error) {
+ r: bytes.Reader
+ bytes.reader_init(&r, b[:])
+ return unmarshal(doc, &r, alloc)
+}
+
+unmarshal :: proc{
+ unmarshal_from_bytes_buff, unmarshal_from_slice, unmarshal_from_handle,
+ unmarshal_from_filename, unmarshal_from_bufio, unmarshal_from_reader,
+}
+
+unmarshal_from_reader :: proc(doc: ^Document, r: io.Reader, alloc: runtime.Allocator = {}) -> (err: Unmarshal_Error) {
+ tr: int
+ defer {
+ if err != nil {
+ log.errorf("Failed to unmarshal at %v (%X) cause of %v", tr, tr, err)
+ }
+ }
+
+ temp_alloc := alloc
+ if alloc == {} {
+ if doc.arena.curr_block == nil || doc.arena.total_reserved == 0 {
+ virtual.arena_init_growing(&doc.arena) or_return
+ }
+ temp_alloc = virtual.arena_allocator(&doc.arena)
+ }
+ context.allocator = temp_alloc
+
+ icc_warn: bool
+ rt := &tr
+
+ doc.header = read_file_header(r, rt) or_return
+ frames := doc.header.frames
+ color_depth := doc.header.color_depth
+ doc.frames = make([]Frame, int(frames)) or_return
+ flags := doc.header.flags
+
+ for &frame in doc.frames {
+ fh: Frame_Header
+ //frame_size := read_dword(r, rt) or_return
+ read_dword(r, rt) or_return
+ frame_magic := read_word(r, rt) or_return
+ if frame_magic != FRAME_MAGIC_NUM {
+ return .Bad_Frame_Magic_Number
+ }
+
+ fh.old_num_of_chunks = read_word(r, rt) or_return
+ fh.duration = read_word(r, rt) or_return
+ if fh.duration == 0 {
+ fh.duration = doc.header.speed
+ }
+
+ read_skip(r, 2, rt) or_return
+ fh.num_of_chunks = read_dword(r, rt) or_return
+
+ chunks := int(fh.num_of_chunks)
+ if chunks == 0 {
+ chunks = int(fh.old_num_of_chunks)
+ }
+
+ frame.header = fh
+ frame.chunks = make([]Chunk, chunks) or_return
+
+ for &chunk in frame.chunks {
+ c_size := int(read_dword(r, rt) or_return)
+ c_type := cast(Chunk_Types)read_word(r, rt) or_return
+
+ switch c_type {
+ case .old_palette_256: chunk = read_old_palette_256(r, rt) or_return
+ case .old_palette_64: chunk = read_old_palette_64(r, rt) or_return
+ case .layer: chunk = read_layer(r, rt, (.Has_UUID in flags)) or_return
+ case .cel: chunk = read_cel(r, rt, int(color_depth), c_size) or_return
+ case .cel_extra: chunk = read_cel_extra(r, rt) or_return
+ case .color_profile: chunk = read_color_profile(r, rt, &icc_warn) or_return
+ case .external_files: chunk = read_external_files(r, rt) or_return
+ case .mask: chunk = read_mask(r, rt) or_return
+ case .path: chunk = read_path()
+ case .tags: chunk = read_tags(r, rt) or_return
+ case .palette: chunk = read_palette(r, rt) or_return
+ case .user_data: chunk = read_user_data(r, rt) or_return
+ case .slice: chunk = read_slice(r, rt) or_return
+ case .tileset: chunk = read_tileset(r, rt) or_return
+
+ case .none: fallthrough
+ case:
+ log.error("Invalid Chunk Type", c_type)
+ return .Invalid_Chunk_Type
+ }
+ }
+ }
+ return
+}
+
+
+unmarshal_chunks :: proc{ unmarshal_multi_chunks, unmarshal_single_chunk }
+
+
+unmarshal_multi_chunks :: proc(r: io.Reader, buf: ^[dynamic]Chunk, chunks: Chunk_Set, alloc := context.allocator) -> (err: Unmarshal_Error) {
+ context.allocator = alloc
+ icc_warn: bool
+ tr: int
+ defer {
+ if err != nil {
+ log.error("Failed to unmarshal at", tr, "cause of", err)
+ }
+ }
+ rt := &tr
+
+ file_header := read_file_header(r, rt) or_return
+ frames := file_header.frames
+ color_depth := int(file_header.color_depth)
+ flags := file_header.flags
+
+ for _ in 0..<frames {
+ read_dword(r, rt) or_return
+ frame_magic := read_word(r, rt) or_return
+ if frame_magic != FRAME_MAGIC_NUM {
+ return .Bad_Frame_Magic_Number
+ }
+ old_num_of_chunks := read_word(r, rt) or_return
+ read_skip(r, 4, rt) or_return
+ num_of_chunks := int(read_dword(r, rt) or_return)
+
+ if num_of_chunks == 0 {
+ num_of_chunks = int(old_num_of_chunks)
+ }
+
+ for _ in 0..<num_of_chunks {
+ c_size := int(read_dword(r, rt) or_return) - 6
+ c_type := cast(Chunk_Types)read_word(r, rt) or_return
+
+ chunk: Chunk
+ switch c_type {
+ case .old_palette_256:
+ if .old_palette_256 in chunks {
+ chunk = read_old_palette_256(r, rt) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .old_palette_64:
+ if .old_palette_64 in chunks {
+ chunk = read_old_palette_64(r, rt) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .layer:
+ if .layer in chunks {
+ chunk = read_layer(r, rt, (.Has_UUID in flags)) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .cel:
+ if .cel in chunks {
+ chunk = read_cel(r, rt, color_depth, c_size+6) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .cel_extra:
+ if .cel_extra in chunks {
+ chunk = read_cel_extra(r, rt) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .color_profile:
+ if .color_profile in chunks {
+ chunk = read_color_profile(r, rt, &icc_warn) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .external_files:
+ if .external_files in chunks {
+ chunk = read_external_files(r, rt) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .mask:
+ if .mask in chunks {
+ chunk = read_mask(r, rt) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .path:
+ if .path in chunks {
+ chunk = read_path()
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .tags:
+ if .tags in chunks {
+ chunk = read_tags(r, rt) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .palette:
+ if .palette in chunks {
+ chunk = read_palette(r, rt) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .user_data:
+ if .user_data in chunks {
+ chunk = read_user_data(r, rt) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .slice:
+ if .slice in chunks {
+ chunk = read_slice(r, rt) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .tileset:
+ if .tileset in chunks {
+ chunk = read_tileset(r, rt) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .none: fallthrough
+ case:
+ log.error("Invalid Chunk Type", c_type)
+ return .Invalid_Chunk_Type
+ }
+
+ if chunk != nil {
+ append(buf, chunk) or_return
+ }
+ }
+ }
+ return
+}
+
+
+unmarshal_single_chunk :: proc (
+ r: io.Reader, buf: ^[dynamic]$T, alloc := context.allocator
+) -> (err: Unmarshal_Error) where intrinsics.type_is_variant_of(Chunk, T) {
+
+ context.allocator = alloc
+ icc_warn: bool
+
+ tr: int
+ defer {
+ if err != nil {
+ log.error("Failed to unmarshal at", tr, "cause of", err)
+ }
+ }
+ rt := &tr
+
+ file_header := read_file_header(r, rt) or_return
+
+ frames := file_header.frames
+ color_depth := file_header.color_depth
+ _ = color_depth
+ flags := file_header.flags
+
+ for _ in 0..<frames {
+ read_dword(r, rt) or_return
+ frame_magic := read_word(r, rt) or_return
+ if frame_magic != FRAME_MAGIC_NUM {
+ return .Bad_Frame_Magic_Number
+ }
+
+ old_num_of_chunks := read_word(r, rt) or_return
+ read_skip(r, 4, rt) or_return
+ num_of_chunks := int(read_dword(r, rt) or_return)
+
+ if num_of_chunks == 0 {
+ num_of_chunks = int(old_num_of_chunks)
+ }
+
+ for _ in 0..<num_of_chunks {
+ c_size := int(read_dword(r, rt) or_return) - 6
+ c_type := cast(Chunk_Types)read_word(r, rt) or_return
+
+ // TODO: This is too dirty for my likeing. Make a proc group for it all.
+ switch c_type {
+ case .old_palette_256:
+ when T == Old_Palette_256_Chunk {
+ append(buf, read_old_palette_256(r, rt) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .old_palette_64:
+ when T == Old_Palette_64_Chunk {
+ append(buf, read_old_palette_64(r, rt) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .layer:
+ when T == Layer_Chunk {
+ append(buf, read_layer(r, rt, (.Has_UUID in flags)) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .cel:
+ when T == Cel_Chunk {
+ append(buf, read_cel(r, rt, int(color_depth), c_size+6) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .cel_extra:
+ when T == Cel_Extra_Chunk {
+ append(buf, read_cel_extra(r, rt) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .color_profile:
+ when T == Color_Profile_Chunk {
+ append(buf, read_color_profile(r, rt, &icc_warn) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .external_files:
+ when T == External_Files_Chunk {
+ append(buf, read_external_files(r, rt) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .mask:
+ when T == Mask_Chunk {
+ append(buf, read_mask(r, rt) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .path:
+ when T == Path_Chunk {
+ append(buf, read_path()) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .tags:
+ when T == Tags_Chunk {
+ append(buf, read_tags(r, rt) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .palette:
+ when T == Palette_Chunk {
+ append(buf, read_palette(r, rt) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .user_data:
+ when T == User_Data_Chunk {
+ append(buf, read_user_data(r, rt) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .slice:
+ when T == Slice_Chunk {
+ append(buf, read_slice(r, rt) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .tileset:
+ when T == Tileset_Chunk {
+ append(buf, read_tileset(r, rt) or_return) or_return
+ } else {
+ read_skip(r, c_size, rt) or_return
+ }
+
+ case .none: fallthrough
+ case:
+ log.error("Invalid Chunk Type", c_type)
+ return .Invalid_Chunk_Type
+ }
+ }
+ }
+
+ return
+}
+
diff --git a/tools/compile_assets/aseprite/util.odin b/tools/compile_assets/aseprite/util.odin
new file mode 100644
index 0000000..1333347
--- /dev/null
+++ b/tools/compile_assets/aseprite/util.odin
@@ -0,0 +1,149 @@
+package aseprite_file_handler
+
+import ir "base:intrinsics"
+import "core:log"
+import "core:reflect"
+import "core:encoding/endian"
+
+@(require) import "core:strconv"
+_ :: reflect
+
+get_chunk_type :: proc(c: Chunk) -> (type: WORD, err: Marshal_Error) {
+ switch _ in c {
+ case Old_Palette_256_Chunk: type = WORD(Chunk_Types.old_palette_256)
+ case Old_Palette_64_Chunk: type = WORD(Chunk_Types.old_palette_64)
+ case Layer_Chunk: type = WORD(Chunk_Types.layer)
+ case Cel_Chunk: type = WORD(Chunk_Types.cel)
+ case Cel_Extra_Chunk: type = WORD(Chunk_Types.cel_extra)
+ case Color_Profile_Chunk: type = WORD(Chunk_Types.color_profile)
+ case External_Files_Chunk: type = WORD(Chunk_Types.external_files)
+ case Mask_Chunk: type = WORD(Chunk_Types.mask)
+ case Path_Chunk: type = WORD(Chunk_Types.path)
+ case Tags_Chunk: type = WORD(Chunk_Types.tags)
+ case Palette_Chunk: type = WORD(Chunk_Types.palette)
+ case User_Data_Chunk: type = WORD(Chunk_Types.user_data)
+ case Slice_Chunk: type = WORD(Chunk_Types.slice)
+ case Tileset_Chunk: type = WORD(Chunk_Types.tileset)
+ case:
+ err = .Invalid_Chunk_Type
+ }
+
+ return
+}
+
+get_cel_type :: proc(c: Cel_Type) -> (type: WORD, err: Marshal_Error) {
+ switch _ in c {
+ case Raw_Cel: type = WORD(Cel_Types.Raw)
+ case Linked_Cel: type = WORD(Cel_Types.Linked_Cel)
+ case Com_Image_Cel: type = WORD(Cel_Types.Compressed_Image)
+ case Com_Tilemap_Cel: type = WORD(Cel_Types.Compressed_Tilemap)
+ case:
+ err = .Invalid_Cel_Type
+ }
+
+ return
+}
+
+get_property_type :: proc(v: Property_Value) -> (type: WORD, err: Marshal_Error) {
+ switch t in v {
+ case nil: type = WORD(Property_Type.Null)
+ case bool: type = WORD(Property_Type.Bool)
+ case i8: type = WORD(Property_Type.I8)
+ case BYTE: type = WORD(Property_Type.U8)
+ case SHORT: type = WORD(Property_Type.I16)
+ case WORD: type = WORD(Property_Type.U16)
+ case LONG: type = WORD(Property_Type.I32)
+ case DWORD: type = WORD(Property_Type.U32)
+ case LONG64: type = WORD(Property_Type.I64)
+ case QWORD: type = WORD(Property_Type.U64)
+ case FIXED: type = WORD(Property_Type.Fixed)
+ case FLOAT: type = WORD(Property_Type.F32)
+ case DOUBLE: type = WORD(Property_Type.F64)
+ case STRING: type = WORD(Property_Type.String)
+ case POINT: type = WORD(Property_Type.Point)
+ case SIZE: type = WORD(Property_Type.Size)
+ case RECT: type = WORD(Property_Type.Rect)
+ case UUID: type = WORD(Property_Type.UUID)
+ case UD_Vec: type = WORD(Property_Type.Vector)
+ case Properties: type = WORD(Property_Type.Properties)
+ case: err = Marshal_Errors.Invalid_Property_Type
+ }
+ return
+}
+
+tiles_to_u8 :: proc(tiles: []TILE, b: []u8) -> (pos: int, err: Write_Error) {
+ next: int
+ for t in tiles {
+ switch v in t {
+ case BYTE:
+ pos = next
+ next += size_of(BYTE)
+ b[pos] = v
+
+ case WORD:
+ pos = next
+ next += size_of(WORD)
+ if !endian.put_u16(b[pos:next], .Little, v) {
+
+ return 0, .Unable_To_Encode_Data
+ }
+ case DWORD:
+ pos = next
+ next += size_of(DWORD)
+ if !endian.put_u32(b[pos:next], .Little, v) {
+ return 0, .Unable_To_Encode_Data
+ }
+ }
+ }
+
+ pos = next
+ return
+}
+
+
+@(private)
+fast_log_str :: proc(lvl: log.Level, str: string, loc := #caller_location) {
+ logger := context.logger
+ if logger.procedure == nil { return }
+ if lvl < logger.lowest_level { return }
+ logger.procedure(logger.data, lvl, str, logger.options, loc)
+}
+
+@(private)
+fast_log_str_enum :: proc(lvl: log.Level, str: string, val: $T, sep := " ", loc := #caller_location) where ir.type_is_enum(T) {
+ logger := context.logger
+ if logger.procedure == nil { return }
+ if lvl < logger.lowest_level { return }
+
+ s := reflect.enum_string(val)
+ buf := make([]u8, len(str) + len(sep) + len(s))
+ defer delete(buf)
+
+ n := copy(buf[:], str)
+ n += copy(buf[n:], sep)
+ copy(buf[n:], s)
+
+ logger.procedure(logger.data, lvl, string(buf), logger.options, loc)
+}
+
+@(private)
+fast_log_str_num :: proc(lvl: log.Level, str: string, val: $T, sep := " ", loc := #caller_location) where ir.type_is_numeric(T) {
+ logger := context.logger
+ if logger.procedure == nil { return }
+ if lvl < logger.lowest_level { return }
+
+ nb: [32]u8
+ s := strconv.append_int(nb[:], i64(val), 10)
+ buf := make([]u8, len(str) + len(sep) + len(s))
+ defer delete(buf)
+
+ n := copy(buf[:], str)
+ n += copy(buf[n:], sep)
+ copy(buf[n:], s)
+
+ logger.procedure(logger.data, lvl, string(buf), logger.options, loc)
+}
+
+@(private)
+fast_log :: proc { fast_log_str, fast_log_str_enum, fast_log_str_num }
+
diff --git a/tools/compile_assets/aseprite/utils/animation.odin b/tools/compile_assets/aseprite/utils/animation.odin
new file mode 100644
index 0000000..7fe5059
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/animation.odin
@@ -0,0 +1,181 @@
+package aseprite_file_handler_utility
+
+
+import "core:time"
+import "core:slice"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+import ase ".."
+
+get_animation_from_doc :: proc(
+ doc: ^ase.Document, anim: ^Animation,
+ use_tag := "", alloc := context.allocator
+) -> (err: Errors) {
+ info: Info
+ get_info(doc, &info, alloc) or_return
+ defer destroy(&info)
+ return get_animation_from_info(info, anim, use_tag)
+}
+
+
+get_animation_from_info :: proc (
+ info: Info, anim: ^Animation, use_tag := "",
+) -> (err: Errors) {
+ context.allocator = info.allocator
+
+ s, f := 0, len(info.frames)
+ tag: Tag
+
+ if use_tag != "" {
+ for t in info.tags {
+ if t.name == use_tag {
+ if len(info.frames) < t.to || len(info.frames) < t.from {
+ return Animation_Error.Tag_Index_Out_Of_Bounds
+ }
+ tag = t
+ s = t.from
+ f = t.to+1
+ break
+ }
+ }
+
+ if tag == {} {
+ return Animation_Error.Tag_Not_Found
+ }
+ }
+
+ if anim.fps == 0 {
+ anim.fps = 30
+ }
+
+ anim.md = info.md
+ anim_frames := make([dynamic][]byte) or_return
+ defer if err != nil { delete(anim_frames) }
+
+ pos: int
+
+ for frame in info.frames[s:f] {
+ img := get_image_bytes_from_frame(frame, info) or_return
+ defer delete(img)
+
+ to_add := f64(frame.duration) * f64(anim.fps) / 1000
+ for _ in 0..<to_add {
+ append_elem(&anim_frames, slice.clone(img) or_return) or_return
+ pos += 1
+ }
+ }
+
+ if tag.direction == .Reverse || tag.direction == .Ping_Pong_Reverse {
+ slice.reverse(anim_frames[:])
+ }
+
+ if tag.direction == .Ping_Pong || tag.direction == .Ping_Pong_Reverse {
+ rev := slice.clone(anim_frames[:])
+ defer delete(rev)
+
+ slice.reverse(rev)
+ append_elems(&anim_frames, ..rev) or_return
+ }
+
+ anim.frames = anim_frames[:]
+ anim.length = time.Second * time.Duration(len(anim_frames) / anim.fps)
+ return
+}
+
+// Assumes 30 FPS & 100ms Frame Duration
+get_animation_from_images :: proc(imgs: []Image, md: Metadata, anim: ^Animation, alloc := context.allocator) -> (err: Errors) {
+ context.allocator = alloc
+ anim.fps = 30
+ anim.md = md
+ anim.length = time.Second * time.Duration(len(imgs) / 10)
+
+ frames := make([dynamic][]byte, 0, 3 * len(imgs)) or_return
+ defer if err != nil { delete(frames) }
+
+ for img in imgs {
+ frame := slice.clone(img.data) or_return
+ append_elem(&frames, frame) or_return
+
+ frame = slice.clone(img.data) or_return
+ append_elem(&frames, frame) or_return
+
+ frame = slice.clone(img.data) or_return
+ append_elem(&frames, frame) or_return
+ }
+
+ anim.frames = frames[:]
+ return
+}
+
+// Assumes 30 FPS & 100ms Frame Duration
+get_animation_from_bytes :: proc(imgs: [][]byte, md: Metadata, anim: ^Animation, alloc := context.allocator) -> (err: Errors) {
+ context.allocator = alloc
+ anim.fps = 30
+ anim.md = md
+ anim.length = time.Second * time.Duration(len(imgs) / 10)
+
+ frames := make([dynamic][]byte, 0, 3 * len(imgs)) or_return
+ defer if err != nil { delete(frames) }
+
+ for img in imgs {
+ append_elem(&frames, slice.clone(img) or_return) or_return
+ append_elem(&frames, slice.clone(img) or_return) or_return
+ append_elem(&frames, slice.clone(img) or_return) or_return
+ }
+
+ anim.frames = frames[:]
+ return
+}
+
+
+get_animation :: proc {
+ get_animation_from_doc,
+ get_animation_from_info,
+ get_animation_from_images,
+ get_animation_from_bytes,
+}
+
+
+// Returns a new image set with Red/Blue Tint or Merge Onion Skin
+apply_onion_skin :: proc(
+ imgs: []Image, opacity := 69, merge := false,
+ alloc := context.allocator
+) -> (frames: []Image, err: Errors) {
+ context.allocator = alloc
+
+ frames = make([]Image, len(imgs)) or_return
+ defer if err != nil { delete(frames) }
+
+ blank := make([]byte, len(imgs[0].data)) or_return
+ defer delete(blank)
+
+ cur, next: []byte
+ last := blank
+ defer if cur != nil { delete(cur) }
+
+ for img, pos in imgs {
+ if pos < len(imgs)-1 {
+ next = imgs[pos+1].data
+ } else {
+ next = blank
+ }
+
+ cur = slice.clone(img.data) or_return
+ if merge {
+ blend_bytes(last, cur, opacity, .Merge) or_return
+ blend_bytes(next, cur, opacity, .Merge) or_return
+ } else {
+ blend_bytes(last, cur, opacity, .Red_Tint) or_return
+ blend_bytes(next, cur, opacity, .Blue_Tint) or_return
+ }
+
+
+ frames[pos] = img
+ frames[pos].data = cur
+ last = cur
+ }
+
+ return
+}
diff --git a/tools/compile_assets/aseprite/utils/blend.odin b/tools/compile_assets/aseprite/utils/blend.odin
new file mode 100644
index 0000000..9bf0f7e
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/blend.odin
@@ -0,0 +1,727 @@
+package aseprite_file_handler_utility
+
+import "core:math"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+
+ASE_USE_BUGGED_SAT :: #config(ASE_USE_BUGGED_SAT, false)
+
+
+// https://printtechnologies.org/standards/files/pdf-reference-1.6-addendum-blend-modes.pdf
+
+@(private)
+slow_alpha :: proc(a: int, b: ..int) -> (res: int) {
+ // α = α1 * α2 *..αn / 255^(n-1)
+ if len(b) == 0 { return a }
+ if len(b) == 1 { return a * b[0] / 255}
+ res = a
+ d := 255
+ for i in b {
+ res *= i
+ d *= 255
+ }
+ return res / d
+}
+
+
+// Modifies current Image (`cur`)
+blend_images :: proc(last, cur: Image, opacity: int, mode: Blend_Mode) -> (err: Blend_Error) {
+ if len(last.data) != len(cur.data) {
+ return Blend_Error.Unequal_Image_Sizes
+ }
+ return blend_bytes(last.data, cur.data, opacity, mode)
+}
+
+// Modifies current Image (`cur`)
+blend_bytes :: proc(last, cur: []byte, opacity: int, mode: Blend_Mode) -> (err: Blend_Error) {
+ if len(last) != len(cur) {
+ return Blend_Error.Unequal_Image_Sizes
+ }
+
+ for pix in 0..<len(cur)/4 {
+ pos := pix * 4
+ l_pix, c_pix: [4]u8
+
+ copy(l_pix[:], last[pos:pos+4])
+ copy(c_pix[:], cur[pos:pos+4])
+
+ r_pix := blend(l_pix, c_pix, i32(opacity), mode) or_return
+
+ copy(cur[pos:pos+4], r_pix[:])
+ }
+ return
+}
+
+
+alpha :: mul
+mul :: proc{mul_u8, mul_i32, mul_int, mul_vec4, mul_vec3}
+
+mul_vec4 :: proc(a, b: [4]i32) -> [4]i32 {
+ t := a * b + 128
+ return {
+ ((t.r >> 8) + t.r) >> 8,
+ ((t.g >> 8) + t.g) >> 8,
+ ((t.b >> 8) + t.b) >> 8,
+ ((t.a >> 8) + t.a) >> 8,
+ }
+}
+
+mul_vec3 :: proc(a, b: [3]i32) -> [3]i32 {
+ t := a * b + 128
+ return {
+ ((t.r >> 8) + t.r) >> 8,
+ ((t.g >> 8) + t.g) >> 8,
+ ((t.b >> 8) + t.b) >> 8,
+ }
+}
+
+
+mul_i32 :: proc(a, b: i32) -> i32 {
+ // License `.\3rd party licenses\libpixman license`
+ // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L67
+
+ t := a * b + 128
+ return ((t >> 8 ) + t ) >> 8
+}
+
+mul_int :: proc(a, b: int) -> i32 {
+ // License `.\3rd party licenses\libpixman license`
+ // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L67
+
+ t := a * b + 128
+ return i32(((t >> 8 ) + t ) >> 8)
+}
+
+mul_u8 :: proc(a, b: byte) -> i32 {
+ // License `.\3rd party licenses\libpixman license`
+ // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L67
+
+ t := a * b + 128
+ return i32(((t >> 8 ) + t ) >> 8)
+}
+
+
+div :: proc( #any_int a, b: u16) -> u16 {
+ // License `.\3rd party licenses\libpixman license`
+ // https://github.com/libpixman/pixman/blob/master/pixman/pixman-combine32.h#L70
+ return (a * 255 + (b / 2)) / b
+}
+
+blend :: proc(last, cur: Pixel, opacity: i32, mode: Blend_Mode) -> (res: Pixel, err: Blend_Error) {
+ // https://github.com/aseprite/aseprite/blob/main/src/doc/blend_funcs.cpp
+ if last.a == 0 {
+ res.rgb = cur.rgb
+ res.a = byte(mul(i32(cur.a), opacity))
+ return
+ }
+
+ back: [4]i32 = {i32(last.r), i32(last.g), i32(last.b), i32(last.a)}
+ pix: [4]i32 = {i32(cur.r), i32(cur.g), i32(cur.b), i32(cur.a)}
+
+ blen: [4]i32
+ switch mode {
+ case .Src: blen = blend_src(back, pix, opacity)
+ case .Merge: blen = blend_merge(back, pix, opacity)
+ case .Neg_BW: blen = blend_neg_bw(back, pix, opacity)
+ case .Red_Tint: blen = blend_red_tint(back, pix, opacity)
+ case .Blue_Tint: blen = blend_blue_tint(back, pix, opacity)
+ case .Dst_Over: blen = blend_normal_dst_over(back, pix, opacity)
+ case .Addition: blen = blend_addition(back, pix, opacity)
+ case .Color: blen = blend_color(back, pix, opacity)
+ case .Color_Burn: blen = blend_color_burn(back, pix, opacity)
+ case .Color_Dodge: blen = blend_color_dodge(back, pix, opacity)
+ case .Darken: blen = blend_darken(back, pix, opacity)
+ case .Difference: blen = blend_difference(back, pix, opacity)
+ case .Divide: blen = blend_divide(back, pix, opacity)
+ case .Exclusion: blen = blend_exclusion(back, pix, opacity)
+ case .Hard_Light: blen = blend_hard_light(back, pix, opacity)
+ case .Hue: blen = blend_hue(back, pix, opacity)
+ case .Lighten: blen = blend_lighten(back, pix, opacity)
+ case .Luminosity: blen = blend_luminosity(back, pix, opacity)
+ case .Multiply: blen = blend_multiply(back, pix, opacity)
+ case .Normal: blen = blend_normal(back, pix, opacity)
+ case .Overlay: blen = blend_overlay(back, pix, opacity)
+ case .Saturation: blen = blend_saturation(back, pix, opacity)
+ case .Screen: blen = blend_screen(back, pix, opacity)
+ case .Soft_Light: blen = blend_soft_light(back, pix, opacity)
+ case .Subtract: blen = blend_subtract(back, pix, opacity)
+ case .Unspecified: fallthrough
+ case:
+ log.error("Invalid Bland Mode provided:", mode)
+ return last, .Invalid_Mode
+ }
+
+ normal := blend_normal(back, pix, opacity)
+ norm_merge := blend_merge(normal, blen, back.a)
+ blen = blend_merge(norm_merge, blen, alpha(back.a, alpha(pix.a, opacity)))
+
+ return {u8(blen.r), u8(blen.g), u8(blen.b), u8(blen.a)}, nil
+}
+
+
+/* ------------------------------------------------------------------- */
+// RGB Blenders
+blend_normal :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ if last.a == 0 {
+ res = cur
+ res.a = alpha(cur.a, opacity)
+ } else if cur.a == 0 {
+ return last
+ }
+
+ cur := cur
+ cur.a = alpha(cur.a, opacity)
+
+ res.a = cur.a + last.a - alpha(last.a, cur.a)
+ res.rgb = last.rgb + (cur.rgb - last.rgb) * cur.a / res.a
+
+ return res
+}
+
+blend_src :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ return last
+}
+
+blend_merge :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+
+ if last.a == 0 {
+ res.rgb = cur.rgb
+
+ } else if cur.a == 0 {
+ res.rgb = last.rgb
+
+ } else {
+ op: [4]i32 = opacity
+ res = last + mul((cur - last), op)
+ }
+
+ res.a = last.a + mul((cur.a - last.a), opacity)
+ if res.a == 0 {
+ res.rgb = 0
+ }
+
+ return
+}
+
+blend_neg_bw :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ if rgba_luma(last) < 128{
+ return { 255, 255, 255, 255 }
+ }
+ return { 0, 0, 0, 255 }
+}
+
+blend_red_tint :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ cur := cur
+ luma := rgba_luma(cur)
+ cur = { (255 + luma) >> 1, luma >> 1, luma >> 1, last.a }
+ return blend_normal(last, cur, opacity)
+}
+
+blend_blue_tint :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ luma := rgba_luma(cur)
+ res = { luma >> 1, luma >> 1, (255 + luma) >> 1, last.a }
+ return blend_normal(last, res, opacity)
+}
+
+blend_normal_dst_over :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.a = alpha(cur.a, opacity)
+ return blend_normal(last, res, opacity)
+}
+
+
+blend_multiply :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res = mul(last, cur)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_screen :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res = last + cur - mul(last, cur)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_overlay :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ hl :: proc(b, s: i32) -> i32 {
+ if s < 128 {
+ return mul(b, s<<1)
+ }
+ return b + ((s<<1)-255) - mul(b, (s<<1)-255)
+ }
+
+ res.r = hl(cur.r, last.r)
+ res.b = hl(cur.b, last.b)
+ res.g = hl(cur.g, last.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_darken :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.r = min(last.r, cur.r)
+ res.b = min(last.b, cur.b)
+ res.g = min(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_lighten :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.r = max(last.r, cur.r)
+ res.b = max(last.b, cur.b)
+ res.g = max(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_color_dodge :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ cd :: proc( #any_int b, s: u32) -> i32 {
+ if b == 0 {
+ return 0
+ }
+ s1 := 255 - s
+ if b >= s1 {
+ return 255
+ }
+ return i32(div(b, s1))
+ }
+
+ res.r = cd(last.r, cur.r)
+ res.b = cd(last.b, cur.b)
+ res.g = cd(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_color_burn :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ cb :: proc( #any_int b, s: u32) -> i32 {
+ if b == 255 {
+ return 255
+ }
+ b1 := 255 - b
+ if b1 >= s {
+ return 0
+ }
+ return i32(255 - div(b1, s))
+ }
+
+ res.r = cb(last.r, cur.r)
+ res.b = cb(last.b, cur.b)
+ res.g = cb(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_hard_light :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ hl :: proc(b, s: i32) -> i32 {
+ if s < 128 {
+ return mul(b, s<<1)
+ }
+ return b + ((s<<1)-255) - mul(b, (s<<1)-255)
+ }
+
+ res.r = hl(last.r, cur.r)
+ res.b = hl(last.b, cur.b)
+ res.g = hl(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_soft_light :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ sl :: proc(b, s: i32) -> i32 {
+ b := f64(b) / 255
+ s := f64(s) / 255
+ r, d: f64
+
+ if b <= 0.25 {
+ d = ((16 * b - 12) * b + 4) * b
+ } else {
+ d = math.sqrt(b)
+ }
+
+ if s <= 0.5 {
+ r = b - (1 - 2 * s) * b * (1 - b)
+ } else {
+ r = b + (2 * s - 1) * (d - b)
+ }
+ return i32(r * 255 + 0.5)
+ }
+
+ res.r = sl(last.r, cur.r)
+ res.b = sl(last.b, cur.b)
+ res.g = sl(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_difference :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.r = abs(last.r - cur.r)
+ res.b = abs(last.b - cur.b)
+ res.g = abs(last.g - cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_exclusion :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ ex :: proc(b, s: i32) -> i32 {
+ return b + s - (mul(b, s) << 1)
+ }
+
+ res.r = ex(last.r, cur.r)
+ res.b = ex(last.b, cur.b)
+ res.g = ex(last.g, cur.g)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+
+
+/* ------------------------------------------------------------------- */
+// HSV Blenders
+blend_hue :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ /*
+ https://github.com/alpine-alpaca/asefile/blob/main/src/blend.rs#L392
+ https://drafts.fxtf.org/compositing-1/#blendinghue
+ https://printtechnologies.org/standards/files/pdf-reference-1.6-addendum-blend-modes.pdf
+ https://github.com/aseprite/aseprite/blob/main/src/doc/blend_funcs.cpp#L425
+ https://gitlab.freedesktop.org/pixman/pixman/-/blob/master/pixman/pixman-combine-float.c?ref_type=heads#L908
+ */
+
+ lpix := [3]f64{f64(last.r), f64(last.g), f64(last.b)} / 255
+ sat := hsv_sat(lpix)
+ lum := hsv_luma(lpix)
+
+ cpix := [3]f64{f64(cur.r), f64(cur.g), f64(cur.b)} / 255
+ cpix = set_sat(cpix, sat)
+ cpix = set_luma(cpix, lum)
+ cpix *= 255
+
+ return blend_normal(last, {i32(cpix.r), i32(cpix.g), i32(cpix.b), cur.a}, opacity)
+}
+
+
+blend_saturation :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ pix := [3]f64{ f64(cur.r), f64(cur.g), f64(cur.b) }
+ pix /= 255
+ s := hsv_sat(pix)
+
+ pix = { f64(last.r), f64(last.g), f64(last.b) }
+ pix /= 255
+ l := hsv_luma(pix)
+
+ pix = set_luma(set_sat(pix, s), l)
+
+ pix *= 255
+ res = { i32(pix.r), i32(pix.g), i32(pix.b), cur.a }
+
+ return blend_normal(last, res, opacity)
+}
+
+
+blend_color :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ pix := [3]f64{ f64(last.r), f64(last.g), f64(last.b) }
+ pix /= 255
+ l := hsv_luma(pix)
+
+ pix = { f64(cur.r), f64(cur.g), f64(cur.b)}
+ pix /= 255
+ pix = set_luma(pix, l)
+
+ pix *= 255
+ res = { i32(pix.r), i32(pix.g), i32(pix.b), cur.a }
+
+ return blend_normal(last, res, opacity)
+}
+
+
+blend_luminosity :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ pix := [3]f64{ f64(cur.r), f64(cur.g), f64(cur.b) }
+ pix /= 255
+ l := hsv_luma(pix)
+
+ pix = { f64(last.r), f64(last.g), f64(last.b) }
+ pix /= 255
+ pix = set_luma(pix, l)
+
+ pix *= 255
+ res = { i32(pix.r), i32(pix.g), i32(pix.b), cur.a }
+
+ return blend_normal(last, res, opacity)
+}
+
+
+blend_addition :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.r = min(last.r + cur.r, 255)
+ res.g = min(last.g + cur.g, 255)
+ res.b = min(last.b + cur.b, 255)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_subtract :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ res.r = max(last.r - cur.r, 0)
+ res.g = max(last.g - cur.g, 0)
+ res.b = max(last.b - cur.b, 0)
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+blend_divide :: proc(last, cur: B_Pixel, opacity: i32) -> (res: B_Pixel) {
+ bd :: proc( #any_int b, s: u32) -> i32 {
+ if b == 0 {
+ return 0
+ } else if b >= s {
+ return 255
+ }
+ return i32(div(b, s))
+ }
+
+ res.r = bd( last.r, cur.r )
+ res.g = bd( last.g, cur.g )
+ res.b = bd( last.b, cur.b )
+ res.a = cur.a
+ return blend_normal(last, res, opacity)
+}
+
+
+
+/* ------------------------------------------------------------------- */
+// RGB Helpers
+
+rgba_luma :: proc(pix: B_Pixel) -> i32 {
+ return rgb_luma(pix.r, pix.b, pix.g)
+}
+
+rgb_luma :: proc(#any_int r, b, g: int) -> i32 {
+ return i32(( r*2126 + g*7152 + b*722 ) / 10000 )
+}
+
+/* ------------------------------------------------------------------- */
+// HSV Helpers
+
+hsv_luma :: proc(p: [3]f64) -> f64 {
+ return 0.3*p.r + 0.59*p.g + 0.11*p.b
+}
+
+hsv_sat :: proc(p: [3]f64) -> f64 {
+ return max(p.r, p.g, p.b) - min(p.r, p.g, p.b)
+}
+
+clip_color :: proc(pix: [3]f64) -> [3]f64 {
+ p := pix
+ lum := hsv_luma(p)
+ n := min(p.r, p.g, p.b)
+ x := max(p.r, p.g, p.b)
+
+ if n < 0 {
+ p = lum + (((p - lum) * lum) / (lum - n))
+ }
+
+ if x > 1 {
+ p = lum + (((p - lum) * (1 - lum)) / (x - lum))
+ }
+
+ return p
+}
+
+set_luma :: proc(p: [3]f64, l: f64) -> [3]f64 {
+ d := l - hsv_luma(p)
+ return clip_color(p + d)
+}
+
+
+set_sat :: proc(p: [3]f64, s: f64) -> (res: [3]f64) {
+ // https://github.com/aseprite/aseprite/blob/main/src/doc/blend_funcs.cpp#L400
+ // The chosen solution is a mix a asefile & pixman
+ // https://github.com/alpine-alpaca/asefile/blob/main/src/blend.rs#L522
+ // https://gitlab.freedesktop.org/pixman/pixman/-/blob/master/pixman/pixman-combine-float.c#L831
+
+
+ when ASE_USE_BUGGED_SAT == true {
+ res = p
+ MIN :: proc(x, y: ^f64) -> ^f64 {
+ return (x^ < y^) ? x : y
+ }
+ MAX :: proc(x, y: ^f64) -> ^f64 {
+ return (x^ > y^) ? x : y
+ }
+
+ r, g, b := &res.r, &res.g, &res.b
+ min := MIN(r, MIN(g, b))
+ max := MAX(r, MAX(g, b))
+ mid := r > g ? (g > b ? g : (r > b ? b : r)) : (g > b ? (b > r ? b : r): g)
+
+ if max > min {
+ mid^ = ((mid^ - min^) * s) / (max^ - min^)
+ max^ = s
+ } else {
+ mid^ = 0
+ max^ = 0
+ }
+ min^ = 0
+
+ } else {
+ val: [3]struct{v: f64, p: int} = {
+ {p.r, 0}, {p.g, 1}, {p.b, 2},
+ }
+ val.rg = val.r.v < val.g.v ? val.rg : val.gr
+ val.rb = val.r.v < val.b.v ? val.rb : val.br
+ val.gb = val.g.v < val.b.v ? val.gb : val.bg
+ min, mid, max := val.r.p, val.g.p, val.b.p
+
+ if (p[max] - p[min]) != 0 {
+ res[mid] = ( (p[mid] - p[min]) * s ) / ( p[max] - p[min] )
+ res[max] = s
+ }
+ }
+
+ return
+}
+
+
+/*
+ The following is for mine (blob) record keeping:
+
+ The way aseprite does set_set is very much bugged.
+ First the sorting doesn't work when if r == g and g < b
+
+ MIN :: proc(x, y: ^f64) -> ^f64 {
+ return (x^ < y^) ? x : y
+ }
+ MAX :: proc(x, y: ^f64) -> ^f64 {
+ return (y^ < x^) ? x : y
+ }
+
+ r, g, b := &pix.r, &pix.g, &pix.b
+ min := MIN(r, MIN(g, b))
+ max := MAX(r, MAX(g, b))
+ mid := r > g ? (g > b ? g : (r > b ? b : r)) : (g > b ? (b > r ? b : r): g)
+
+ Second the way it's value calculations is also wrong & only leaves the blue channel
+ if max > min {
+ mid^ = ((mid^ - min^) * s) / (max^ - min^)
+ max^ = s
+ } else {
+ mid^ = 0
+ max^ = 0
+ }
+
+ The sullotion griven by asefile
+ // r --*--*----- min
+ // | |
+ // g --*--|--*-- mid
+ // | |
+ // b -----*--*-- max
+
+ res = p
+ val := [3][2]f64{{p.r, 0}, {p.g, 1}, {p.b, 2}}
+
+ val.rg = val.r[0] < val.g[0] ? val.rg : val.gr
+ val.rb = val.r[0] < val.b[0] ? val.rb : val.br
+ val.gb = val.g[0] < val.b[0] ? val.gb : val.bg
+ min, mid, max := int(val.r[1]), int(val.g[1]), int(val.b[1])
+
+
+ if max > min {
+ res[mid] = ((res[mid] - res[min]) * s) / (res[max] - res[min])
+ res[max] = s
+ } else {
+ res[mid], res[max] = 0, 0
+ }
+ res[min] = 0
+
+ Pointer based version og working sullotion from pixman
+ res = p
+ min, mid, max: ^f64
+ if p.r > p.g {
+ if p.r > p.b {
+ max = &res.r
+ if p.g > p.b {
+ mid = &res.g
+ min = &res.b
+
+ } else {
+ mid = &res.b
+ min = &res.g
+ }
+
+ } else {
+ max = &res.b
+ mid = &res.r
+ min = &res.g
+ }
+
+ } else {
+ if p.r > p.b {
+ max = &res.g
+ mid = &res.r
+ min = &res.b
+
+ } else {
+ min = &res.r
+ if p.g > p.b {
+ max = &res.g
+ mid = &res.b
+
+ } else {
+ max = &res.b
+ mid = &res.g
+ }
+ }
+ }
+
+
+ if (max^ - min^) == 0 {
+ mid^ = 0
+ max^ = 0
+ } else {
+ mid^ = ((mid^ - min^) * s) / (max^ - min^)
+ max^ = s
+ }
+ min^ = 0
+
+
+ Pointerless pixman
+ min, mid, max: int
+ if p.r > p.g {
+ if p.r > p.b {
+ max = 0
+ if p.g > p.b {
+ mid = 1
+ min = 2
+
+ } else {
+ mid = 2
+ min = 1
+ }
+
+ } else {
+ max = 2
+ mid = 0
+ min = 1
+ }
+
+ } else {
+ if p.r > p.b {
+ max = 1
+ mid = 0
+ min = 2
+
+ } else {
+ min = 0
+ if p.g > p.b {
+ max = 1
+ mid = 2
+
+ } else {
+ max = 2
+ mid = 1
+ }
+ }
+ }
+
+ if (p[max] - p[min]) != 0 {
+ res[mid] = ((p[mid] - p[min]) * s) / (p[max] - p[min])
+ res[max] = s
+ }
+*/
diff --git a/tools/compile_assets/aseprite/utils/common.odin b/tools/compile_assets/aseprite/utils/common.odin
new file mode 100644
index 0000000..69d7c71
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/common.odin
@@ -0,0 +1,303 @@
+package aseprite_file_handler_utility
+
+import "base:runtime"
+import "core:mem"
+import "core:slice"
+import "core:image"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+import ase ".."
+
+
+UTILS_DEBUG_MODE :: #config(ASE_UTILS_DEBUG, ODIN_DEBUG)
+
+
+/* ================================== Destruction procs ================================== */
+destroy_frame :: proc(frame: Frame, alloc := context.allocator) -> runtime.Allocator_Error {
+ for &cel in frame.cels {
+ destroy(&cel, alloc) or_return
+ }
+ return delete(frame.cels, alloc)
+}
+
+destroy_frames :: proc(frames: []Frame, alloc := context.allocator) -> runtime.Allocator_Error {
+ for frame in frames {
+ for &cel in frame.cels {
+ destroy(&cel, alloc) or_return
+ }
+ delete(frame.cels, alloc) or_return
+ }
+ return delete(frames, alloc)
+}
+
+destroy_image :: proc(img: Image, alloc := context.allocator) -> runtime.Allocator_Error {
+ return delete(img.data, alloc)
+}
+
+destroy_images :: proc(imgs: []Image, alloc := context.allocator) -> runtime.Allocator_Error {
+ for img in imgs {
+ delete(img.data, alloc) or_return
+ }
+ return delete(imgs, alloc)
+}
+
+destroy_image_bytes :: proc(imgs: [][]byte, alloc := context.allocator) -> runtime.Allocator_Error {
+ for img in imgs {
+ delete(img, alloc) or_return
+ }
+ return delete(imgs, alloc)
+}
+
+destroy_animation :: proc(anim: ^Animation, alloc := context.allocator) -> runtime.Allocator_Error {
+ for frame in anim.frames {
+ delete(frame, alloc) or_return
+ }
+ return delete(anim.frames, alloc)
+}
+
+destroy_info :: proc(info: ^Info) -> runtime.Allocator_Error {
+ context.allocator = info.allocator
+ destroy_frames(info.frames) or_return
+ delete(info.layers) or_return
+ delete(info.palette) or_return
+ delete(info.tags) or_return
+ delete(info.tilesets) or_return
+ destroy_slices(info.slices) or_return
+ return nil
+}
+
+destroy_cel :: proc(cel: ^Cel, alloc := context.allocator) -> runtime.Allocator_Error {
+ return delete(cel.tilemap.tiles, alloc)
+}
+
+destroy_layers :: proc(lay: []Layer, alloc := context.allocator) -> runtime.Allocator_Error {
+ return delete(lay, alloc)
+}
+
+destroy_palette :: proc(pal: Palette, alloc := context.allocator) -> runtime.Allocator_Error {
+ return delete(pal)
+}
+
+destroy_slices :: proc(sls: []Slice, alloc := context.allocator) -> runtime.Allocator_Error {
+ for sl in sls {
+ destroy_slice(sl, alloc) or_return
+ }
+ return delete(sls)
+}
+
+destroy_slice :: proc(sl: Slice, alloc := context.allocator) -> runtime.Allocator_Error {
+ return delete(sl.keys)
+}
+
+destroy_sheet :: proc(s: Sprite_Sheet, alloc := context.allocator) -> runtime.Allocator_Error {
+ return destroy_image(s.img, alloc)
+}
+
+destroy :: proc {
+ destroy_frames,
+ destroy_image,
+ destroy_animation,
+ destroy_image_bytes,
+ destroy_images,
+ destroy_info,
+ destroy_cel,
+ destroy_frame,
+ destroy_layers,
+ destroy_palette,
+ destroy_slices,
+ destroy_slice,
+ destroy_sheet,
+}
+/* ======================================================================================= */
+
+
+get_metadata :: proc(header: ase.File_Header) -> (md: Metadata) {
+ return {
+ int(header.width),
+ int(header.height),
+ Pixel_Depth(header.color_depth),
+ u8(header.transparent_index),
+ }
+}
+
+
+// Use with slice.sort_by, .sort_by_with_indices, .stable_sort_by, .is_sorted_by & .reverse_sort_by
+cel_less :: proc(i, j: Cel) -> bool {
+ // https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md#note5
+ ior := i.layer + i.z_index
+ jor := j.layer + j.z_index
+ return ior < jor || (ior == jor && i.z_index < j.z_index )
+}
+
+// Use with slice.sort_by_cmp, .stable_sort_by_cmp, .is_sorted_cmp & .reverse_sort_by_cmp
+cel_cmp :: proc(i, j: Cel) -> slice.Ordering {
+ // https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md#note5
+ iz := i.z_index
+ jz := j.z_index
+ ior := i.layer + iz
+ jor := j.layer + jz
+
+ if ior < jor { return .Less }
+ else if ior > jor { return .Greater }
+ else if iz < jz { return .Less }
+ else if iz > jz { return .Greater }
+ return .Equal
+}
+
+
+has_new_palette :: proc(doc: ^ase.Document) -> bool {
+ for f in doc.frames {
+ for c in f.chunks {
+ _ = c.(ase.Palette_Chunk) or_continue
+ return true
+ }
+ }
+ return false
+}
+
+
+has_tileset :: proc(doc: ^ase.Document) -> bool {
+ for f in doc.frames {
+ for c in f.chunks {
+ _ = c.(ase.Tileset_Chunk) or_continue
+ return true
+ }
+ }
+ return false
+}
+
+
+// Uses Nearest-neighbor Upscaling
+upscale_image_from_bytes :: proc(img: []byte, md: Metadata, factor := 10, alloc := context.allocator) -> (res: []byte, res_md: Metadata, err: Errors) {
+ ch := int(md.bpp) >> 3
+ if len(img) != (md.width * md.height * ch) {
+ fast_log(.Error, "image size doesn't match metadata")
+ err = .Buffer_Size_Not_Match_Metadata
+ return
+ }
+
+ res = make([]byte, len(img) * factor * factor, alloc) or_return
+ res_md = {md.width*factor, md.height*factor, md.bpp, md.trans_idx}
+
+ for h in 0..<md.height {
+ for w in 0..<md.width {
+ start := (h*md.width*factor + w) * factor * ch
+ first := res[start:start + factor * ch]
+
+ copy(first[:ch], img[(h*md.width + w) * ch:])
+
+ for x in 1..<factor {
+ copy(res[start + x * ch:][:ch], first)
+ }
+
+ for y in 1..<factor {
+ copy(res[start + (y*md.width*factor*ch):], first)
+ }
+ }
+ }
+
+ return
+}
+
+// Uses Nearest-neighbor Upscaling
+upscale_image_from_img :: proc(img: Image, factor := 10, alloc := context.allocator) -> (res: Image, err: Errors) {
+ res.data, res.md = upscale_image_from_bytes(img.data, img.md, factor, alloc) or_return
+ return
+}
+
+// Uses Nearest-neighbor Upscaling
+upscale_image :: proc { upscale_image_from_img, upscale_image_from_bytes }
+
+
+upscale_all_from_imgs :: proc(imgs: []Image, factor := 10, alloc := context.allocator) -> (res: []Image, err: Errors) {
+ res = make([]Image, len(imgs), alloc) or_return
+ for img, pos in imgs {
+ res[pos] = upscale_image_from_img(img, factor, alloc) or_return
+ }
+ return
+}
+
+upscale_all_from_byte:: proc(imgs: [][]byte, md: Metadata, factor := 10, alloc := context.allocator) -> (res: [][]byte, res_md: Metadata, err: Errors) {
+ res = make([][]byte, len(imgs), alloc) or_return
+ if len(imgs) == 0 { return }
+ res[0], res_md = upscale_image_from_bytes(imgs[0], md, factor, alloc) or_return
+
+ for img, pos in imgs[1:] {
+ res[pos], _ = upscale_image_from_bytes(img, md, factor, alloc) or_return
+ }
+ return
+}
+
+upscale_all :: proc{ upscale_all_from_imgs, upscale_all_from_byte }
+
+
+compute_alpha :: proc(img: []u8, alloc := context.allocator) -> (res: []u8, err: Errors) {
+ if len(img) % 4 != 0 {
+ fast_log(.Error, "Given buffer isn't RGBA")
+ return nil, .Buffer_Not_RGBA
+ }
+
+ img_buf := mem.slice_data_cast([][4]u8, img)
+ buf := make([][3]u8, len(img_buf), alloc) or_return
+
+ for p, pos in img_buf {
+ bp := [4]i32{ i32(p.r), i32(p.g), i32(p.b), i32(p.a) }
+ bp.rgb = bp.rgb * bp.a / (255 + bp.a - alpha(bp.a, 255))
+ buf[pos] = { u8(bp.r), u8(bp.g), u8(bp.b) }
+ }
+
+ return mem.slice_data_cast([]u8, buf), nil
+}
+
+
+remove_alpha :: proc(img: []u8, alloc := context.allocator) -> (res: []u8, err: Errors) {
+ if len(img) % 4 != 0 {
+ fast_log(.Error, "Given buffer isn't RGBA")
+ return nil, .Buffer_Not_RGBA
+ }
+
+ img_buf := mem.slice_data_cast([][4]u8, img)
+ buf := make([][3]u8, len(img_buf), alloc) or_return
+
+ for p, pos in img_buf {
+ buf[pos] = p.rgb
+ }
+
+ return mem.slice_data_cast([]u8, buf), nil
+}
+
+
+// Converts `utils.Image` to a `core:image.Image` with allocation
+to_core_image :: proc(buf: []byte, md: Metadata, alloc := context.allocator) -> (img: image.Image, err: runtime.Allocator_Error) {
+ img.width = md.width
+ img.height = md.height
+ img.depth = 8
+ img.channels = 4
+ img.pixels.buf = make([dynamic]byte, len(buf), alloc) or_return
+
+ copy(img.pixels.buf[:], buf)
+ return
+}
+
+// Converts `utils.Image` to a `core:image.Image` with no allocation
+to_core_image_non_alloc :: proc(buf: []byte, md: Metadata) -> (img: image.Image) {
+ img.width = md.width
+ img.height = md.height
+ img.depth = 8
+ img.channels = 4
+
+ raw := runtime.Raw_Dynamic_Array {
+ data = raw_data(buf),
+ len = len(buf),
+ cap = len(buf),
+ allocator = runtime.nil_allocator(),
+ }
+
+ img.pixels.buf = transmute([dynamic]byte)raw
+ return
+}
+
+
+
diff --git a/tools/compile_assets/aseprite/utils/extract.odin b/tools/compile_assets/aseprite/utils/extract.odin
new file mode 100644
index 0000000..f1b009d
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/extract.odin
@@ -0,0 +1,613 @@
+package aseprite_file_handler_utility
+
+import "base:runtime"
+import "core:math/fixed"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+import ase ".."
+
+
+cels_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Cel, err: runtime.Allocator_Error) {
+ cels := make([dynamic]Cel, alloc) or_return
+ defer if err != nil { delete(cels) }
+
+ for frame in doc.frames {
+ f_cels := get_cels(frame, alloc) or_return
+ for &c in f_cels {
+ if c.raw == nil && c.tilemap.tiles == nil {
+ for l in cels[c.link:] {
+ if l.layer == c.layer {
+ c.height = l.height
+ c.width = l.width
+ c.raw = l.raw
+ }
+ }
+ }
+ }
+
+ append(&cels, ..f_cels) or_return
+ delete(f_cels, alloc) or_return
+ }
+
+ return cels[:], nil
+}
+
+cels_from_doc_frame :: proc(frame: ase.Frame, alloc := context.allocator) -> (res: []Cel, err: runtime.Allocator_Error) {
+ cels := make([dynamic]Cel, alloc) or_return
+ defer if err != nil { delete(cels) }
+
+ for chunk in frame.chunks {
+ #partial switch c in chunk {
+ case ase.Cel_Chunk:
+ cel := Cel {
+ pos = { int(c.x), int(c.y) },
+ opacity = int(c.opacity_level),
+ z_index = int(c.z_index),
+ layer = int(c.layer_index),
+ }
+
+ switch v in c.cel {
+ case ase.Com_Image_Cel:
+ cel.width = int(v.width)
+ cel.height = int(v.height)
+ cel.raw = v.pixels
+
+ case ase.Raw_Cel:
+ cel.width = int(v.width)
+ cel.height = int(v.height)
+ cel.raw = v.pixels
+
+ case ase.Linked_Cel:
+ cel.link = int(v)
+
+ case ase.Com_Tilemap_Cel:
+
+ cel.tilemap = Tilemap {
+ width = int(v.width),
+ height = int(v.height),
+ x_flip = uint(v.bitmask_x), // Bitmask for X flip
+ y_flip = uint(v.bitmask_y), // Bitmask for Y flip
+ diag_flip = uint(v.bitmask_diagonal), // Bitmask for diagonal flip (swap X/Y axis)
+ tiles = make([]int, len(v.tiles), alloc) or_return,
+ }
+
+ for &n, p in cel.tilemap.tiles {
+ switch t in v.tiles[p] {
+ case ase.BYTE: n = int(t)
+ case ase.WORD: n = int(t)
+ case ase.DWORD: n = int(t)
+ }
+ }
+
+ }
+ append(&cels, cel) or_return
+
+ case ase.Cel_Extra_Chunk:
+ if ase.Cel_Extra_Flag.Precise in c.flags {
+ extra := Precise_Bounds {
+ fixed.to_f64(c.x), fixed.to_f64(c.y),
+ fixed.to_f64(c.width), fixed.to_f64(c.height),
+ }
+ cels[len(cels)-1].extra = extra
+ }
+ }
+ }
+
+ return cels[:], nil
+}
+
+get_cels :: proc{ cels_from_doc_frame, cels_from_doc }
+
+
+layers_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Layer, err: runtime.Allocator_Error) {
+ layers := make([dynamic]Layer, alloc) or_return
+ defer if err != nil { delete(layers) }
+
+ for frame in doc.frames {
+ f_lays := get_layers(frame, .Layer_Opacity in doc.header.flags) or_return
+ append(&layers, ..f_lays) or_return
+ delete(f_lays, alloc) or_return
+ }
+
+ return layers[:], nil
+}
+
+layers_from_doc_frame :: proc(frame: ase.Frame, layer_valid_opacity := false, alloc := context.allocator) -> (res: []Layer, err: runtime.Allocator_Error) {
+ layers := make([dynamic]Layer, alloc) or_return
+ defer if err != nil { delete(layers) }
+
+ all_lays := make([dynamic]^ase.Layer_Chunk) or_return
+ defer delete(all_lays)
+
+ for chunk in frame.chunks {
+ #partial switch &v in chunk {
+ case ase.Layer_Chunk:
+ lay := Layer {
+ name = v.name,
+ opacity = int(v.opacity) if layer_valid_opacity else 255,
+ index = len(layers),
+ blend_mode = Blend_Mode(v.blend_mode),
+ visiable = .Visiable in v.flags,
+ tileset = int(v.tileset_index),
+ is_background = .Background in v.flags,
+ }
+
+ #reverse for l in all_lays {
+ if l.type == .Group {
+ if .Visiable not_in l.flags {
+ lay.visiable = false
+ break
+ }
+ if l.child_level == 0 {
+ break
+ }
+ }
+ }
+
+ append(&all_lays, &v) or_return
+ append(&layers, lay) or_return
+ }
+ }
+
+ return layers[:], nil
+}
+
+get_layers :: proc{ layers_from_doc_frame, layers_from_doc }
+
+
+tags_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Tag, err: runtime.Allocator_Error) {
+ tags := make([dynamic]Tag, alloc)
+ defer if err != nil { delete(tags) }
+
+ for frame in doc.frames {
+ f_tags := get_tags(frame, alloc) or_return
+ append(&tags, ..f_tags) or_return
+ delete(f_tags, alloc) or_return
+ }
+
+ return tags[:], nil
+}
+
+tags_from_doc_frame :: proc(frame: ase.Frame, alloc := context.allocator) -> (res: []Tag, err: runtime.Allocator_Error) {
+ tags := make([dynamic]Tag, alloc) or_return
+ defer if err != nil { delete(tags) }
+
+ for chunk in frame.chunks {
+ #partial switch v in chunk {
+ case ase.Tags_Chunk:
+ for t in v {
+ tag := Tag {
+ from = int(t.from_frame),
+ to = int(t.to_frame),
+ direction = t.loop_direction,
+ name = t.name,
+ }
+ append(&tags, tag) or_return
+ }
+ }
+ }
+
+ return tags[:], nil
+}
+
+get_tags :: proc{ tags_from_doc_frame, tags_from_doc }
+
+
+frames_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (frames: []Frame, err: runtime.Allocator_Error) {
+ return get_frames(doc.frames, alloc)
+}
+
+frames_from_doc_frames :: proc(data: []ase.Frame, alloc := context.allocator) -> (frames: []Frame, err: runtime.Allocator_Error) {
+ res := make([dynamic]Frame, alloc) or_return
+ defer if err != nil { delete(res) }
+
+ for frame in data {
+ append(&res, get_frame(frame) or_return) or_return
+ }
+ return
+}
+
+get_frames :: proc {
+ frames_from_doc,
+ frames_from_doc_frames,
+}
+
+get_frame :: proc(data: ase.Frame, alloc := context.allocator) -> (frame: Frame, err: runtime.Allocator_Error) {
+ frame.duration = i64(data.header.duration)
+ frame.cels = get_cels(data, alloc) or_return
+ return
+}
+
+
+palette_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (palette: Palette, err: Errors) {
+ pal := make([dynamic]Color, alloc) or_return
+ defer if err != nil { delete(pal) }
+
+ for frame in doc.frames {
+ get_palette(frame, &pal, has_new_palette(doc)) or_return
+ }
+
+ return pal[:], nil
+}
+
+palette_from_doc_frame:: proc(frame: ase.Frame, pal: ^[dynamic]Color, has_new: bool) -> (err: Errors) {
+ for chunk in frame.chunks {
+ #partial switch c in chunk {
+ case ase.Palette_Chunk:
+ if int(c.last_index) >= len(pal) {
+ resize(pal, int(c.last_index)+1) or_return
+ }
+
+ for i in c.first_index..=c.last_index {
+ if int(i) > len(pal) {
+ return Palette_Error.Color_Index_Out_of_Bounds
+ }
+
+ if n, ok := c.entries[i].name.(string); ok {
+ pal[i].name = n
+ }
+ pal[i].color = c.entries[i].color
+
+ }
+
+ case ase.Old_Palette_256_Chunk:
+ if has_new { continue }
+ for p in c {
+ first := len(pal) + int(p.entries_to_skip)
+ last := first + len(p.colors)
+ if last >= len(pal) {
+ resize(pal, last) or_return
+ }
+
+ for i in first..<last {
+ if i >= len(pal) {
+ return Palette_Error.Color_Index_Out_of_Bounds
+ }
+ pal[i].color.rgb = p.colors[i]
+ if p.colors[i] != 0 {
+ pal[i].color.a = 255
+ }
+
+ }
+ }
+
+ case ase.Old_Palette_64_Chunk:
+ if has_new { continue }
+ for p in c {
+ first := len(pal) + int(p.entries_to_skip)
+ last := first + len(p.colors)
+ if last >= len(pal) {
+ resize(pal, last) or_return
+ }
+
+ for i in first..<last {
+ if i >= len(pal) {
+ return Palette_Error.Color_Index_Out_of_Bounds
+ }
+
+ pal[i].color.rgb = p.colors[i]
+ if p.colors[i] != 0 {
+ pal[i].color.a = 255
+ }
+ }
+ }
+ }
+ }
+
+ return
+}
+
+get_palette :: proc{ palette_from_doc, palette_from_doc_frame }
+
+
+tileset_from_doc :: proc(doc: ^ase.Document, alloc := context.allocator) -> (ts: []Tileset, err: runtime.Allocator_Error) {
+ buf := make([dynamic]Tileset, alloc) or_return
+ for frame in doc.frames {
+ err = get_tileset(frame, &buf, alloc)
+ if err != nil {
+ return buf[:], err
+ }
+ }
+ if len(buf) > 0 {
+ log.warn("Tilemaps & Tilesets currently only work for RGBA colour space.")
+ }
+ return buf[:], nil
+}
+
+tileset_from_doc_frame :: proc(frame: ase.Frame, buf: ^[dynamic]Tileset, alloc := context.allocator) -> (err: runtime.Allocator_Error) {
+ for chunk in frame.chunks {
+ #partial switch v in chunk {
+ case ase.Tileset_Chunk:
+ ts: Tileset = {
+ int(v.id),
+ int(v.width),
+ int(v.height),
+ int(v.num_of_tiles),
+ v.name,
+ nil,
+ }
+
+ if t, ok := v.compressed.(ase.Tileset_Compressed); ok {
+ ts.tiles = (Pixels)(t)
+ }
+
+ append(buf, ts) or_return
+
+ case ase.User_Data_Chunk:
+ }
+ }
+
+ return
+}
+
+get_tileset :: proc{ tileset_from_doc, tileset_from_doc_frame }
+
+
+get_info :: proc(doc: ^ase.Document, info: ^Info, alloc := context.allocator) -> (err: Errors) {
+ context.allocator = alloc
+ info.allocator = alloc
+
+ layer_valid_opacity := .Layer_Opacity in doc.header.flags
+ has_new := has_new_palette(doc)
+
+ frames := make([dynamic]Frame) or_return
+ lays := make([dynamic]Layer) or_return
+ tags := make([dynamic]Tag) or_return
+ all_ts := make([dynamic]Tileset) or_return
+ pal := make([dynamic]Color) or_return
+ sls := make([dynamic]Slice) or_return
+ md := get_metadata(doc.header)
+
+ all_lays := make([dynamic]^ase.Layer_Chunk) or_return
+ defer delete(all_lays)
+
+ hue_sat_warn: bool
+
+ // TODO: Make big assumption that only Cel Chunks appear after first frame.
+
+ for doc_frame in doc.frames {
+ frame: Frame
+ frame.duration = i64(doc_frame.header.duration)
+ cels := make([dynamic]Cel) or_return
+
+ for &chunk in doc_frame.chunks {
+
+ #partial switch &c in chunk {
+ case ase.Cel_Chunk:
+ cel := Cel {
+ pos = {int(c.x), int(c.y)},
+ opacity = int(c.opacity_level),
+ z_index = int(c.z_index),
+ layer = int(c.layer_index),
+ }
+
+ switch v in c.cel {
+ case ase.Com_Image_Cel:
+ cel.width = int(v.width)
+ cel.height = int(v.height)
+ cel.raw = v.pixels
+
+ case ase.Raw_Cel:
+ cel.width = int(v.width)
+ cel.height = int(v.height)
+ cel.raw = v.pixels
+
+ case ase.Linked_Cel:
+ for l in frames[v].cels {
+ if l.layer == cel.layer {
+ cel.height = l.height
+ cel.width = l.width
+ cel.raw = l.raw
+ cel.link = int(v)
+ }
+ }
+
+ case ase.Com_Tilemap_Cel:
+ cel.tilemap = Tilemap {
+ width = int(v.width),
+ height = int(v.height),
+ x_flip = uint(v.bitmask_x), // Bitmask for X flip
+ y_flip = uint(v.bitmask_y), // Bitmask for Y flip
+ diag_flip = uint(v.bitmask_diagonal), // Bitmask for diagonal flip (swap X/Y axis)
+ tiles = make([]int, len(v.tiles), alloc) or_return,
+ }
+
+ for &n, p in cel.tilemap.tiles {
+ switch t in v.tiles[p] {
+ case ase.BYTE: n = int(t)
+ case ase.WORD: n = int(t)
+ case ase.DWORD: n = int(t)
+ }
+ }
+ }
+
+ append(&cels, cel) or_return
+
+ case ase.Cel_Extra_Chunk:
+ if ase.Cel_Extra_Flag.Precise in c.flags {
+ extra := Precise_Bounds {
+ fixed.to_f64(c.x), fixed.to_f64(c.y),
+ fixed.to_f64(c.width), fixed.to_f64(c.height),
+ }
+ cels[len(cels)-1].extra = extra
+ }
+
+ case ase.Layer_Chunk:
+ lay := Layer {
+ name = c.name,
+ opacity = int(c.opacity) if layer_valid_opacity else 255,
+ index = len(lays),
+ blend_mode = Blend_Mode(c.blend_mode),
+ visiable = .Visiable in c.flags,
+ tileset = int(c.tileset_index),
+ is_background = .Background in c.flags,
+ }
+
+ when !ASE_USE_BUGGED_SAT {
+ if !hue_sat_warn && (lay.blend_mode == .Saturation || lay.blend_mode == .Hue) {
+ log.infof("Layer: \"%v\"; \"%v\" blend mode is bugged in Aseprite, in ways we can't replicate.", lay.name, lay.blend_mode)
+ log.info("By default we use a fixed version. Compile with `ASE_USE_BUGGED_SAT=true` to use a bugged version.")
+ hue_sat_warn = true
+ }
+ }
+
+
+ if c.child_level != 0 {
+ #reverse for l in all_lays {
+ if l.type == .Group {
+ if .Visiable not_in l.flags {
+ lay.visiable = false
+ break
+ }
+ if l.child_level == 0 {
+ break
+ }
+ }
+ }
+ }
+
+ append(&lays, lay) or_return
+ append(&all_lays, &c) or_return
+
+ case ase.Tags_Chunk:
+ for t in c {
+ tag := Tag {
+ int(t.from_frame),
+ int(t.to_frame),
+ t.loop_direction,
+ t.name,
+ }
+ append(&tags, tag) or_return
+ }
+
+ case ase.Palette_Chunk:
+ if int(c.last_index) >= len(pal) {
+ resize(&pal, int(c.last_index)+1) or_return
+ }
+
+ for i in c.first_index..=c.last_index {
+ if int(i) >= len(pal) {
+ err = Palette_Error.Color_Index_Out_of_Bounds
+ return
+ }
+
+ if n, ok := c.entries[i].name.(string); ok {
+ pal[i].name = n
+ }
+ pal[i].color = c.entries[i].color
+ }
+
+ case ase.Old_Palette_256_Chunk:
+ if has_new { continue }
+ for p in c {
+ first := len(pal) + int(p.entries_to_skip)
+ last := first + len(p.colors)
+ if last >= len(pal) {
+ resize(&pal, last) or_return
+ }
+
+ for i in first..<last {
+ if i >= len(pal) {
+ err = Palette_Error.Color_Index_Out_of_Bounds
+ return
+ }
+ pal[i].color.rgb = p.colors[i]
+ if p.colors[i] != 0 {
+ pal[i].color.a = 255
+ }
+ }
+ }
+
+ case ase.Old_Palette_64_Chunk:
+ if has_new { continue }
+ for p in c {
+ first := len(pal) + int(p.entries_to_skip)
+ last := first + len(p.colors)
+ if last >= len(pal) {
+ resize(&pal, last) or_return
+ }
+
+ for i in first..<last {
+ if i >= len(pal) {
+ err = Palette_Error.Color_Index_Out_of_Bounds
+ return
+ }
+ if max(p.colors[i].r, p.colors[i].b, p.colors[i].g) > 63 {
+ err = Palette_Error.Color_Index_Out_of_Bounds
+ return
+ }
+
+ // https://github.com/alpine-alpaca/asefile/blob/2274c354cea6764f85597252a0d2228e64709348/src/palette.rs#L134
+ // Scale such that 0 -> 0 & 63 -> 255
+ pal[i].color.r = p.colors[i].r << 2 | p.colors[i].r >> 4
+ pal[i].color.g = p.colors[i].g << 2 | p.colors[i].g >> 4
+ pal[i].color.b = p.colors[i].b << 2 | p.colors[i].b >> 4
+ if p.colors[i] != 0 {
+ pal[i].color.a = 255
+ }
+ }
+ }
+
+ case ase.Tileset_Chunk:
+ ts: Tileset
+ ts.id = int(c.id)
+ ts.width = int(c.width)
+ ts.height = int(c.height)
+ ts.name = c.name
+
+ if t, ok := c.compressed.(ase.Tileset_Compressed); ok {
+ ts.tiles = (Pixels)(t)
+ }
+
+ append(&all_ts, ts) or_return
+
+ case ase.Slice_Chunk:
+ sl: Slice
+ sl.name = c.name
+ sl.flags = c.flags
+ sl.keys = make([]Slice_Key, len(c.keys))
+
+ for &key, pos in sl.keys {
+ c_key := c.keys[pos]
+ key.frame = int(c_key.frame_num)
+
+ key.x = int(c_key.x)
+ key.y = int(c_key.y)
+ key.w = int(c_key.width)
+ key.h = int(c_key.height)
+
+ if center, ok := c_key.center.(ase.Slice_Center); ok {
+ key.center = {
+ int(center.x), int(center.y),
+ int(center.width), int(center.height),
+ }
+ }
+
+ if pivot, ok := c_key.pivot.(ase.Slice_Pivot); ok {
+ key.pivot = { int(pivot.x), int(pivot.y) }
+ }
+ }
+ append(&sls, sl)
+ }
+ }
+
+ frame.cels = cels[:]
+ append(&frames, frame) or_return
+ }
+
+ info^ = {
+ frames = frames[:],
+ layers = lays[:],
+ tags = tags[:],
+ tilesets = all_ts[:],
+ slices = sls[:],
+ palette = pal[:],
+ md = md,
+ allocator = alloc,
+ }
+
+ return nil
+}
+
diff --git a/tools/compile_assets/aseprite/utils/image.odin b/tools/compile_assets/aseprite/utils/image.odin
new file mode 100644
index 0000000..ded33b7
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/image.odin
@@ -0,0 +1,443 @@
+package aseprite_file_handler_utility
+
+import "base:runtime"
+import ir "base:intrinsics"
+
+import "core:slice"
+import "core:mem"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+import ase ".."
+
+
+// Only Uses the first frame
+get_image_from_doc :: proc(doc: ^ase.Document, frame := 0, alloc := context.allocator) -> (img: Image, err: Errors) {
+ context.allocator = alloc
+
+ if len(doc.frames) <= frame {
+ return {}, Image_Error.Frame_Index_Out_Of_Bounds
+ }
+
+ info: Info
+ get_info(doc, &info) or_return
+ defer destroy(&info)
+
+ return get_image_from_frame(info.frames[frame], info)
+}
+
+get_image_from_doc_frame :: proc(
+ frame: ase.Frame, info: Info,
+) -> (img: Image, err: Errors) {
+
+ raw_frame := get_frame(frame, info.allocator) or_return
+ defer delete(raw_frame.cels, info.allocator)
+
+ return get_image_from_frame(raw_frame, info)
+}
+
+get_image_from_cel :: proc(
+ cel: Cel, layer: Layer, info: Info,
+) -> (img: Image, err: Errors) {
+ img.width = cel.width
+ img.height = cel.height
+ img.bpp = .RGBA
+
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[layer.tileset]
+ c := cel_from_tileset(cel, ts, info.md.bpp, info.allocator) or_return
+ defer delete(c.raw)
+
+ img.data = make([]byte, c.width * c.height * 4, info.allocator) or_return
+ if !layer.is_background && info.md.bpp == .Indexed {
+ img_p := mem.slice_data_cast([]Pixel, img.data)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+ }
+ write_cel(img.data[:], c, layer, info.md, info.palette) or_return
+
+ } else {
+ img.data = make([]byte, cel.width * cel.height * 4, info.allocator) or_return
+ if !layer.is_background && info.md.bpp == .Indexed {
+ img_p := mem.slice_data_cast([]Pixel, img.data)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+ }
+ write_cel(img.data[:], cel, layer, info.md, info.palette) or_return
+ }
+
+ return
+}
+
+get_image_from_frame :: proc(
+ frame: Frame, info: Info,
+) -> (img: Image, err: Errors) {
+
+ img.md = info.md
+ img.bpp = .RGBA
+ img.data = get_image_bytes(frame, info) or_return
+ return
+}
+
+get_image :: proc {
+ get_image_from_doc,
+ get_image_from_doc_frame,
+ get_image_from_frame,
+ get_image_from_cel,
+}
+
+
+// Only Uses the First Frame
+get_image_bytes_from_doc :: proc(doc: ^ase.Document, frame := 0, alloc := context.allocator) -> (img: []byte, err: Errors) {
+ context.allocator = alloc
+ md := get_metadata(doc.header)
+
+ raw_frame := get_frame(doc.frames[frame]) or_return
+ defer delete(raw_frame.cels)
+
+ layers := get_layers(doc) or_return
+ defer delete(layers)
+
+ palette := get_palette(doc) or_return
+ defer delete(palette)
+
+ ts := get_tileset(doc) or_return
+ defer delete(ts)
+
+ info := Info{layers=layers, palette=palette, tilesets=ts, allocator=alloc, md=md}
+
+ return get_image_bytes(raw_frame, info)
+}
+
+get_image_bytes_from_doc_frame :: proc(
+ frame: ase.Frame, info: Info,
+) -> (img: []byte, err: Errors) {
+
+ raw_frame := get_frame(frame, info.allocator) or_return
+ defer destroy(raw_frame, info.allocator)
+ return get_image_bytes(raw_frame, info)
+}
+
+get_image_bytes_from_cel :: proc (
+ cel: Cel, layer: Layer, info: Info,
+) -> (img: []byte, err: Errors) {
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[layer.tileset]
+ c := cel_from_tileset(cel, ts, info.md.bpp, info.allocator) or_return
+ defer delete(c.raw)
+
+ img = make([]byte, c.width * c.height * 4, info.allocator) or_return
+ if !layer.is_background && info.md.bpp == .Indexed {
+ img_p := mem.slice_data_cast([]Pixel, img)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+ }
+ write_cel(img, c, layer, info.md, info.palette) or_return
+
+ } else {
+ img = make([]byte, cel.width * cel.height * 4, info.allocator) or_return
+ if !layer.is_background && info.md.bpp == .Indexed {
+ img_p := mem.slice_data_cast([]Pixel, img)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+ }
+ write_cel(img, cel, layer, info.md, info.palette) or_return
+ }
+
+ return
+}
+
+get_image_bytes_from_frame :: proc(
+ frame: Frame, info: Info,
+) -> (img: []byte, err: Errors) {
+ context.allocator = info.allocator
+
+ img = make([]byte, info.md.width * info.md.height * 4) or_return
+ if len(frame.cels) == 0 { return }
+
+ if !slice.is_sorted_by(frame.cels, cel_less) {
+ slice.sort_by(frame.cels, cel_less)
+ }
+
+ if !info.layers[0].is_background && info.md.bpp == .Indexed {
+ img_p := mem.slice_data_cast([]Pixel, img)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+ }
+
+ for cel in frame.cels {
+ lay := info.layers[cel.layer]
+ if !lay.visiable { continue }
+
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[lay.tileset]
+ c := cel_from_tileset(cel, ts, info.md.bpp, info.allocator) or_return
+ defer delete(c.raw)
+
+ write_cel(img, c, lay, info.md, info.palette) or_return
+
+ } else {
+ write_cel(img, cel, lay, info.md, info.palette) or_return
+ }
+ }
+
+ return
+}
+
+get_image_bytes :: proc {
+ get_image_bytes_from_doc,
+ get_image_bytes_from_doc_frame,
+ get_image_bytes_from_cel,
+ get_image_bytes_from_frame,
+}
+
+
+get_all_images :: proc(doc: ^ase.Document, alloc := context.allocator) -> (imgs: []Image, err: Errors) {
+ context.allocator = alloc
+ imgs = make([]Image, len(doc.frames)) or_return
+ defer if err != nil { destroy(imgs)}
+
+ info: Info
+ get_info(doc, &info) or_return
+ defer destroy(&info)
+
+ for frame, p in info.frames {
+ imgs[p] = get_image(frame, info) or_return
+ }
+
+ return
+}
+
+get_all_images_bytes :: proc(doc: ^ase.Document, alloc := context.allocator) -> (imgs: [][]byte, err: Errors) {
+ context.allocator = alloc
+ imgs = make([][]byte, len(doc.frames)) or_return
+ defer if err != nil { destroy(imgs) }
+
+ md := get_metadata(doc.header)
+
+ layers := get_layers(doc) or_return
+ defer delete(layers)
+
+ palette := get_palette(doc) or_return
+ defer delete(palette)
+
+ ts := get_tileset(doc) or_return
+ defer delete(ts)
+
+ info := Info{layers=layers, palette=palette, tilesets=ts, allocator=alloc, md=md}
+
+ for frame, p in doc.frames {
+ imgs[p] = get_image_bytes(frame, info) or_return
+ }
+
+ return
+}
+
+
+get_cels_as_imgs :: proc(doc: ^ase.Document, frame_idx := 0, alloc := context.allocator) -> (res: []Image, err: Errors) {
+ context.allocator = alloc
+
+ info: Info
+ get_info(doc, &info) or_return
+ defer destroy(&info)
+
+ if len(info.frames) < frame_idx {
+ return nil, .Frame_Index_Out_Of_Bounds
+ }
+
+ frame := info.frames[frame_idx]
+
+ res = make([]Image, len(frame.cels)) or_return
+ if len(frame.cels) == 0 { return }
+
+ if !slice.is_sorted_by(frame.cels, cel_less) {
+ slice.sort_by(frame.cels, cel_less)
+ }
+
+ for cel, i in frame.cels {
+ lay := info.layers[cel.layer]
+ if !lay.visiable { continue }
+
+ res[i] = get_image_from_cel(cel, lay, info) or_return
+ }
+
+ return
+}
+
+
+get_all_cels_as_imgs :: proc(doc: ^ase.Document, alloc := context.allocator) -> (res: []Image, err: Errors) {
+ context.allocator = alloc
+
+ info: Info
+ get_info(doc, &info) or_return
+ defer destroy(&info)
+
+ imgs := make([dynamic]Image) or_return
+
+ for frame in info.frames {
+ if len(frame.cels) == 0 { continue }
+
+ if !slice.is_sorted_by(frame.cels, cel_less) {
+ slice.sort_by(frame.cels, cel_less)
+ }
+
+ for cel in frame.cels {
+ lay := info.layers[cel.layer]
+ if !lay.visiable { continue }
+
+ img := get_image_from_cel(cel, lay, info) or_return
+ append(&imgs, img)
+ }
+ }
+
+ return imgs[:], nil
+}
+
+cel_from_tileset :: proc(cel: Cel, ts: Tileset, chans: Pixel_Depth, alloc: runtime.Allocator) -> (c: Cel, err: Errors) {
+ c = cel
+ c.width = cel.tilemap.width * ts.width
+ c.height = cel.tilemap.height * ts.height
+ ch := int(chans)/8
+
+ if (ts.height * ts.width * len(c.tilemap.tiles) * ch) != (c.width * c.height * ch) {
+ return {}, .Tileset_Cel_Sizes_Mismatch
+ }
+
+ c.raw = make([]byte, ts.height * ts.width * len(c.tilemap.tiles) * ch, alloc) or_return
+
+ for h in 0..<c.tilemap.height {
+ for w in 0..<c.tilemap.width {
+ s := (h * c.tilemap.width * ts.height + w) * ts.width * ch
+ s1 := c.tilemap.tiles[h * c.tilemap.width + w] * ts.width * ts.height * ch
+
+ for y in 0..<ts.height {
+ copy(c.raw[s+(y*ts.width*c.tilemap.width*ch):][:ch*ts.width], ts.tiles[s1+(y*ts.width*ch):])
+ }
+ }
+ }
+
+ return
+}
+
+// Write a cel to an image's data. Assumes tilemaps & linked cels have already been handled.
+write_cel :: proc (
+ buf: []byte, cel: Cel, layer: Layer, md: Metadata,
+ pal: Palette = nil,
+) -> (err: Errors) {
+ // TODO: Allow for both arbitrary reads & writes, i.e. cropping.
+
+ if len(cel.raw) <= 0 {
+ fast_log(.Debug, "No Cel data to write.")
+ return
+ }
+
+ if len(buf) < (md.height * md.width * 4) {
+ fast_log(.Error, "Image buffer size is smaller than Metadata.")
+ return .Buffer_To_Small
+ }
+
+ when UTILS_DEBUG_MODE {
+ switch md.bpp {
+ case .Indexed:
+ if pal == nil {
+ fast_log(.Error, "Indexed Color Mode. No Palette")
+ return .Indexed_BPP_No_Palette
+ }
+ fallthrough
+ case .Grayscale, .RGBA:
+ if len(cel.raw) % (int(md.bpp) / 8) != 0 {
+ fast_log(.Error, "Size of cel not a multipule of channels. ")
+ return .Cel_Size_Not_Of_BPP
+ }
+ case:
+ fast_log(.Error, "Invalid Color Mode: ", md.bpp)
+ return .Invalid_BPP
+ }
+ }
+
+ /*offset := [2]int {
+ abs(cel.x) if cel.x < 0 else 0,
+ abs(cel.y) if cel.y < 0 else 0,
+ }*/
+ offset := [2]int {
+ abs(min(0, cel.x)),
+ abs(min(0, cel.y)),
+ }
+
+ bounds := Bounds {
+ x = clamp(cel.x, 0, md.width),
+ y = clamp(cel.y, 0, md.height),
+ width = clamp(cel.width, 0, md.width),
+ height = clamp(cel.height, 0, md.height),
+ }
+
+ when UTILS_DEBUG_MODE {
+ if !( bounds.x <= md.width && bounds.y <= md.height \
+ && bounds.width <= md.width && bounds.height <= md.height \
+ && offset.x <= md.width && offset.y <= md.height \
+ && bounds.x >= 0 && bounds.y >= 0 \
+ && bounds.width >= 0 && bounds.height >= 0 \
+ && offset.x >= 0 && offset.y >= 0 ) {
+ fast_log(.Error, "Cel out of bounds of Image bounds.")
+ return .Cel_Out_Of_Bounds,
+ }
+ }
+
+ y_loop: for y in 0..<bounds.height {
+ for x in 0..<bounds.width {
+ pix: [4]byte
+ idx := (y + offset.y) * cel.width + x + offset.x
+
+ // Convert to RGBA
+ switch md.bpp {
+ case .Indexed:
+ if cel.raw[idx] == md.trans_idx {
+ continue
+ }
+ pix = pal[cel.raw[idx]].color
+
+ case .Grayscale:
+ pix.rgb = cel.raw[idx * 2]
+ pix.a = cel.raw[idx * 2 + 1]
+
+ case .RGBA:
+ // Note(blob):
+ // This is comparable to slice casting before & idxing in to that.
+ // `mem.slice_data_cast()` or `slice.reinterpret()`
+ // On average is slightly faster but more variable in optimized builds
+ pix = ir.unaligned_load((^[4]byte)(&cel.raw[idx * 4]))
+ }
+
+ if pix.a != 0 {
+ iidx := ((y + bounds.y) * md.width + x + bounds.x) * 4
+ if len(buf) <= iidx {
+ break y_loop
+ }
+
+ ipix := (^[4]byte)(&buf[iidx])
+
+ if ipix.a != 0 {
+ // Blend pixels)
+ p := ir.unaligned_load(ipix)
+ a := alpha(cel.opacity, layer.opacity)
+ pix = blend(p, pix, a, layer.blend_mode) or_return
+
+ } else {
+ // Merge Alpha & Opacities
+ pix.a = u8(alpha(i32(pix.a), alpha(cel.opacity, layer.opacity)))
+ }
+
+ ir.unaligned_store(ipix, pix)
+ }
+ }
+ }
+
+ return
+}
+
diff --git a/tools/compile_assets/aseprite/utils/sprite_sheet.odin b/tools/compile_assets/aseprite/utils/sprite_sheet.odin
new file mode 100644
index 0000000..ceb4b76
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/sprite_sheet.odin
@@ -0,0 +1,382 @@
+package aseprite_file_handler_utility
+
+import "base:runtime"
+import "core:slice"
+
+@require import "core:fmt"
+@require import "core:log"
+
+import ase ".."
+
+
+create_sprite_sheet :: proc {
+ create_sprite_sheet_from_doc,
+ create_sprite_sheet_from_info,
+}
+
+
+/*
+Creates internal allocation on `context.temp_allocator`, will attempt to clean up after itself.
+*/
+create_sprite_sheet_from_doc :: proc (
+ doc: ^ase.Document, s_info: Sprite_Info,
+ write_rules := DEFAULT_SPRITE_WRITE_RULES, alloc := context.allocator
+) -> (res: Sprite_Sheet, err: Errors) {
+
+ runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(context.allocator == context.temp_allocator)
+ info: Info
+ get_info(doc, &info, context.temp_allocator) or_return
+
+ return create_sprite_sheet_from_info(info, s_info, write_rules, alloc)
+}
+
+
+/*
+Creates internal allocation on `context.temp_allocator`, will attempt to clean up after itself.
+*/
+create_sprite_sheet_from_info :: proc (
+ info: Info, s_info: Sprite_Info,
+ write_rules := DEFAULT_SPRITE_WRITE_RULES, alloc := context.allocator
+) -> (res: Sprite_Sheet, err: Errors) {
+
+ switch {
+ case write_rules.align < min(Sprite_Alignment) || max(Sprite_Alignment) < write_rules.align:
+ err = .Invalid_Alignment
+ return
+
+ case s_info.size.x < write_rules.offset.x || s_info.size.y < write_rules.offset.y:
+ err = .Invalid_Offset
+ return
+
+ case s_info.spacing.x < 0 || s_info.spacing.y < 0:
+ err = .Invalid_Spacing
+ return
+
+ case s_info.boarder.x < 0 || s_info.boarder.y < 0:
+ err = .Invalid_Boarder
+ return
+
+ case s_info.count <= 0:
+ err = .Invalid_Count
+ return
+
+ case (s_info.size.x * s_info.size.y) < (info.md.width * info.md.height):
+ if !write_rules.ingore_sprite_size {
+ err = .Sprite_Size_to_Small
+ return
+ }
+
+ // If `shrink_to_pixels` is set the new bounds may fit just fine.
+ // However it's not reasonable to check them all, instead we just assume they will be.
+ if !write_rules.shrink_to_pixels {
+ fast_log(.Warning, "Sprite smaller than Frame. Ingoring & continuing.")
+ }
+ }
+
+ // Note(blob):
+ // Gets the clostest multiple of `s_info.count` that's `>=` to `len(info.frames)`.
+ // Allows for `len(info.frames)` to not be a multiple of `s_info.count`;
+ // and still make a valid grid.
+ frame_count := len(info.frames) + (s_info.count - ((len(info.frames) - 1) %% s_info.count + 1))
+
+ y_count := max( 1, frame_count / s_info.count )
+ width := ( s_info.count * s_info.size.x ) + ( (s_info.count - 1) * s_info.spacing.x )
+ height := ( y_count * s_info.size.y ) + ( (y_count - 1) * s_info.spacing.y )
+
+
+ img_width := width + (s_info.boarder.x * 2)
+ img_height := height + (s_info.boarder.y * 2)
+ img_size := img_width * img_height * 4
+
+ res.info = s_info
+ res.img = {
+ width = img_width,
+ height = img_height,
+ bpp = .RGBA,
+ data = make([]u8, img_size, alloc) or_return,
+ }
+
+ defer {
+ if err != nil {
+ delete(res.img.data, alloc)
+ }
+ }
+
+
+ if write_rules.use_index_bg_colour && info.md.bpp == .Indexed && !info.layers[0].is_background {
+ img_p := slice.reinterpret([]Pixel, res.img.data)
+ c := info.palette[info.md.trans_idx].color
+ c.a = 0
+ slice.fill(img_p, c)
+
+ } else {
+ fill_colour(res.img.data, write_rules.background_colour)
+ }
+
+ runtime.DEFAULT_TEMP_ALLOCATOR_TEMP_GUARD(context.allocator == context.temp_allocator)
+ tileset_alloc := context.temp_allocator
+
+ sprite_pos: [2]int
+ sw, sh := s_info.size.x, s_info.size.y
+
+ for frame in info.frames {
+ defer {
+ sprite_pos.x += s_info.size.x + s_info.spacing.x
+ if width <= sprite_pos.x {
+ sprite_pos.x = 0
+ sprite_pos.y += s_info.size.y + s_info.spacing.y
+ }
+
+ if height + s_info.spacing.y < sprite_pos.y {
+ panic("Sprite Y Pos is OOB. This shouldn't happen... send help.")
+ }
+ }
+
+ if len(frame.cels) == 0 {
+ continue
+ }
+
+ if !slice.is_sorted_by(frame.cels, cel_less) {
+ slice.sort_by(frame.cels, cel_less)
+ }
+
+ fw, fh := info.md.width, info.md.height
+ fp: [2]int = 0
+
+ if write_rules.shrink_to_pixels {
+
+ min_pos := [2]int{fw, fh}
+ max_pos: [2]int
+
+ for cel in frame.cels {
+ layer := info.layers[cel.layer]
+ if !layer.visiable || layer.is_background { continue }
+
+ size := [2]int{cel.width, cel.height}
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[layer.tileset]
+ size = {ts.width, ts.height}
+ }
+
+ min_pos = {
+ min(min_pos.x, cel.pos.x),
+ min(min_pos.y, cel.pos.y),
+ }
+ max_pos = {
+ max(max_pos.x, cel.pos.x + size.x),
+ max(max_pos.y, cel.pos.y + size.y),
+ }
+ }
+
+ frame_size := max_pos - min_pos
+
+ fw, fh = frame_size.x, frame_size.y
+ fp = min_pos
+ }
+
+ cel_offset := write_rules.offset + s_info.boarder + sprite_pos
+
+ alignment: [2]int
+ // Sprite Sheet Aligment https://www.desmos.com/geometry/miqzk9ijus
+ switch write_rules.align {
+ case .Top_Left: // Default Alignment
+ case .Top_Center: alignment.x = (sw - fw) / 2
+ case .Top_Right: alignment.x = (sw - fw)
+
+ case .Mid_Left: alignment.y = (sh - fh) / 2
+ case .Mid_Center: alignment = { (sw - fw) / 2, (sh - fh) / 2 }
+ case .Mid_Right: alignment = { (sw - fw), (sh - fh) / 2 }
+
+ case .Bot_Left: alignment.y = sh - fh
+ case .Bot_Center: alignment = { (sw - fw) / 2, sh - fh }
+ case .Bot_Right: alignment = { (sw - fw), sh - fh }
+
+ case:
+ err = .Invalid_Alignment
+ return
+ }
+
+
+ for cel in frame.cels {
+ layer := info.layers[cel.layer]
+ skip := (!layer.visiable) || (write_rules.ingore_bg_layers && layer.is_background)
+ if skip { continue }
+
+ s_cel := cel
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[layer.tileset]
+ s_cel = cel_from_tileset(cel, ts, info.md.bpp, tileset_alloc) or_return
+ }
+
+ if layer.is_background {
+ // NOTE(blob):
+ // This isn't truly right & only for really works for solid colours.
+ // `write_cel` would need to rewrite support both arbitrary reads & writes.
+ s_cel.pos += cel_offset
+ s_cel.width = s_info.size.x
+ s_cel.height = s_info.size.y
+ write_cel(res.img.data, s_cel, layer, res.img.md, info.palette) or_return
+ continue
+ }
+
+ s_cel.pos += alignment + cel_offset - fp
+ // Make sure we don't pass a negitive position.
+ s_cel.pos = { max(0, s_cel.pos.x), max(0, s_cel.pos.y) }
+
+ write_cel(res.img.data, s_cel, layer, res.img.md, info.palette) or_return
+ }
+ }
+
+ return
+}
+
+
+// Finds the smallest Sprite size need to fit all visable pixels.
+// Ingores Background Layers
+find_min_sprite_size :: proc(info: Info, make_square := true) -> (res: [2]int) {
+
+ for frame in info.frames {
+ for cel in frame.cels {
+ layer := info.layers[cel.layer]
+ if !layer.visiable || layer.is_background { continue }
+
+ size := [2]int{ cel.width, cel.height }
+ if cel.tilemap.tiles != nil {
+ ts := info.tilesets[layer.tileset]
+ size = {ts.width, ts.height}
+ }
+
+ res.x = max(res.x, size.x)
+ res.y = max(res.y, size.y)
+ }
+ }
+
+ if make_square {
+ res = max(res.x, res.y)
+ }
+
+ return
+}
+
+
+draw_sheet_grid :: proc(sheet: ^Sprite_Sheet, colour: [4]u8) {
+ draw_sheet_spacing(sheet, colour, true)
+}
+
+
+draw_sheet_spacing :: proc(sheet: ^Sprite_Sheet, colour: [4]u8, always_draw: bool) {
+ img := sheet.img
+ info := sheet.info
+ assert(img.bpp == .RGBA)
+
+ raw := slice.reinterpret([][4]u8, img.data)
+
+ row_count := (img.height - info.size.y - (info.boarder.y * 2)) / ( info.size.y + info.spacing.y )
+ if 0 < row_count {
+ row_block := img.width * info.size.y
+ row_space := img.width * info.spacing.y
+ row_step := row_block + row_space
+ row_fill := always_draw ? max(img.width, row_space) : row_space
+
+ row_offset := row_block + img.width * info.boarder.x
+ base := raw[row_offset:][:row_fill]
+
+ slice.fill(base, colour)
+
+ for row in 1..<row_count {
+ start := row_step * row + row_offset
+ copy(raw[start:], base)
+ }
+ }
+
+ col_count := info.count - 1
+ if 0 < col_count {
+ col_block := info.size.x + info.spacing.x
+ col_fill := always_draw ? max(1, info.spacing.y) : info.spacing.y
+ col_offset := info.size.x + info.boarder.x
+
+ base := raw[col_offset:][:col_fill]
+ slice.fill(base, colour)
+
+ for col in 1..<col_count {
+ pos := col_block * col + col_offset
+ copy(raw[pos:], base)
+ }
+
+ for y in 1..<img.height {
+ start := img.width * y + col_offset
+ for col in 0..<col_count {
+ pos := col_block * col + start
+ copy(raw[pos:], base)
+ }
+ }
+ }
+
+ return
+}
+
+
+draw_sheet_boarder :: proc(sheet: ^Sprite_Sheet, colour: [4]u8) {
+ img := sheet.img
+ info := sheet.info
+ assert(img.bpp == .RGBA)
+
+ raw := slice.reinterpret([][4]u8, img.data)
+
+ if 0 < info.boarder.y {
+ base := raw[:img.width * info.boarder.y]
+ slice.fill(base, colour)
+ copy(raw[(img.height - info.boarder.y) * img.width:], base)
+ }
+
+ if 0 < info.boarder.x {
+ base_start := img.width * info.boarder.y
+ base := raw[base_start:][:info.boarder.x]
+ count := img.height - (info.boarder.y * 2)
+
+ slice.fill(base, colour)
+ copy(raw[base_start + img.width - info.boarder.x:], base)
+
+ for pos in 0..<count {
+ start := base_start + (img.width * pos)
+ copy(raw[start:], base)
+ copy(raw[start + img.width - info.boarder.x:], base)
+ }
+ }
+
+ return
+}
+
+
+find_pixel_bounds :: proc(img: Image, bg_colour: [4]u8 = 0, check_trans := true) -> (bounds: Bounds) {
+ assert(img.md.bpp == .RGBA)
+ raw := slice.reinterpret([][4]u8, img.data)
+
+ min_pos, max_pos := find_pixel_bounds_min_max(raw, img.width, img.height, bg_colour, check_trans)
+
+ return { pos = min_pos, width = max_pos.x - min_pos.x, height = max_pos.y - min_pos.y }
+}
+
+
+find_pixel_bounds_min_max :: proc(img: [][4]u8, width, height: int, bg_colour: [4]u8, check_trans: bool) -> (min_pos, max_pos: [2]int) {
+
+ min_pos = { width, height }
+ max_pos = { 0, 0 }
+
+ for y in 0..<height {
+ for x in 0..<width {
+ pix := img[y * width + x]
+ if (pix.a == 0 && check_trans) || pix == bg_colour {
+ continue
+ }
+
+ min_pos = { min(x, min_pos.x), min(y, min_pos.y) }
+ max_pos = { max(x, max_pos.x), max(y, max_pos.y) }
+ }
+ }
+
+ max_pos += 1
+
+ return
+}
+
diff --git a/tools/compile_assets/aseprite/utils/types.odin b/tools/compile_assets/aseprite/utils/types.odin
new file mode 100644
index 0000000..bf2cd0c
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/types.odin
@@ -0,0 +1,306 @@
+package aseprite_file_handler_utility
+
+import "base:runtime"
+import "core:time"
+
+import ase ".."
+
+
+// Errors
+Palette_Error :: enum {
+ None,
+ Color_Index_Out_of_Bounds,
+}
+Blend_Error :: enum {
+ None,
+ Invalid_Mode,
+ Unequal_Image_Sizes,
+}
+Image_Error :: enum {
+ None,
+ Frame_Index_Out_Of_Bounds,
+ Indexed_BPP_No_Palette,
+ Invalid_BPP,
+ Cel_Out_Of_Bounds,
+ Cel_Size_Not_Of_BPP,
+ Buffer_To_Small,
+ Buffer_Size_Not_Match_Metadata,
+ Buffer_Not_RGBA,
+}
+Animation_Error :: enum {
+ None,
+ Tag_Not_Found,
+ Tag_Index_Out_Of_Bounds,
+}
+
+Tileset_Error :: enum {
+ None,
+ Tileset_Cel_Sizes_Mismatch,
+}
+
+Sprite_Sheet_Error :: enum {
+ None,
+ Sprite_Size_to_Small,
+ Invalid_Alignment,
+ Invalid_Offset,
+ Invalid_Count,
+ Invalid_Spacing,
+ Invalid_Boarder,
+}
+
+Errors :: union #shared_nil {
+ runtime.Allocator_Error,
+ Image_Error,
+ Animation_Error,
+ Tileset_Error,
+ Blend_Error,
+ Palette_Error,
+ Sprite_Sheet_Error,
+}
+
+// Raw Types
+B_Pixel :: [4]i32
+Pixel :: [4]byte
+Pixels :: []byte
+
+Vec2 :: [2]int
+
+// ASE Document types
+
+Precise_Bounds :: struct {
+ // Truely fixpoint but I anit dealing with that shit
+ x, y, width, height: f64,
+}
+
+Bounds :: struct {
+ using pos: Vec2,
+ width, height: int,
+}
+
+Cel :: struct {
+ using bounds: Bounds,
+ opacity: int,
+ link: int,
+ layer: int,
+ z_index: int, // https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md#note5
+ raw: Pixels `fmt:"-"`,
+ tilemap: Tilemap,
+ extra: Maybe(Precise_Bounds),
+}
+
+Tilemap :: struct {
+ width: int,
+ height: int,
+ x_flip: uint,
+ y_flip: uint,
+ diag_flip: uint,
+ tiles: []int,
+}
+
+Layer :: struct {
+ name: string,
+ opacity: int,
+ visiable: bool,
+ is_background: bool,
+ index: int,
+ blend_mode: Blend_Mode,
+ tileset: int,
+}
+
+Frame :: struct {
+ duration: i64, // in milliseconds
+ cels: []Cel,
+}
+
+Tag :: struct {
+ from: int,
+ to: int,
+ direction: ase.Tag_Loop_Dir,
+ name: string,
+}
+
+Palette :: []Color
+
+Color :: struct {
+ using color: Pixel,
+ name: string,
+}
+
+// Bits per pixel
+Pixel_Depth :: enum {
+ Indexed = 8,
+ Grayscale = 16,
+ RGBA = 32,
+}
+
+// Not needed RN. We'll only ever handle sRGB.
+Color_Space :: enum {
+ None,
+ sRGB,
+ ICC,
+}
+
+Metadata :: struct {
+ width: int,
+ height: int,
+ bpp: Pixel_Depth,
+ trans_idx: u8,
+}
+
+Slice_Key :: struct {
+ frame: int,
+ x, y: int,
+ w, h: int,
+ center: [4]int,
+ pivot: [2]int,
+}
+
+Slice :: struct {
+ flags: ase.Slice_Flags,
+ name: string,
+ keys: []Slice_Key,
+}
+
+Tileset :: struct {
+ id: int,
+ width: int,
+ height: int,
+ num: int,
+ name: string,
+ tiles: Pixels,
+}
+
+
+Info :: struct {
+ frames: []Frame,
+ layers: []Layer,
+ tags: []Tag,
+ tilesets: []Tileset,
+ slices: []Slice,
+ palette: Palette,
+
+ md: Metadata,
+ allocator: runtime.Allocator,
+}
+
+
+
+// Sprite Sheet
+Sprite_Info :: struct {
+ size: [2]int, // Size of a sprite.
+ spacing: [2]int, // Spacing between each sprite.
+ boarder: [2]int, // Boarder between sheet & image edge.
+ count: int, // Sprites per row.
+}
+
+// Govern's how Frames are writen a Sprite
+Sprite_Write_Rules :: struct {
+
+ // What point on the Frame & Sprite to align.
+ // Effective when Frame.size < Sprite.size.
+ align: Sprite_Alignment,
+
+ // Offset from the alignment point.
+ offset: [2]int,
+
+ // Resulting Sheet's Background Colour
+ background_colour: [4]u8,
+
+ // Shrinks Frame to the bounds of its visable pixels.
+ // NOTE(blob):
+ // Using this may slighly change the positioning.
+ // This is expected & intended behaviour.
+ shrink_to_pixels: bool,
+
+ // Whether to use the Background Colour
+ // of the ase file or use `background_colour`;
+ // Only used for `Indexed` colour mode.
+ use_index_bg_colour: bool,
+
+ // Whether to ignore Background layers.
+ ingore_bg_layers: bool,
+
+ // Whether to ingore the Sprite size being
+ // smaller then the Frame frame.
+ ingore_sprite_size: bool,
+}
+
+// Sprite Sheet Alignment https://www.desmos.com/geometry/miqzk9ijus
+Sprite_Alignment :: enum {
+ Top_Left, Top_Center, Top_Right,
+ Mid_Left, Mid_Center, Mid_Right,
+ Bot_Left, Bot_Center, Bot_Right,
+
+ // Default Alignment
+ Base = Top_Left,
+
+ // Some helpers... cause why not.
+ Top = Top_Center,
+ Middle = Mid_Center,
+ Bottom = Bot_Center,
+ Left = Mid_Left,
+ Right = Mid_Right,
+}
+
+
+// Precomputed Types. They own all their data.
+Image :: struct {
+ using md: Metadata,
+ data: Pixels `fmt:"-"`,
+}
+
+Animation :: struct {
+ using md: Metadata,
+ fps: int,
+ length: time.Duration,
+ frames: []Pixels,
+}
+
+Sprite_Sheet :: struct {
+ using img: Image,
+ info: Sprite_Info,
+}
+
+
+
+Blend_Mode :: enum {
+ Unspecified = -1,
+ Src = -2,
+ Merge = -3,
+ Neg_BW = -4,
+ Red_Tint = -5,
+ Blue_Tint = -6,
+ Dst_Over = -7,
+
+ Normal = 00,
+ Multiply = 01,
+ Screen = 02,
+ Overlay = 03,
+ Darken = 04,
+ Lighten = 05,
+ Color_Dodge = 06,
+ Color_Burn = 07,
+ Hard_Light = 08,
+ Soft_Light = 09,
+ Difference = 10,
+ Exclusion = 11,
+ Hue = 12,
+ Saturation = 13,
+ Color = 14,
+ Luminosity = 15,
+ Addition = 16,
+ Subtract = 17,
+ Divide = 18,
+}
+
+
+DEFAULT_SPRITE_WRITE_RULES :: Sprite_Write_Rules {
+ align = .Mid_Center,
+ offset = {0, 0},
+ background_colour = {0, 0, 0, 0},
+ shrink_to_pixels = false,
+ use_index_bg_colour = true,
+ ingore_bg_layers = false,
+ ingore_sprite_size = false,
+}
+
diff --git a/tools/compile_assets/aseprite/utils/utils_internal.odin b/tools/compile_assets/aseprite/utils/utils_internal.odin
new file mode 100644
index 0000000..6915fe5
--- /dev/null
+++ b/tools/compile_assets/aseprite/utils/utils_internal.odin
@@ -0,0 +1,110 @@
+package aseprite_file_handler_utility
+
+import ir "base:intrinsics"
+import "base:runtime"
+import "core:reflect"
+import "core:strconv"
+import "core:slice"
+
+@(require) import "core:fmt"
+@(require) import "core:log"
+
+_ :: reflect
+
+
+@(private)
+fast_log_str :: proc(lvl: log.Level, str: string, loc := #caller_location) {
+ logger := context.logger
+ if logger.procedure == nil { return }
+ if lvl < logger.lowest_level { return }
+ logger.procedure(logger.data, lvl, str, logger.options, loc)
+}
+
+@(private)
+fast_log_str_enum :: proc(lvl: log.Level, str: string, val: $T, sep := " ", loc := #caller_location) where ir.type_is_enum(T) {
+ logger := context.logger
+ if logger.procedure == nil { return }
+ if lvl < logger.lowest_level { return }
+
+ s := reflect.enum_string(val)
+ buf := make([]u8, len(str) + len(sep) + len(s))
+ defer delete(buf)
+
+ n := copy(buf[:], str)
+ n += copy(buf[n:], sep)
+ copy(buf[n:], s)
+
+ logger.procedure(logger.data, lvl, string(buf), logger.options, loc)
+}
+
+@(private)
+fast_log_str_num :: proc(lvl: log.Level, str: string, val: $T, sep := " ", loc := #caller_location) where ir.type_is_integer(T) {
+ logger := context.logger
+ if logger.procedure == nil { return }
+ if lvl < logger.lowest_level { return }
+
+ nb: [32]u8
+ s := strconv.append_int(nb[:], i64(val), 10)
+ buf := make([]u8, len(str) + len(sep) + len(s))
+ defer delete(buf)
+
+ n := copy(buf[:], str)
+ n += copy(buf[n:], sep)
+ copy(buf[n:], s)
+
+ logger.procedure(logger.data, lvl, string(buf), logger.options, loc)
+}
+
+@(private)
+fast_log :: proc {fast_log_str, fast_log_str_enum, fast_log_str_num}
+
+
+// Internal Debugging Tool.
+format_pixels :: proc(img: Image, x := 4, y := 4, alloc := context.allocator) -> (str: string, err: runtime.Allocator_Error) #optional_allocator_error {
+ ch := int(img.bpp) >> 3
+
+ size := len(img.data) * 3 /*pixel in bytes*/ \
+ + (len(img.data)/ch - 1) /*comas*/ \
+ + (len(img.data)*2/ch) /*<space>|*/ \
+ + (img.width*img.height*4) /*tile gap*/
+
+ sb := make([dynamic]byte, 0, size) or_return
+ buf: [3]byte
+
+ for n in 0..<len(img.data)/ch {
+ if n %% (img.width) == 0 {
+ append(&sb, '|') or_return
+ }
+
+ s := strconv.write_int(buf[:], i64(img.data[n*ch]), 10)
+ for _ in 0..<3-len(s) {
+ append(&sb, ' ') or_return
+ }
+ append(&sb, s) or_return
+
+ for i in 1..<ch {
+ append(&sb, ',')
+ s = strconv.write_int(buf[:], i64(img.data[n*ch+i]), 10)
+ for _ in 0..<3-len(s) {
+ append(&sb, ' ') or_return
+ }
+ append(&sb, s) or_return
+ }
+ append(&sb, '|') or_return
+
+ if (n+1) %% img.width == 0 {
+ append(&sb, '\n') or_return
+ }else if (n+1) %% x == 0 {
+ append(&sb, " |") or_return
+ }
+ if (n+1) %% (img.width * y) == 0 {
+ append(&sb, '\n') or_return
+ }
+ }
+
+ return string(sb[:]), nil
+}
+
+fill_colour :: proc(data: []u8, colour: [4]u8) {
+ slice.fill(slice.reinterpret([][4]u8, data), colour)
+}
diff --git a/tools/compile_assets/aseprite/write.odin b/tools/compile_assets/aseprite/write.odin
new file mode 100644
index 0000000..52568fc
--- /dev/null
+++ b/tools/compile_assets/aseprite/write.odin
@@ -0,0 +1,304 @@
+package aseprite_file_handler
+
+import "core:io"
+import "core:log"
+import "core:encoding/endian"
+
+
+write_bool :: proc(w: io.Writer, data: bool, size: ^int) -> (written: int, err: Write_Error) {
+ return write_byte(w, u8(data), size)
+}
+
+write_i8 :: proc(w: io.Writer, data: i8, size: ^int) -> (written: int, err: Write_Error) {
+ return write_byte(w, u8(data), size)
+}
+
+write_byte :: proc(w: io.Writer, data: BYTE, size: ^int) -> (written: int, err: Write_Error) {
+ return 1, io.write_byte(w, data, size)
+}
+
+write_word :: proc(w: io.Writer, data: WORD, size: ^int) -> (written: int, err: Write_Error) {
+ buf: [2]byte
+ if !endian.put_u16(buf[:], .Little, data) {
+ return written, .Unable_To_Encode_Data
+ }
+
+ written = io.write(w, buf[:], size) or_return
+ if written != 2 {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_short :: proc(w: io.Writer, data: SHORT, size: ^int) -> (written: int, err: Write_Error) {
+ buf: [2]byte
+ if !endian.put_i16(buf[:], .Little, data) {
+ return written, .Unable_To_Encode_Data
+ }
+
+ written = io.write(w, buf[:], size) or_return
+ if written != 2 {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_dword :: proc(w: io.Writer, data: DWORD, size: ^int) -> (written: int, err: Write_Error) {
+ buf: [4]byte
+ if !endian.put_u32(buf[:], .Little, data) {
+ return written, .Unable_To_Encode_Data
+ }
+
+ written = io.write(w, buf[:], size) or_return
+ if written != 4 {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_long :: proc(w: io.Writer, data: LONG, size: ^int) -> (written: int, err: Write_Error) {
+ buf: [4]byte
+ if !endian.put_i32(buf[:], .Little, data) {
+ return written, .Unable_To_Encode_Data
+ }
+
+ written = io.write(w, buf[:], size) or_return
+ if written != 4 {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_fixed :: proc(w: io.Writer, data: FIXED, size: ^int) -> (written: int, err: Write_Error) {
+ return write(w, data.i, size)
+}
+
+write_float :: proc(w: io.Writer, data: FLOAT, size: ^int) -> (written: int, err: Write_Error) {
+ buf: [4]byte
+ if !endian.put_f32(buf[:], .Little, data) {
+ return written, .Unable_To_Encode_Data
+ }
+
+ written = io.write(w, buf[:], size) or_return
+ if written != 4 {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_double :: proc(w: io.Writer, data: DOUBLE, size: ^int) -> (written: int, err: Write_Error) {
+ buf: [8]byte
+ if !endian.put_f64(buf[:], .Little, data) {
+ return written, .Unable_To_Encode_Data
+ }
+
+ written = io.write(w, buf[:], size) or_return
+ if written != 8 {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_qword :: proc(w: io.Writer, data: QWORD, size: ^int) -> (written: int, err: Write_Error) {
+ buf: [8]byte
+ if !endian.put_u64(buf[:], .Little, data) {
+ return written, .Unable_To_Encode_Data
+ }
+
+ written = io.write(w, buf[:], size) or_return
+ if written != 8 {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_long64 :: proc(w: io.Writer, data: LONG64, size: ^int) -> (written: int, err: Write_Error) {
+ buf: [8]byte
+ if !endian.put_i64(buf[:], .Little, data) {
+ return written, .Unable_To_Encode_Data
+ }
+
+ written = io.write(w, buf[:], size) or_return
+ if written != 8 {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_string :: proc(w: io.Writer, data: STRING, size: ^int) -> (written: int, err: Write_Error) {
+ written = write_word(w, WORD(len(data)), size) or_return
+ if written != 2 {
+ return written, .Wrong_Write_Size
+ }
+
+ str := transmute([]u8)data
+ written += write_bytes(w, str[:], size) or_return
+ if written != 2 + len(data) {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_point :: proc(w: io.Writer, data: POINT, size: ^int) -> (written: int, err: Write_Error) {
+ written = write_long(w, data.x, size) or_return
+ if written != 4 {
+ return written, .Wrong_Write_Size
+ }
+
+ written += write_long(w, data.y, size) or_return
+ if written != 8 {
+ return written, .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_size :: proc(w: io.Writer, data: SIZE, size: ^int) -> (written: int, err: Write_Error) {
+ written = write_long(w, data.w, size) or_return
+ if written != 4 {
+ return written, .Wrong_Write_Size
+ }
+
+ written += write_long(w, data.h, size) or_return
+ if written != 8 {
+ return written, .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_rect :: proc(w: io.Writer, data: RECT, size: ^int) -> (written: int, err: Write_Error) {
+ write_point(w, data.origin, size) or_return
+ write_size(w, data.size, size) or_return
+ return
+}
+
+write_uuid:: proc(w: io.Writer, data: UUID, size: ^int) -> (written: int, err: Write_Error) {
+ t := data
+ written = io.write(w, t[:], size) or_return
+ if written != 16 {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_pixel :: proc(w: io.Writer, data: PIXEL, size: ^int) -> (written: int, err: Write_Error) {
+ return write_byte(w, data, size)
+}
+
+write_pixels :: proc(w: io.Writer, data: []PIXEL, size: ^int) -> (written: int, err: Write_Error) {
+ return write_bytes(w, data[:], size)
+}
+
+write_tile :: proc(w: io.Writer, data: TILE, size: ^int) -> (written: int, err: Write_Error) {
+ switch v in data {
+ case BYTE: written = write_byte(w, v, size) or_return
+ case WORD: written = write_word(w, v, size) or_return
+ case DWORD: written = write_dword(w, v, size) or_return
+ }
+
+ return
+}
+
+write_tiles :: proc(w: io.Writer, data: []TILE, size: ^int) -> (written: int, err: Write_Error) {
+ if len(data) == 0 {
+ return 0, .Array_To_Small
+ }
+
+ for tile in data {
+ switch v in tile {
+ case BYTE: written += write_byte(w, v, size) or_return
+ case WORD: written += write_word(w, v, size) or_return
+ case DWORD: written += write_dword(w, v, size) or_return
+ }
+ }
+
+ return
+}
+
+write_bytes :: proc(w: io.Writer, data: []u8, size: ^int) -> (written: int, err: Write_Error) {
+ written, err = io.write(w, data[:], size)
+ if err != nil {
+ log.error("Failed to write bytes", data[:], string(data[:]), written, size^)
+ return
+ }
+
+ if written != len(data) {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_skip :: proc(w: io.Writer, to_skip: int, size: ^int) -> (written: int, err: Write_Error) {
+ for _ in 0..<to_skip {
+ io.write_byte(w, 0x0, size) or_return
+ written += 1
+ }
+
+ if written != to_skip {
+ err = .Wrong_Write_Size
+ }
+
+ return
+}
+
+write_ud_value :: proc(w: io.Writer, data: Property_Value, size: ^int) -> (err: Marshal_Error) {
+ switch v in data {
+ case nil:
+ case bool: write(w, v, size) or_return
+ case i8: write(w, v, size) or_return
+ case BYTE: write(w, v, size) or_return
+ case SHORT: write(w, v, size) or_return
+ case WORD: write(w, v, size) or_return
+ case LONG: write(w, v, size) or_return
+ case DWORD: write(w, v, size) or_return
+ case LONG64: write(w, v, size) or_return
+ case QWORD: write(w, v, size) or_return
+ case FIXED: write(w, v, size) or_return
+ case FLOAT: write(w, v, size) or_return
+ case DOUBLE: write(w, v, size) or_return
+ case STRING: write(w, v, size) or_return
+ case POINT: write(w, v, size) or_return
+ case SIZE: write(w, v, size) or_return
+ case RECT: write(w, v, size) or_return
+ case UUID: write(w, v, size) or_return
+
+ case UD_Vec:
+ write(w, DWORD(len(v)), size) or_return
+ write(w, WORD(0x0), size) or_return
+ for value in v {
+ type := get_property_type(value) or_return
+ write(w, type, size) or_return
+ write(w, value, size) or_return
+ }
+
+ case Properties:
+ write(w, DWORD(len(v)), size) or_return
+ for key, val in v {
+ write(w, key, size) or_return
+ type := get_property_type(val) or_return
+ write(w, type, size) or_return
+ write(w, val, size) or_return
+ }
+ }
+ return
+}
+
+write :: proc {
+ write_bool, write_i8, write_byte, write_word, write_short, write_dword,
+ write_long, write_fixed, write_float, write_double, write_qword,
+ write_long64, write_string, write_point, write_size, write_rect,
+ write_uuid, write_tile, write_bytes, write_skip, write_ud_value,
+}
+
diff --git a/tools/compile_assets/loaders.odin b/tools/compile_assets/loaders.odin
new file mode 100644
index 0000000..136ba07
--- /dev/null
+++ b/tools/compile_assets/loaders.odin
@@ -0,0 +1,199 @@
+package assets_gen
+
+import os "core:os/os2"
+import "core:fmt"
+import "core:strings"
+import "core:path/filepath"
+import "core:image"
+import "core:image/png"
+import "core:image/qoi"
+
+import ase "aseprite"
+import aseutil "aseprite/utils"
+
+load_map :: proc(path: string, _: ^os.File, _: ^os.File) {
+ line := fmt.aprintf("#load(\"%v\")", path)
+ maps[filepath.stem(path)] = line
+ paths_to_res_type[path] = "Map_Id"
+}
+
+load_tileset :: proc(path: string, _: ^os.File, _: ^os.File) {
+ line := fmt.aprintf("#load(\"%v\")", path)
+ tilesets[filepath.stem(path)] = line
+ paths_to_res_type[path] = "Tileset_Id"
+}
+
+load_qoi :: proc(path: string, qoi: ^os.File, output: ^os.File) {
+ line := fmt.aprintf("{{data = #load(%w)}}", path)
+
+ images[filepath.stem(path)] = line
+ paths_to_res_type[path] = "Image_Id"
+}
+
+load_png :: proc(path: string, png_file: ^os.File, output: ^os.File) {
+ // Convert all PNG files to QOI and store those
+
+ png_bytes, read_err := os.read_entire_file(png_file, context.allocator)
+ if read_err != nil {
+ die("Could not read file (%v)", read_err)
+ }
+ defer delete(png_bytes)
+
+ img, png_err := png.load_from_bytes(png_bytes)
+ if png_err != nil {
+ die("Could not parse file (%v)", png_err)
+ }
+ defer image.destroy(img)
+
+ info, fi_err := os.fstat(png_file, allocator = context.temp_allocator)
+ if fi_err != nil {
+ die("Could not load file info (%v)", fi_err)
+ }
+
+ compiled_path := strings.concatenate(
+ {COMPILED_DIR, filepath.stem(info.name), ".qoi"},
+ allocator = context.temp_allocator,
+ )
+ qoi_err := qoi.save_to_file(compiled_path, img)
+
+ if qoi_err != nil {
+ die("Could not convert PNG to QOI (%v)", qoi_err)
+ }
+
+ absolute_path, abs_ok := filepath.abs(
+ compiled_path,
+ allocator = context.temp_allocator,
+ )
+ if !abs_ok {
+ die("Could not find absolute path to a compiled file (%v)", compiled_path)
+ }
+
+ line := fmt.aprintf("{{data = #load(%w)}}", absolute_path)
+
+ images[filepath.stem(path)] = line
+ paths_to_res_type[path] = "Image_Id"
+}
+
+@(private="file")
+load_sprite_sheet :: proc(path: string, doc: ^ase.Document) {
+ sprite_sheet, ss_err := aseutil.create_sprite_sheet(doc, {
+ size = {int(doc.header.width), int(doc.header.height)},
+ count = int(doc.header.frames),
+ })
+ if ss_err != nil {
+ die(
+ "Could not create sprite sheet from aseprite file %v (%v)",
+ path,
+ ss_err,
+ )
+ }
+ defer aseutil.destroy(sprite_sheet)
+
+ fmt.println("loaded ss")
+
+ pixels := make(
+ []image.RGBA_Pixel,
+ sprite_sheet.width * sprite_sheet.height,
+ allocator = context.temp_allocator,
+ )
+
+ i := 0
+ for i < sprite_sheet.width * sprite_sheet.height {
+ channel := i * 4
+ pixels[i].r = sprite_sheet.data[channel + 0]
+ pixels[i].g = sprite_sheet.data[channel + 1]
+ pixels[i].b = sprite_sheet.data[channel + 2]
+ pixels[i].a = sprite_sheet.data[channel + 3]
+ i += 1
+ }
+
+ img, img_ok := image.pixels_to_image(
+ pixels,
+ sprite_sheet.width,
+ sprite_sheet.height,
+ )
+ if !img_ok {
+ die("Could not create sprite sheet image %v", path)
+ }
+
+ compiled_path := strings.concatenate(
+ {COMPILED_DIR, filepath.stem(path), "-sheet.qoi"},
+ allocator = context.temp_allocator,
+ )
+ qoi_err := qoi.save_to_file(compiled_path, &img)
+
+ if qoi_err != nil {
+ die("Could not save spritesheet %v (%v)", path, qoi_err)
+ }
+
+ absolute_path, abs_ok := filepath.abs(
+ compiled_path,
+ allocator = context.temp_allocator,
+ )
+ if !abs_ok {
+ die("Could not find absolute path to a compiled file (%v)", compiled_path)
+ }
+
+ line := fmt.aprintf("{{data = #load(%w)}}", absolute_path)
+ images[filepath.stem(path)] = line
+ paths_to_res_type[path] = "Image_Id"
+}
+
+@(private="file")
+load_animation :: proc(path: string, doc: ^ase.Document) {
+ tags: map[string]struct{
+ from: i32,
+ to: i32,
+ }
+ defer delete(tags)
+
+ for frame in doc.frames {
+ for chunk in frame.chunks {
+ bin_tags, is_tags := chunk.(ase.Tags_Chunk)
+ if !is_tags {
+ continue
+ }
+
+ for bin_tag in bin_tags {
+ tags[bin_tag.name] = {
+ from = i32(bin_tag.from_frame),
+ to = i32(bin_tag.to_frame),
+ }
+ }
+ }
+ }
+
+ frame_durations := make([]i32, doc.header.frames)
+ defer delete(frame_durations)
+
+ i := 0
+ for frame in doc.frames {
+ frame_durations[i] = i32(frame.header.duration)
+ i += 1
+ }
+
+ line := fmt.aprintf(
+ "{{frame_count = %w, frame_durations = %w, tags = %w}}",
+ len(frame_durations),
+ frame_durations[:],
+ tags,
+ )
+
+ animations[filepath.stem(path)] = line
+}
+
+load_ase :: proc(path: string, ase_file: ^os.File, output: ^os.File) {
+ doc: ase.Document
+ defer ase.destroy_doc(&doc)
+
+ unmarshal_err := ase.unmarshal(&doc, path, alloc = context.temp_allocator)
+ if unmarshal_err != nil {
+ die("Could not unmarshal aseprite file %v (%v)", path, unmarshal_err)
+ }
+
+ // Load sprite sheet
+ load_sprite_sheet(path, &doc)
+
+ // Load animation
+ load_animation(path, &doc)
+}
diff --git a/tools/compile_assets/main.odin b/tools/compile_assets/main.odin
new file mode 100644
index 0000000..60c8a8c
--- /dev/null
+++ b/tools/compile_assets/main.odin
@@ -0,0 +1,328 @@
+package assets_gen
+
+import os "core:os/os2"
+import "core:fmt"
+import "core:path/filepath"
+import "core:strings"
+
+COMPILED_DIR :: ".compiled-res/"
+HELP_STR :: `USAGE: %v [input dir] [output file].odin`
+
+Asset_Loader :: #type proc(string, ^os.File, ^os.File)
+
+file_content :=
+`#+feature dynamic-literals
+package demonchime
+
+// DO NOT EDIT
+//
+// This file is autogenerated by tools/compile_assets
+// All resource types are defined in 'src/resources.odin'.
+
+import rl "vendor:raylib"
+
+Image_Id :: enum {
+<image-enum>}
+
+Animation_Id :: enum {
+<anim-enum>}
+
+Map_Id :: enum {
+<map-enum>}
+
+Tileset_Id :: enum {
+<tileset-enum>}
+
+Resource_Id :: union {
+ Image_Id,
+ Animation_Id,
+ Map_Id,
+ Tileset_Id,
+}
+
+images: [Image_Id]Image_Resource
+animations: [Animation_Id]Animation_Resource
+maps: [Map_Id]Map_Resource
+tilesets: [Tileset_Id]Tileset_Resource
+
+path_to_id: map[string]Resource_Id
+
+load_resources :: proc() {
+ load_images()
+ load_anims()
+ load_maps()
+ load_tilesets()
+
+ // Allow conversion from paths to a resource id, since it's a better way to
+ // reference resources in other resources (JSON is a good example).
+<resource-paths>}
+
+@(private="file")
+load_images :: proc() {
+<image-load>}
+
+@(private="file")
+load_anims :: proc() {
+<anim-load>}
+
+@(private="file")
+load_maps :: proc() {
+<maps-load>}
+
+@(private="file")
+load_tilesets :: proc() {
+<tileset-load>}
+`
+
+images: map[string]string
+animations: map[string]string
+maps: map[string]string
+tilesets: map[string]string
+
+paths_to_res_type: map[string]string
+
+die :: proc(msg: string, args: ..any, exit_code := 1) {
+ fmt.eprintfln(msg, ..args)
+ os.exit(exit_code)
+}
+
+print_help :: proc(exit_code := 1) {
+ die(HELP_STR, os.args[0], exit_code = exit_code)
+}
+
+recursive_read_directory :: proc(
+ dir: string,
+ allocator := context.allocator,
+) -> ([]os.File_Info, os.Error) {
+ context.allocator = allocator
+
+ full_files: [dynamic]os.File_Info
+
+ files, read_dir_err := os.read_all_directory_by_path(
+ dir,
+ allocator = allocator,
+ )
+ if read_dir_err != nil {
+ return nil, read_dir_err
+ }
+ defer delete(files)
+
+ for file in files {
+ fmt.printfln("Found %-15v '%v'", file.type, file.fullpath)
+ #partial switch file.type {
+ case .Directory:
+ subdir_files, sub_err := recursive_read_directory(file.fullpath)
+ if sub_err != nil {
+ delete(full_files)
+ return nil, sub_err
+ }
+ defer delete(subdir_files)
+
+ append(&full_files, ..subdir_files)
+
+ os.file_info_delete(file, allocator)
+ case .Regular:
+ append(&full_files, file)
+ case:
+ delete(full_files)
+ die("Invalid file type %v", file.type)
+ }
+ }
+
+ return full_files[:], nil
+}
+
+create_enum :: proc(
+ content: string,
+ replace: string,
+ elements: map[string]string
+) -> string {
+ ids := ""
+
+ for element in elements {
+ ids = strings.concatenate({
+ ids,
+ " ",
+ strings.to_upper_snake_case(element, context.temp_allocator),
+ ",\n"
+ }, allocator = context.temp_allocator)
+ }
+
+ replaced, _ := strings.replace_all(
+ content,
+ replace,
+ ids,
+ allocator = context.temp_allocator,
+ )
+
+ return replaced
+}
+
+create_loads :: proc(
+ content: string,
+ map_name: string,
+ placeholder: string,
+ elements: map[string]string
+) -> string {
+ load := ""
+ for element in elements {
+ load = strings.concatenate({
+ load,
+ " ",
+ map_name,
+ "[.",
+ strings.to_upper_snake_case(element, context.temp_allocator),
+ "] = ",
+ elements[element],
+ "\n",
+ }, allocator = context.temp_allocator)
+ }
+ return set_placeholder(content, placeholder, load)
+}
+
+set_placeholder :: proc(
+ content: string,
+ placeholder: string,
+ with: string
+) -> string {
+ new_content, _ := strings.replace_all(
+ content,
+ placeholder,
+ with,
+ allocator = context.temp_allocator,
+ )
+ return new_content
+}
+
+main :: proc() {
+ if len(os.args) != 3 {
+ print_help()
+ }
+
+ input_dir := os.args[1]
+ output_file_path := os.args[2]
+
+ output_file, open_err := os.open(output_file_path, {.Write, .Create, .Trunc})
+ if open_err != nil {
+ die("Could not create output file '%v' (%v)", output_file_path, open_err)
+ }
+ defer os.close(output_file)
+
+ dir_info, stat_err := os.stat(input_dir, context.temp_allocator)
+ if stat_err != nil {
+ die("Error reading directory %v", stat_err)
+ }
+
+ if dir_info.type != .Directory {
+ die("Expected directory (Got %v)", dir_info.type)
+ }
+
+ files, dir_read_err := recursive_read_directory(input_dir)
+ if dir_read_err != nil {
+ die("Could not iterate directory %v", dir_read_err)
+ }
+ defer {
+ for file in files {
+ os.file_info_delete(file, context.allocator)
+ }
+ delete(files)
+ }
+
+ free_all(context.temp_allocator)
+
+ loaders: map[string]Asset_Loader
+ defer delete(loaders)
+
+ loaders["tmj"] = load_map
+ // loaders["world"] = load_json
+ loaders["tsj"] = load_tileset
+ loaders["qoi"] = load_qoi
+ loaders["png"] = load_png
+ loaders["ase"] = load_ase
+
+ os.make_directory_all(COMPILED_DIR)
+
+ fmt.println("Generating assets file...")
+ for file in files {
+ ext := filepath.ext(file.fullpath)[1:]
+ loader, has := &loaders[ext]
+ if !has {
+ fmt.printfln(
+ "%-25v Skipped\tNo loader for '%v'",
+ file.name,
+ ext,
+ )
+ continue
+ }
+
+ f, open_err := os.open(file.fullpath)
+ if open_err != nil {
+ fmt.printfln("%-25v Skipped\tCould not load file", file.name)
+ continue
+ }
+ defer os.close(f)
+
+ fmt.printfln("%-25v Loading...", file.name)
+ loader^(file.fullpath, f, output_file)
+ fmt.printfln("%-25v Loaded", file.name)
+
+ free_all(context.temp_allocator)
+ }
+
+ content := file_content
+
+ images_to_enum: map[string]string
+ defer delete(images_to_enum)
+
+ content = create_enum(content, "<image-enum>", images)
+ content = create_enum(content, "<anim-enum>", animations)
+ content = create_enum(content, "<map-enum>", maps)
+ content = create_enum(content, "<tileset-enum>", tilesets)
+
+ content = create_loads(content, "images", "<image-load>", images)
+ content = create_loads(content, "animations", "<anim-load>", animations)
+ content = create_loads(content, "maps", "<maps-load>", maps)
+ content = create_loads(content, "tilesets", "<tileset-load>", tilesets)
+
+ res_paths := ""
+ cwd, _ := os.get_working_directory(context.temp_allocator)
+
+ for file in files {
+ res_type, is_res := paths_to_res_type[file.fullpath]
+ if !is_res {
+ continue
+ }
+
+ res_name := strings.to_upper_snake_case(
+ filepath.stem(file.fullpath),
+ context.temp_allocator,
+ )
+ rel_path, _ := filepath.rel(cwd, file.fullpath, context.temp_allocator)
+ res_paths = strings.concatenate({
+ res_paths,
+ " path_to_id[\"",
+ rel_path,
+ "\"] = ",
+ res_type,
+ ".",
+ res_name,
+ "\n",
+ }, allocator = context.temp_allocator)
+ }
+ content = set_placeholder(content, "<resource-paths>", res_paths)
+
+ os.write_string(output_file, content)
+
+ for image in images {
+ delete(images[image])
+ }
+ for anim in animations {
+ delete(animations[anim])
+ }
+ for tmap in maps {
+ delete(maps[tmap])
+ }
+
+ free_all(context.temp_allocator)
+}
+