diff options
37 files changed, 6830 insertions, 229 deletions
diff --git a/.compiled-res/player-ase_frames.qoi b/.compiled-res/player-ase_frames.qoi Binary files differnew file mode 100644 index 0000000..1b2b8f0 --- /dev/null +++ b/.compiled-res/player-ase_frames.qoi diff --git a/.compiled-res/player-sheet.qoi b/.compiled-res/player-sheet.qoi Binary files differnew file mode 100644 index 0000000..4c91f6f --- /dev/null +++ b/.compiled-res/player-sheet.qoi diff --git a/.compiled-res/player.qoi b/.compiled-res/player.qoi Binary files differnew file mode 100644 index 0000000..1b2b8f0 --- /dev/null +++ b/.compiled-res/player.qoi diff --git a/res/tilesets.qoi b/.compiled-res/tilesets.qoi Binary files differindex ecc869f..ecc869f 100644 --- a/res/tilesets.qoi +++ b/.compiled-res/tilesets.qoi @@ -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 Binary files differdeleted file mode 100644 index 4f637ae..0000000 --- a/res/ase/player.ase +++ /dev/null diff --git a/res/img/player.ase b/res/img/player.ase Binary files differnew file mode 100644 index 0000000..47a7dfa --- /dev/null +++ b/res/img/player.ase diff --git a/res/tilesets.png b/res/img/tilesets.png Binary files differindex 4a431fe..4a431fe 100644 --- a/res/tilesets.png +++ b/res/img/tilesets.png 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 Binary files differdeleted file mode 100644 index 11ebb7b..0000000 --- a/res/player.qoi +++ /dev/null @@ -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) +} + |
