diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e4908a1..0000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "src/aseprite"] - path = src/aseprite - url = https://github.com/bersK/odin-aseprite.git -[submodule "src/dialog/libtinyfiledialogs"] - path = src/dialog/libtinyfiledialogs - url = https://github.com/native-toolkit/libtinyfiledialogs.git diff --git a/README.md b/README.md index 3a0d31f..5f53772 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,16 @@ Yet-Another-Atlas-Packer by Stefan Stefanov ## Description -Simple atlas packer using `stb_rect_pack` from the `stb` family of header libraries & `raylib` for rendering/ui. Here's a quick preview on [youtube](https://youtu.be/4_dKq7G57Lw) of the application. +Simple atlas packer for .aseprite files. Generates metadata and potentially embeds in an output source file of your choosing. + +Uses `stb_rect_pack` from the `stb` family of header libraries & `raylib` for rendering/UI. Here's a quick preview on [youtube](https://youtu.be/4_dKq7G57Lw) of the application. -The goal of the tool is to take in multiple aseprite files and pack them into a single atlas, outputting some metadata in the process in the form of -json and/or source files for direct use in odin (maybe more languages too). +The goal of the tool is to take in multiple aseprite files and pack them into a single atlas, outputting metadata in the process in the form of +JSON and/or source files for direct use in odin (or other languages through a customization file). I'm using a library for marshalling the aseprite files found [here](https://github.com/blob1807/odin-aseprite) on github. diff --git a/src/aseprite_odin_generator/aseprite_odin_generator.odin b/examples/aseprite_odin_generator.odin similarity index 51% rename from src/aseprite_odin_generator/aseprite_odin_generator.odin rename to examples/aseprite_odin_generator.odin index 563452d..d400cc7 100644 --- a/src/aseprite_odin_generator/aseprite_odin_generator.odin +++ b/examples/aseprite_odin_generator.odin @@ -1,42 +1,29 @@ -package generator +package cli -import ase "../aseprite" +import ase "../vendors/aseprite" import "core:encoding/json" import "core:fmt" -import "core:mem" import "core:os" -import fp "core:path/filepath" import "core:slice" import s "core:strings" -import "core:testing" import rl "vendor:raylib" import stbrp "vendor:stb/rect_pack" -import gen ".." -import utils "../utils" +import gen "../src/generator" ATLAS_SIZE :: 512 -IMPORT_PATH :: "./src/aseprite_odin_generator/big.aseprite" -EXPORT_PATH :: "./src/aseprite_odin_generator/atlas.png" +IMPORT_PATH :: "./example.aseprite" +EXPORT_PATH :: "./atlas.png" main :: proc() { - args := utils.parse_arguments(os.args[1:]) - fmt.println(args) - - if ok := utils.CLIFlagType.Help in args; ok { - fmt.println("Help called!") - utils.print_help() - return - } ase_file, ase_ok := os.read_entire_file(IMPORT_PATH) if !ase_ok { fmt.panicf("Couldn't load file!") } - cwd := os.get_current_directory() - target_dir := s.concatenate({cwd, "\\src\\aseprite_odin_generator\\"}) + target_dir := os.get_current_directory() atlas: rl.Image = rl.GenImageColor(ATLAS_SIZE, ATLAS_SIZE, rl.BLANK) atlas_entries: [dynamic]gen.AtlasEntry = make([dynamic]gen.AtlasEntry) @@ -45,13 +32,10 @@ main :: proc() { metadata := gen.pack_atlas_entries(atlas_entries[:], &atlas, 10, 10) json_bytes, jerr := json.marshal(metadata) - os.write_entire_file("src/aseprite_odin_generator/metadata.json", json_bytes) + os.write_entire_file("./metadata.json", json_bytes) sb := gen.metadata_source_code_generate(metadata[:], gen.odin_source_generator_metadata) odin_output_str := s.to_string(sb) - os.write_entire_file( - "src/aseprite_odin_generator/output.odino", - transmute([]byte)odin_output_str, - ) + os.write_entire_file("./output.odin", transmute([]byte)odin_output_str) rl.ExportImage(atlas, EXPORT_PATH) } diff --git a/src/aseprite_odin_generator/big.aseprite b/examples/sample.aseprite similarity index 100% rename from src/aseprite_odin_generator/big.aseprite rename to examples/sample.aseprite diff --git a/repo_assets/image.png b/resources/repo_assets/image.png similarity index 100% rename from repo_assets/image.png rename to resources/repo_assets/image.png diff --git a/styles/style_candy.rgs b/resources/styles/style_candy.rgs similarity index 100% rename from styles/style_candy.rgs rename to resources/styles/style_candy.rgs diff --git a/scripts/build_atlas.bat b/scripts/build_atlas.bat new file mode 100644 index 0000000..d650386 --- /dev/null +++ b/scripts/build_atlas.bat @@ -0,0 +1,2 @@ +@echo off +odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap-debug.exe -debug \ No newline at end of file diff --git a/scripts/build_atlas_release.bat b/scripts/build_atlas_release.bat new file mode 100644 index 0000000..9fb82e6 --- /dev/null +++ b/scripts/build_atlas_release.bat @@ -0,0 +1,2 @@ +@echo off +odin build src/frontend -define:RAYLIB_SHARED=true -out:build/yaap.exe -o:speed \ No newline at end of file diff --git a/scripts/build_cli.bat b/scripts/build_cli.bat new file mode 100644 index 0000000..a13a781 --- /dev/null +++ b/scripts/build_cli.bat @@ -0,0 +1,2 @@ +@echo off +odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli-debug.exe -debug \ No newline at end of file diff --git a/scripts/build_cli_release.bat b/scripts/build_cli_release.bat new file mode 100644 index 0000000..5bd9255 --- /dev/null +++ b/scripts/build_cli_release.bat @@ -0,0 +1,2 @@ +@echo off +odin build examples/aseprite_odin_generator.odin -file -define:RAYLIB_SHARED=true -out:build/yaap-cli.exe -o:speed \ No newline at end of file diff --git a/scripts/build_debug.bat b/scripts/build_debug.bat deleted file mode 100644 index d5ab9ff..0000000 --- a/scripts/build_debug.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_debug.exe -debug diff --git a/scripts/build_debug.sh b/scripts/build_debug.sh deleted file mode 100755 index 7df52c7..0000000 --- a/scripts/build_debug.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -odin build src/main_release -out:build/game_debug.bin -no-bounds-check -debug diff --git a/scripts/build_generator_debug.bat b/scripts/build_generator_debug.bat deleted file mode 100644 index 2340f93..0000000 --- a/scripts/build_generator_debug.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -odin build src/aseprite_odin_generator -define:RAYLIB_SHARED=true -out:build_generator/aseprite_odin_generator.exe -debug diff --git a/scripts/build_hot_reload.bat b/scripts/build_hot_reload.bat deleted file mode 100644 index 3890995..0000000 --- a/scripts/build_hot_reload.bat +++ /dev/null @@ -1,21 +0,0 @@ -@echo off - -rem Build game.dll -odin build src -show-timings -use-separate-modules -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game.dll -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug -IF %ERRORLEVEL% NEQ 0 exit /b 1 - -rem If game.exe already running: Then only compile game.dll and exit cleanly -QPROCESS "game.exe">NUL -IF %ERRORLEVEL% EQU 0 exit /b 1 - -rem build game.exe -odin build src/main_hot_reload -use-separate-modules -out:build/game.exe -strict-style -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -debug -IF %ERRORLEVEL% NEQ 0 exit /b 1 - -rem copy raylib.dll from odin folder to here -if not exist "raylib.dll" ( - echo "Please copy raylib.dll from /vendor/raylib/windows/raylib.dll to the same directory as game.exe" - exit /b 1 -) - -exit /b 0 diff --git a/scripts/build_hot_reload.sh b/scripts/build_hot_reload.sh deleted file mode 100755 index 996a14d..0000000 --- a/scripts/build_hot_reload.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash - -VET="-strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon" - -# NOTE: this is a recent addition to the Odin compiler, if you don't have this command -# you can change this to the path to the Odin folder that contains vendor, eg: "~/Odin". -ROOT=$(odin root) -if [ ! $? -eq 0 ]; then - echo "Your Odin compiler does not have the 'odin root' command, please update or hardcode it in the script." - exit 1 -fi - -set -eu - -# Figure out the mess that is dynamic libraries. -case $(uname) in -"Darwin") - case $(uname -m) in - "arm64") LIB_PATH="macos-arm64" ;; - *) LIB_PATH="macos" ;; - esac - - DLL_EXT=".dylib" - EXTRA_LINKER_FLAGS="-Wl,-rpath $ROOT/vendor/raylib/$LIB_PATH" - ;; -*) - DLL_EXT=".so" - EXTRA_LINKER_FLAGS="'-Wl,-rpath=\$ORIGIN/linux'" - - # Copy the linux libraries into the project automatically. - if [ ! -d "linux" ]; then - mkdir linux - cp -r $ROOT/vendor/raylib/linux/libraylib*.so* linux - fi - ;; -esac - -# Build the game. -odin build src -use-separate-modules -extra-linker-flags:"$EXTRA_LINKER_FLAGS" -show-timings -define:RAYLIB_SHARED=true -build-mode:dll -out:build/game_tmp$DLL_EXT -debug $VET - -# Need to use a temp file on Linux because it first writes an empty `game.so`, which the game will load before it is actually fully written. -mv ./build/game_tmp$DLL_EXT ./build/game$DLL_EXT - -# Do not build the game.bin if it is already running. -if ! pgrep game.bin > /dev/null; then - odin build src/main_hot_reload -use-separate-modules -out:build/game.bin $VET -debug -fi diff --git a/scripts/build_release.bat b/scripts/build_release.bat deleted file mode 100644 index 0d1ac5b..0000000 --- a/scripts/build_release.bat +++ /dev/null @@ -1,2 +0,0 @@ -@echo off -odin build src/main_release -define:RAYLIB_SHARED=true -out:build/game_release.exe -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon -subsystem:windows diff --git a/scripts/build_release.sh b/scripts/build_release.sh deleted file mode 100644 index 257abc0..0000000 --- a/scripts/build_release.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -odin build src/main_release -out:build/game_release.bin -no-bounds-check -o:speed -strict-style -vet-unused -vet-using-stmt -vet-using-param -vet-style -vet-semicolon diff --git a/scripts/setup.bat b/scripts/setup.bat new file mode 100644 index 0000000..abf078d --- /dev/null +++ b/scripts/setup.bat @@ -0,0 +1,14 @@ +@echo off + +mkdir build + +FOR /F "tokens=*" %%g IN ('odin root') do (SET ODIN_ROOT=%%g) + +echo %ODIN_ROOT% + +@REM If it fails to find your odin root folder, copy the raylib.dll manually into the build folder, it's a runtime requirement +copy %ODIN_ROOT%\vendor\raylib\windows\raylib.dll build\raylib.dll + +pushd vendors\dialog +call .\build.bat +popd \ No newline at end of file diff --git a/src/aseprite_odin_generator/atlas.png b/src/aseprite_odin_generator/atlas.png deleted file mode 100644 index 94facee..0000000 Binary files a/src/aseprite_odin_generator/atlas.png and /dev/null differ diff --git a/src/aseprite_odin_generator/metadata.json b/src/aseprite_odin_generator/metadata.json deleted file mode 100644 index be61ff1..0000000 --- a/src/aseprite_odin_generator/metadata.json +++ /dev/null @@ -1 +0,0 @@ -[{"name":"Edinica","location":[95,10],"size":[58,57]},{"name":"Dvoika_0","location":[234,10],"size":[55,31]},{"name":"Dvoika_1","location":[163,10],"size":[61,33]},{"name":"Troika","location":[10,10],"size":[75,75]}] \ No newline at end of file diff --git a/src/globals.odin b/src/frontend/globals.odin similarity index 87% rename from src/globals.odin rename to src/frontend/globals.odin index 7808893..c9aae32 100644 --- a/src/globals.odin +++ b/src/frontend/globals.odin @@ -1,13 +1,12 @@ +package frontend -package game - +import generator "../generator" import rl "vendor:raylib" -PixelWindowHeight :: 180 +PIXEL_WINDOW_HEIGHT :: 180 FILE_DIALOG_SIZE :: 1000 scaling: f32 = 2 -w, h: f32 WindowInformation :: struct { w: f32, @@ -38,7 +37,7 @@ PackerSettings :: struct { output_odin: bool, } -GameMemory :: struct { +ApplicationState :: struct { file_dialog_text_buffer: [FILE_DIALOG_SIZE + 1]u8, is_packing_whole_source_folder: bool, should_open_file_dialog: bool, @@ -59,5 +58,7 @@ GameMemory :: struct { should_render_atlas: bool, atlas_render_has_preview: bool, atlas_render_size: i32, - atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata), + atlas_metadata: Maybe([dynamic]generator.SpriteAtlasMetadata), } + +g_mem: ApplicationState diff --git a/src/game.odin b/src/frontend/main.odin similarity index 51% rename from src/game.odin rename to src/frontend/main.odin index b289b82..5962de0 100644 --- a/src/game.odin +++ b/src/frontend/main.odin @@ -1,35 +1,65 @@ -// This file is compiled as part of the `odin.dll` file. It contains the -// procs that `game.exe` will call, such as: -// -// game_init: Sets up the game state -// game_update: Run once per frame -// game_shutdown: Shuts down game and frees memory -// game_memory: Run just before a hot reload, so game.exe has a pointer to the -// game's memory. -// game_hot_reloaded: Run after a hot reload so that the `g_mem` global variable -// can be set to whatever pointer it was in the old DLL. - -package game +package frontend import "core:fmt" +import "core:log" import "core:math" +import "core:mem" +import "core:mem/virtual" +import "core:os" import "core:strings" -import "utils" import rl "vendor:raylib" -import diag "dialog" +import diag "../../vendors/dialog" +import generator "../generator" -g_mem: ^GameMemory +main :: proc() { + default_allocator := context.allocator + tracking_allocator: mem.Tracking_Allocator + mem.tracking_allocator_init(&tracking_allocator, default_allocator) + context.allocator = mem.tracking_allocator(&tracking_allocator) -game_camera :: proc() -> rl.Camera2D { - w = f32(rl.GetScreenWidth()) - h = f32(rl.GetScreenHeight()) + mode: int = 0 + when ODIN_OS == .Linux || ODIN_OS == .Darwin { + mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH + } - return {zoom = h / PixelWindowHeight, target = {}, offset = {w / 2, h / 2}} -} + logh, logh_err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode) -ui_camera :: proc() -> rl.Camera2D { - return {zoom = scaling} + if logh_err == os.ERROR_NONE { + os.stdout = logh + os.stderr = logh + } + + logger := + logh_err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger() + context.logger = logger + defer if logh_err == os.ERROR_NONE { + log.destroy_file_logger(logger) + } + + rl.SetConfigFlags({.WINDOW_RESIZABLE}) + rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer") + defer rl.CloseWindow() + rl.SetWindowMinSize(1400, 800) + + for !rl.WindowShouldClose() { + update() + draw() + + for b in tracking_allocator.bad_free_array { + log.error("Bad free at: %v", b.location) + } + + clear(&tracking_allocator.bad_free_array) + + free_all(context.temp_allocator) + } + + for key, value in tracking_allocator.allocation_map { + log.error("%v: Leaked %v bytes\n", value.location, value.size) + } + + mem.tracking_allocator_destroy(&tracking_allocator) } update :: proc() { @@ -39,11 +69,10 @@ update :: proc() { win_info.h = f32(rl.GetScreenHeight()) win_info.height_scaled = win_info.h / scaling win_info.width_scaled = win_info.w / scaling - w = win_info.w - h = win_info.h // Update the virtual mouse position (needed for GUI interaction to work properly for instance) - rl.SetMouseScale(1 / scaling, 1 / scaling) + mouse_scale := 1 / scaling + rl.SetMouseScale(mouse_scale, mouse_scale) if g_mem.should_open_file_dialog { open_file_dialog() @@ -65,6 +94,10 @@ draw :: proc() { free_all(context.temp_allocator) } +ui_camera :: proc() -> rl.Camera2D { + return {zoom = scaling} +} + draw_screen_ui :: proc() { rl.BeginMode2D(ui_camera()) defer rl.EndMode2D() @@ -72,19 +105,47 @@ draw_screen_ui :: proc() { draw_atlas_settings_and_preview() } +pick_sources :: proc() { + g_mem.should_open_file_dialog = true + g_mem.source_location_type = .SourceFiles +} + +pick_output :: proc() { + g_mem.should_open_file_dialog = true + g_mem.source_location_type = .OutputFolder +} + +pack_atlas :: proc() { + g_mem.should_render_atlas = true +} + +save :: proc() { + generator.save_output( + g_mem.output_folder_path, + g_mem.atlas_metadata, + g_mem.atlas_render_texture_target, + ) +} + +save_to :: proc() { + // if output_folder, ok := g_mem.output_folder_path.(string); ok { + // generator.save_metadata_simple(output_folder, g_mem.atlas_metadata, nil, nil, nil) + // } +} + draw_screen_target :: proc() { atlas_render_target := &g_mem.atlas_render_texture_target rl.BeginTextureMode(atlas_render_target^) defer rl.EndTextureMode() - atlas_entries: [dynamic]AtlasEntry + atlas_entries: [dynamic]generator.AtlasEntry delete(atlas_entries) if files, ok := g_mem.source_files_to_pack.([]string); ok { - unmarshall_aseprite_files(files, &atlas_entries) + generator.unmarshall_aseprite_files(files, &atlas_entries) } else { - fmt.println("No source folder or files set! Can't pack the void!!!") + log.error("No source folder or files set! Can't pack the void!!!") g_mem.should_render_atlas = false return } @@ -97,12 +158,17 @@ draw_screen_target :: proc() { padding_y := g_mem.packer_settings.pixel_padding_y_int if g_mem.packer_settings.padding_enabled else 0 - g_mem.atlas_metadata = pack_atlas_entries(atlas_entries[:], &atlas, padding_x, padding_y) + g_mem.atlas_metadata = generator.pack_atlas_entries( + atlas_entries[:], + &atlas, + padding_x, + padding_y, + ) // OpenGL's Y buffer is flipped rl.ImageFlipVertical(&atlas) // rl.UnloadTexture(atlas_render_target.texture) - fmt.println("Packed everything!") + log.info("Packed everything!") atlas_render_target.texture = rl.LoadTextureFromImage(atlas) g_mem.should_render_atlas = false @@ -113,14 +179,14 @@ draw_atlas_settings_and_preview :: proc() { left_half_rect := rl.Rectangle { x = 0, y = 0, - width = auto_cast g_mem.window_info.width_scaled / 3, - height = auto_cast g_mem.window_info.height_scaled, + width = cast(f32)g_mem.window_info.width_scaled / 3, + height = cast(f32)g_mem.window_info.height_scaled, } right_half_rect := rl.Rectangle { - x = auto_cast g_mem.window_info.width_scaled / 3, + x = cast(f32)g_mem.window_info.width_scaled / 3, y = 0, - width = auto_cast (g_mem.window_info.width_scaled / 3) * 2, - height = auto_cast g_mem.window_info.height_scaled, + width = cast(f32)(g_mem.window_info.width_scaled / 3) * 2, + height = cast(f32)g_mem.window_info.height_scaled, } rl.DrawRectangleRec(left_half_rect, rl.WHITE) rl.DrawRectangleRec(right_half_rect, rl.MAROON) @@ -149,9 +215,9 @@ draw_atlas_settings_and_preview :: proc() { elements_height += small_offset / 2 @(static) - DropdownBox000EditMode: bool + dropdown_resolution_edit_mode: bool @(static) - DropdownBox000Active: i32 + dropdown_resolution_mode: i32 dropdown_rect := rl.Rectangle { x = small_offset, @@ -159,20 +225,18 @@ draw_atlas_settings_and_preview :: proc() { width = left_half_rect.width - small_offset * 2, height = small_offset, } - // Because we want to render this ontop of everything else, we can just 'defer' it at the end of the draw function defer { - if DropdownBox000EditMode {rl.GuiLock()} + if dropdown_resolution_edit_mode {rl.GuiLock()} if rl.GuiDropdownBox( dropdown_rect, "256x;512x;1024x;2048x;4096x", - &DropdownBox000Active, - DropdownBox000EditMode, + &dropdown_resolution_mode, + dropdown_resolution_edit_mode, ) { - DropdownBox000EditMode = !DropdownBox000EditMode - fmt.println(DropdownBox000Active) - g_mem.atlas_render_size = 256 * auto_cast math.pow(2, f32(DropdownBox000Active)) + dropdown_resolution_edit_mode = !dropdown_resolution_edit_mode + g_mem.atlas_render_size = 256 * auto_cast math.pow(2, f32(dropdown_resolution_mode)) } rl.GuiUnlock() } @@ -183,77 +247,90 @@ draw_atlas_settings_and_preview :: proc() { if SettingsDropdownBoxActive == 0 { padding_settings_y := elements_height { - defer rl.GuiGroupBox( - { - x = small_offset / 2, - y = padding_settings_y, - width = left_half_rect.width - small_offset, + defer { + padding_settings_rect := rl.Rectangle { + x = small_offset / 2, + y = padding_settings_y, + width = left_half_rect.width - small_offset, height = elements_height - padding_settings_y, - }, - "Padding Settings", - ) + } + rl.GuiGroupBox(padding_settings_rect, "Padding Settings") + } elements_height += small_offset + enable_padding_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = small_offset, + height = small_offset, + } rl.GuiCheckBox( - { - x = small_offset, - y = elements_height, - width = small_offset, - height = small_offset, - }, + enable_padding_rect, " Enable padding", &g_mem.packer_settings.padding_enabled, ) elements_height += small_offset * 2 - if (rl.GuiSpinner( - { - x = small_offset, - y = elements_height, - width = big_offset * 2, - height = small_offset, - }, - "", - &g_mem.packer_settings.pixel_padding_x_int, - 0, - 10, - spinner_edit_mode, - )) > - 0 {spinner_edit_mode = !spinner_edit_mode} - rl.GuiLabel( - { - x = (small_offset * 2) + big_offset * 2, - y = elements_height, - width = big_offset, + // Padding X spinner and label + { + padding_x_spinner_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = big_offset * 2, height = small_offset, - }, - "Padding X", - ) + } + padding_x_spinner := rl.GuiSpinner( + padding_x_spinner_rect, + "", + &g_mem.packer_settings.pixel_padding_x_int, + 0, + 10, + spinner_edit_mode, + ) + if (padding_x_spinner) > 0 { + spinner_edit_mode = !spinner_edit_mode + } + rl.GuiLabel( + { + x = (small_offset * 2) + big_offset * 2, + y = elements_height, + width = big_offset, + height = small_offset, + }, + "Padding X", + ) + } elements_height += small_offset * 2 - if (rl.GuiSpinner( - { - x = small_offset, - y = elements_height, - width = big_offset * 2, - height = small_offset, - }, - "", - &g_mem.packer_settings.pixel_padding_y_int, - 0, - 10, - spinner_edit_mode, - )) > - 0 {spinner_edit_mode = !spinner_edit_mode} - rl.GuiLabel( - { - x = (small_offset * 2) + big_offset * 2, - y = elements_height, - width = big_offset, + // Padding Y spinner and label + { + padding_y_spinner_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = big_offset * 2, height = small_offset, - }, - "Padding Y", - ) + } + padding_y_spinner := rl.GuiSpinner( + padding_y_spinner_rect, + "", + &g_mem.packer_settings.pixel_padding_y_int, + 0, + 10, + spinner_edit_mode, + ) + if (padding_y_spinner) > 0 { + spinner_edit_mode = !spinner_edit_mode + } + rl.GuiLabel( + { + x = (small_offset * 2) + big_offset * 2, + y = elements_height, + width = big_offset, + height = small_offset, + }, + "Padding Y", + ) + } elements_height += small_offset * 2 } @@ -264,94 +341,78 @@ draw_atlas_settings_and_preview :: proc() { { actions_label_y := elements_height - defer rl.GuiGroupBox( - { - x = small_offset / 2, - y = actions_label_y, - width = left_half_rect.width - small_offset, + + defer { + actions_rect := rl.Rectangle { + x = small_offset / 2, + y = actions_label_y, + width = left_half_rect.width - small_offset, height = elements_height - actions_label_y, - }, - "Actions", - ) + } + rl.GuiGroupBox(actions_rect, "Actions") + } elements_height += small_offset - if rl.GuiButton( - { - x = small_offset, - y = elements_height, - width = left_half_rect.width / 2 - small_offset, - height = small_offset, - }, - "Pick Source(s)", - ) { - g_mem.should_open_file_dialog = true - g_mem.source_location_type = .SourceFiles + pick_sources_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(pick_sources_rect, "Pick Source(s)") { + pick_sources() } - if rl.GuiButton( - { - x = left_half_rect.width / 2, - y = elements_height, - width = left_half_rect.width / 2 - small_offset, - height = small_offset, - }, - "Pick Output", - ) { - g_mem.should_open_file_dialog = true - g_mem.source_location_type = .OutputFolder + pick_output_rect := rl.Rectangle { + x = left_half_rect.width / 2, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(pick_output_rect, "Pick Output") { + pick_output() } elements_height += small_offset * 2 - - if rl.GuiButton( - { - x = small_offset, - y = elements_height, - width = left_half_rect.width / 2 - small_offset, - height = small_offset, - }, - "Pack Atlas", - ) { - g_mem.should_render_atlas = true + pack_atlas_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(pack_atlas_rect, "Pack Atlas") { + pack_atlas() } - if rl.GuiButton( - { - x = left_half_rect.width / 2, - y = elements_height, - width = left_half_rect.width / 2 - small_offset, - height = small_offset, - }, - "Clear Atlas", - ) { + clear_atlas_rect := rl.Rectangle { + x = left_half_rect.width / 2, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(clear_atlas_rect, "Clear Atlas") { clear_atlas_data() } elements_height += small_offset * 2 - if rl.GuiButton( - { - x = small_offset, - y = elements_height, - width = left_half_rect.width / 2 - small_offset, - height = small_offset, - }, - "Save", - ) { - save_output() + save_rect := rl.Rectangle { + x = small_offset, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(save_rect, "Save") { + save() } - if rl.GuiButton( - { - x = left_half_rect.width / 2, - y = elements_height, - width = left_half_rect.width / 2 - small_offset, - height = small_offset, - }, - "Save To...", - ) { - if output_folder, ok := g_mem.output_folder_path.(string); ok { - save_metadata_simple(output_folder) - } + save_to_rect := rl.Rectangle { + x = left_half_rect.width / 2, + y = elements_height, + width = left_half_rect.width / 2 - small_offset, + height = small_offset, + } + if rl.GuiButton(save_to_rect, "Save To...") { + save_to() } elements_height += small_offset * 2 } @@ -437,9 +498,9 @@ open_file_dialog :: proc() { source_files_to_pack := strings.clone_from_cstring(file_paths_conc, context.allocator) g_mem.source_files_to_pack = strings.split(source_files_to_pack, "|") - fmt.println(g_mem.source_files_to_pack) + log.info(g_mem.source_files_to_pack) } else { - fmt.println("No files were selected!") + log.error("No files were selected!") } case .SourceFolder: @@ -451,9 +512,9 @@ open_file_dialog :: proc() { ) if len(file) > 0 { g_mem.source_location_to_pack = strings.clone_from_cstring(file) - fmt.println(g_mem.source_location_to_pack) + log.info(g_mem.source_location_to_pack) } else { - fmt.println("Got an empty path from the file dialog!") + log.error("Got an empty path from the file dialog!") } case .OutputFolder: @@ -465,9 +526,9 @@ open_file_dialog :: proc() { ) if len(file) > 0 { g_mem.output_folder_path = strings.clone_from_cstring(file) - fmt.println(g_mem.output_folder_path) + log.info(g_mem.output_folder_path) } else { - fmt.println("Got an empty path from the file dialog!") + log.error("Got an empty path from the file dialog!") } case .SaveFileAs: @@ -475,7 +536,7 @@ open_file_dialog :: proc() { patterns: []cstring = {"*.png"} if default_path, ok := g_mem.output_folder_path.(string); ok { default_path_filename := strings.concatenate( - {default_path, os_file_separator, "atlas.png"}, + {default_path, generator.OS_FILE_SEPARATOR, "atlas.png"}, ) default_path_to_save: cstring = strings.clone_to_cstring(default_path_filename) file_path = cstring( @@ -491,7 +552,11 @@ open_file_dialog :: proc() { file_path = cstring(diag.save_file_dialog("Save as...", "", 1, &patterns[0], "Atlas")) } if file_path != nil { - save_output() + generator.save_output( + g_mem.output_folder_path, + g_mem.atlas_metadata, + g_mem.atlas_render_texture_target, + ) } } @@ -500,9 +565,8 @@ open_file_dialog :: proc() { } clear_atlas_data :: proc() { - if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { + if metadata, ok := g_mem.atlas_metadata.([dynamic]generator.SpriteAtlasMetadata); ok { delete(metadata) - // g_mem.atlas_metadata = nil } g_mem.atlas_render_has_preview = false } diff --git a/src/utils/cli.odin b/src/generator/cli.odin similarity index 66% rename from src/utils/cli.odin rename to src/generator/cli.odin index 8b9411f..5a48ebe 100644 --- a/src/utils/cli.odin +++ b/src/generator/cli.odin @@ -1,8 +1,11 @@ -package utils +package generator import "core:fmt" import s "core:strings" +// todo(stefan): Simplify this whole flags business, +// this can be implemented in a simpler fashion + CLIFlagType :: enum { Unknown, InputFiles, @@ -18,7 +21,7 @@ CLIFlagType :: enum { CLI_FLAG_STRINGS := [CLIFlagType][]string { .Unknown = {""}, .Help = {"h", "help"}, - .InputFiles = {"f", "input-files"}, + .InputFiles = {"i", "input-files"}, .InputFolder = {"d", "input-directory"}, .OutputFolder = {"o", "out"}, .EnableMetadataOutput = {"m", "export-metadata"}, @@ -30,13 +33,13 @@ CLI_FLAG_STRINGS := [CLIFlagType][]string { CLI_FLAG_DESCRIPTIONS := [CLIFlagType]string { .Unknown = "Invalid flag", .Help = "Prints the help message... hello!", - .InputFiles = "(real) path the source files for the packer (realpaths only), for multiple files you can provide one string of concateneted paths, separated by a ';'", - .InputFolder = "(real) path to a folder full of source files. This is an alternative to the -i[,input-files] flag", - .OutputFolder = "(real) path to the output folder for all the resulting files to be saved to.", + .InputFiles = "Full path to the source files for the packer, for multiple files you can provide one string of concateneted paths, separated by a ';'", + .InputFolder = "Full path to a folder full of source files. This is an alternative to the -i[,input-files] flag", + .OutputFolder = "Full path to the output folder for all the resulting files to be saved to.", .EnableMetadataOutput = "Whether or not to export metadata (JSON or source files with the offsets for the packer sprites in the atlas)", - .ConfigPath = "(real) path to a config file (json) that contains string definitions for exporting custom source files. More on this in the docs.", - .MetadataJSONOutputPath = "(real) path for the resulting JSON that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag", - .SourceCodeOutputPathOutputPath = "(real) path for the resulting source code file that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag", + .ConfigPath = "Full path to a config file (json) that contains string definitions for exporting custom source files. More on this in the docs.", + .MetadataJSONOutputPath = "Full path for the resulting JSON that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag", + .SourceCodeOutputPathOutputPath = "Full path for the resulting source code file that will be generated for the atlas. It overrides the name & location in regards to the -o[,output-folder] flag", } CLIOutputSettings :: struct { @@ -83,12 +86,9 @@ print_help :: proc() { for flag in CLIFlagType { if flag == .Unknown do continue - fmt.printfln( - "Flag: -%v,%v \t -- %v", - CLI_FLAG_STRINGS[flag][0], - CLI_FLAG_STRINGS[flag][1], - CLI_FLAG_DESCRIPTIONS[flag], - ) + flag_info := CLI_FLAG_STRINGS[flag] + flag_desc := CLI_FLAG_DESCRIPTIONS[flag] + fmt.printfln("Flag: -%v,%v \t -- %v", flag_info[0], flag_info[1], flag_desc) } } diff --git a/src/generator.odin b/src/generator/generator.odin similarity index 82% rename from src/generator.odin rename to src/generator/generator.odin index 571cca2..5e78041 100644 --- a/src/generator.odin +++ b/src/generator/generator.odin @@ -1,8 +1,9 @@ -package game +package generator -import ase "./aseprite" +import ase "../../vendors/aseprite" import "core:encoding/json" import "core:fmt" +import "core:log" import "core:os" import fp "core:path/filepath" import "core:slice" @@ -10,15 +11,12 @@ import "core:strings" import rl "vendor:raylib" import stbrp "vendor:stb/rect_pack" -import utils "./utils" - when ODIN_OS == .Windows { - os_file_separator :: "\\" + OS_FILE_SEPARATOR :: "\\" } else { - os_file_separator :: "/" + OS_FILE_SEPARATOR :: "/" } - CellData :: struct { layer_index: u16, opacity: u8, @@ -40,6 +38,35 @@ SpriteAtlasMetadata :: struct { size: [2]i32, } +SourceCodeGeneratorMetadata :: struct { + file_defines: struct { + top: string, + bottom: string, + file_name: string, + file_extension: string, + }, + lanugage_settings: struct { + first_class_enum_arrays: bool, // for languages that support creating arrays that contain for each enum value an entry in the enum_data.entry_line: .EnumCase = {array entry} + }, + custom_data_type: struct { + name: string, + type_declaration: string, // contains one param: custom_data_type.name + the rest of the type declaration like braces of the syntax & the type members + }, + enum_data: struct { + name: string, + begin_line: string, // contains one params: enum_data.name + entry_line: string, + end_line: string, + }, + array_data: struct { + name: string, + type: string, + begin_line: string, // array begin line contains 2 params in the listed order: array.name, array.type + entry_line: string, // array entry contains 5 params in the listed order: cell.name, cell.location.x, cell.location.y, cell.size.x, cell.size.y, + end_line: string, + }, +} + unmarshall_aseprite_dir :: proc( path: string, atlas_entries: ^[dynamic]AtlasEntry, @@ -53,7 +80,7 @@ unmarshall_aseprite_dir :: proc( unmarshall_aseprite_files_file_info(fis, atlas_entries, alloc) } } else { - fmt.println("Couldn't open folder: ", path) + log.errorf("Couldn't open folder: ", path) } } @@ -87,7 +114,7 @@ unmarshall_aseprite_files :: proc( extension := fp.ext(file) if extension != ".aseprite" do continue - fmt.println("Unmarshalling file: ", file) + log.infof("Unmarshalling file: ", file) ase.unmarshal_from_filename(file, &aseprite_document, alloc) atlas_entry := atlas_entry_from_compressed_cells(aseprite_document) atlas_entry.path = file @@ -97,11 +124,11 @@ unmarshall_aseprite_files :: proc( } /* - Goes through all the chunks in an aseprite document & copies the `Com_Image_Cel` cells in a separate image + Goes through all the chunks in an aseprite document & copies the `Com_Image_Cel` cells in a separate image */ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entry: AtlasEntry) { atlas_entry.frames = auto_cast len(document.frames) - fmt.println("N Frames: ", len(document.frames)) + log.infof("N Frames: ", len(document.frames)) // NOTE(stefan): Since the expected input for the program is multiple files containing a single sprite // it's probably a safe assumption most of the files will be a single layer with 1 or more frames // which means we can first prod the file for information about how many frames are there and @@ -110,13 +137,13 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr // instead of iterating all layers for each frame // might be even quicker to first get that information an allocate at once the amount of cells we need for frame, frameIdx in document.frames { - fmt.printfln("Frame_{0} Chunks: ", frameIdx, len(frame.chunks)) + log.infof("Frame_{0} Chunks: ", frameIdx, len(frame.chunks)) for chunk in frame.chunks { if cel_chunk, ok := chunk.(ase.Cel_Chunk); ok { cel_img, ci_ok := cel_chunk.cel.(ase.Com_Image_Cel) if !ci_ok do continue - fmt.println(cel_chunk.layer_index) + log.info(cel_chunk.layer_index) cell := CellData { img = rl.Image { @@ -134,7 +161,7 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr } if layer_chunk, ok := chunk.(ase.Layer_Chunk); ok { - fmt.println("Layer chunk: ", layer_chunk) + log.infof("Layer chunk: ", layer_chunk) append(&atlas_entry.layer_names, layer_chunk.name) } } @@ -148,7 +175,7 @@ atlas_entry_from_compressed_cells :: proc(document: ase.Document) -> (atlas_entr } /* - Takes in a slice of entries, an output texture and offsets (offset_x/y) + Takes in a slice of entries, an output texture and offsets (offset_x/y) */ pack_atlas_entries :: proc( entries: []AtlasEntry, @@ -201,10 +228,10 @@ pack_atlas_entries :: proc( stbrp.init_target(&ctx, atlas.width, atlas.height, &nodes[0], i32(num_entries)) res := stbrp.pack_rects(&ctx, &rects[0], i32(num_entries)) if res == 1 { - fmt.println("Packed everything successfully!") - fmt.printfln("Rects: {0}", rects[:]) + log.info("Packed everything successfully!") + log.infof("Rects: {0}", rects[:]) } else { - fmt.println("Failed to pack everything!") + log.error("Failed to pack everything!") } for rect, rectIdx in rects { @@ -228,7 +255,7 @@ pack_atlas_entries :: proc( // note(stefan): drawing the sprite in the atlas in the packed coordinates rl.ImageDraw(atlas, cell.img, src_rect, dst_rect, rl.WHITE) - fmt.printfln("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect) + log.infof("Src rect: {0}\nDst rect:{1}", src_rect, dst_rect) } metadata := make([dynamic]SpriteAtlasMetadata, allocator) @@ -250,7 +277,7 @@ pack_atlas_entries :: proc( } cell_metadata := SpriteAtlasMetadata { name = cell_name, - location = { + location = { auto_cast rect.x + auto_cast offset_x, auto_cast rect.y + auto_cast offset_y, }, @@ -262,53 +289,24 @@ pack_atlas_entries :: proc( return metadata } -SourceCodeGeneratorMetadata :: struct { - file_defines: struct { - top: string, - bottom: string, - file_name: string, - file_extension: string, - }, - lanugage_settings: struct { - first_class_enum_arrays: bool, // for languages that support creating arrays that contain for each enum value an entry in the enum_data.entry_line: .EnumCase = {array entry} - }, - custom_data_type: struct { - name: string, - type_declaration: string, // contains one param: custom_data_type.name + the rest of the type declaration like braces of the syntax & the type members - }, - enum_data: struct { - name: string, - begin_line: string, // contains one params: enum_data.name - entry_line: string, - end_line: string, - }, - array_data: struct { - name: string, - type: string, - begin_line: string, // array begin line contains 2 params in the listed order: array.name, array.type - entry_line: string, // array entry contains 5 params in the listed order: cell.name, cell.location.x, cell.location.y, cell.size.x, cell.size.y, - end_line: string, - }, -} - odin_source_generator_metadata := SourceCodeGeneratorMetadata { - file_defines = { + file_defines = { top = "package atlas_bindings\n\n", bottom = "", file_name = "metadata", file_extension = ".odin", }, - custom_data_type = { + custom_data_type = { name = "AtlasRect", type_declaration = "%v :: struct {{ x, y, w, h: i32 }}\n\n", }, - enum_data = { + enum_data = { name = "AtlasEnum", begin_line = "%v :: enum {{\n", entry_line = "\t%s,\n", end_line = "}\n\n", }, - array_data = { + array_data = { name = "ATLAS_SPRITES", type = "[]AtlasRect", begin_line = "%v := %v {{\n", @@ -320,23 +318,23 @@ odin_source_generator_metadata := SourceCodeGeneratorMetadata { cpp_source_generator_metadata := SourceCodeGeneratorMetadata { - file_defines = { + file_defines = { top = "#include \n\n", bottom = "", file_name = "metadata", file_extension = ".hpp", }, - custom_data_type = { + custom_data_type = { name = "AtlasRect", type_declaration = "struct %v {{\n\tint x;\n\tint y;\n\tint w;\n\tint h;\n}};\n\n", }, - enum_data = { + enum_data = { name = "AtlasEnum", begin_line = "enum %v {{\n", entry_line = "\t%s,\n", end_line = "\n\tCOUNT\n}\n\n", }, - array_data = { + array_data = { name = "ATLAS_SPRITES", type = "AtlasRect[size_t(AtlasEnum::COUNT)-1]", begin_line = "{1} {0} = {{\n", @@ -346,9 +344,9 @@ cpp_source_generator_metadata := SourceCodeGeneratorMetadata { } /* - Generates a barebones file with the package name "atlas_bindings", - the file contains an array of offsets, indexed by an enum. - The enum has unique names + Generates a barebones file with the package name "atlas_bindings", + the file contains an array of offsets, indexed by an enum. + The enum has unique names */ generate_odin_enums_and_atlas_offsets_file_sb :: proc( metadata: []SpriteAtlasMetadata, @@ -389,7 +387,7 @@ generate_odin_enums_and_atlas_offsets_file_sb :: proc( // end offsets array strings.write_string(&sb, "}\n\n") - fmt.println("\n", strings.to_string(sb)) + log.info("\n", strings.to_string(sb)) return sb } @@ -405,26 +403,21 @@ metadata_source_code_generate :: proc( strings.write_string(&sb, codegen.file_defines.top) // Introduce the Rect type - // strings.write_string(&sb, "AtlasRect :: struct { x, y, w, h: i32 }\n\n") strings.write_string( &sb, fmt.aprintf(codegen.custom_data_type.type_declaration, codegen.custom_data_type.name), ) // start enum - // strings.write_string(&sb, "AtlasSprite :: enum {\n") strings.write_string(&sb, fmt.aprintf(codegen.enum_data.begin_line, codegen.enum_data.name)) { for cell in metadata { - // strings.write_string(&sb, fmt.aprintf("\t%s,\n", cell.name)) strings.write_string(&sb, fmt.aprintf(codegen.enum_data.entry_line, cell.name)) } } // end enum - // strings.write_string(&sb, "}\n\n") strings.write_string(&sb, codegen.enum_data.end_line) // start offsets array - // strings.write_string(&sb, "ATLAS_SPRITES := []AtlasRect {\n") strings.write_string( &sb, fmt.aprintf( @@ -448,42 +441,46 @@ metadata_source_code_generate :: proc( } } // end offsets array - // strings.write_string(&sb, "}\n\n") + strings.write_string(&sb, codegen.array_data.end_line) strings.write_string(&sb, codegen.file_defines.bottom) - fmt.println("\n", strings.to_string(sb)) + log.info("\n", strings.to_string(sb)) return sb } -save_output :: proc() { - output_path, ok := g_mem.output_folder_path.(string) +save_output :: proc( + output_folder_path: Maybe(string), + atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata), + atlas_render_texture_target: rl.RenderTexture2D, +) { + output_path, ok := output_folder_path.(string) if !ok || output_path == "" { - fmt.println("Output path is empty!") + log.error("Output path is empty!") return } - image := rl.LoadImageFromTexture(g_mem.atlas_render_texture_target.texture) + image := rl.LoadImageFromTexture(atlas_render_texture_target.texture) rl.ImageFlipVertical(&image) cstring_atlas_output_path := strings.clone_to_cstring( - strings.concatenate({output_path, os_file_separator, "atlas.png"}), + strings.concatenate({output_path, OS_FILE_SEPARATOR, "atlas.png"}), ) rl.ExportImage(image, cstring_atlas_output_path) - if metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { - fmt.println("Building metadata...") + if metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata); ok { + log.info("Building metadata...") if json_metadata, jok := json.marshal(metadata); jok == nil { os.write_entire_file( - strings.concatenate({output_path, os_file_separator, "metadata.json"}), + strings.concatenate({output_path, OS_FILE_SEPARATOR, "metadata.json"}), json_metadata, ) } else { - fmt.println("Failed to marshall the atlas metadata to a json!") + log.error("Failed to marshall the atlas metadata to a json!") } // TODO(stefan): Think of a more generic alternative to just straight output to a odin file @@ -492,14 +489,14 @@ save_output :: proc() { sb := generate_odin_enums_and_atlas_offsets_file_sb(metadata[:]) odin_metadata := strings.to_string(sb) ok := os.write_entire_file( - strings.concatenate({output_path, os_file_separator, "metadata.odin"}), + strings.concatenate({output_path, OS_FILE_SEPARATOR, "metadata.odin"}), transmute([]byte)odin_metadata, ) if !ok { - fmt.println("Failed to save 'metadata.odin'") + log.error("Failed to save 'metadata.odin'") } } else { - fmt.println("No metadata to export!") + log.error("No metadata to export!") } } @@ -509,30 +506,31 @@ save_metadata_simple :: proc( json_file_name: Maybe(string), source_file_name: Maybe(string), source_gen_metadata: Maybe(SourceCodeGeneratorMetadata), + atlas_metadata: Maybe([dynamic]SpriteAtlasMetadata), ) { json_file_base_name, json_file_name_ok := json_file_name.(string) source_file_base_name, source_file_name_ok := source_file_name.(string) if !json_file_name_ok && !source_file_name_ok { - fmt.println("Neither a json file name or a source code filename has been provided!") + log.error("Neither a json file name or a source code filename has been provided!") return } - metadata, ok := g_mem.atlas_metadata.([dynamic]SpriteAtlasMetadata);if !ok { - fmt.println("No metadata to export!") + metadata, ok := atlas_metadata.([dynamic]SpriteAtlasMetadata);if !ok { + log.error("No metadata to export!") } - fmt.println("Building metadata...") + log.info("Building metadata...") if json_file_name_ok { if json_metadata, jok := json.marshal(metadata); jok == nil { json_output_path := strings.concatenate( - {output_path, os_file_separator, json_file_base_name}, + {output_path, OS_FILE_SEPARATOR, json_file_base_name}, ) if ok = os.write_entire_file(json_output_path, json_metadata); !ok { - fmt.println("Failed to write json to file: ", json_output_path) + log.errorf("Failed to write json to file: ", json_output_path) } } else { - fmt.println("Failed to marshall the atlas metadata to a json!") + log.error("Failed to marshall the atlas metadata to a json!") } } @@ -544,45 +542,45 @@ save_metadata_simple :: proc( source_metadata := strings.to_string(sb) source_output_path := strings.concatenate( - { + { output_path, - os_file_separator, + OS_FILE_SEPARATOR, codegen.file_defines.file_name, codegen.file_defines.file_extension, }, ) ok := os.write_entire_file(source_output_path, transmute([]byte)source_metadata) if !ok { - fmt.println("Failed to save source code to file:", source_output_path) + log.errorf("Failed to save source code to file:", source_output_path) } } else { sb := metadata_source_code_generate(metadata[:], odin_source_generator_metadata) odin_metadata := strings.to_string(sb) source_output_path := strings.concatenate( - {output_path, os_file_separator, "metadata.odin"}, + {output_path, OS_FILE_SEPARATOR, "metadata.odin"}, ) ok := os.write_entire_file(source_output_path, transmute([]byte)odin_metadata) if !ok { - fmt.println("Failed to save source code to file:", source_output_path) + log.errorf("Failed to save source code to file:", source_output_path) } } } } save_metadata :: proc( - settings: utils.CLIPackerSettings, + settings: CLIPackerSettings, atlas_entries: []AtlasEntry, atlas_metadata: []SpriteAtlasMetadata, ) { - metadata, ok := settings.metadata.(utils.CLIMetadataSettings);if !ok do return + metadata, ok := settings.metadata.(CLIMetadataSettings);if !ok do return if json_path, ok := metadata.json_path.(string); ok { json_bytes, jerr := json.marshal(atlas_metadata) if jerr == nil { os.write_entire_file(json_path, json_bytes) } else { - fmt.println("Failed to marshall metadata") + log.error("Failed to marshall metadata") } } if source_code_path, ok := metadata.source_code_path.(string); ok { diff --git a/src/main_hot_reload/main_hot_reload.odin b/src/main_hot_reload/main_hot_reload.odin deleted file mode 100644 index 99f4692..0000000 --- a/src/main_hot_reload/main_hot_reload.odin +++ /dev/null @@ -1,196 +0,0 @@ -// Development game exe. Loads game.dll and reloads it whenever it changes. - -package main - -import "core:c/libc" -import "core:dynlib" -import "core:fmt" -import "core:log" -import "core:mem" -import "core:os" - -when ODIN_OS == .Windows { - DLL_EXT :: ".dll" -} else when ODIN_OS == .Darwin { - DLL_EXT :: ".dylib" -} else { - DLL_EXT :: ".so" -} - -copy_dll :: proc(to: string) -> bool { - exit: i32 - when ODIN_OS == .Windows { - exit = libc.system(fmt.ctprintf("copy game.dll {0}", to)) - } else { - exit = libc.system(fmt.ctprintf("cp game" + DLL_EXT + " {0}", to)) - } - - if exit != 0 { - fmt.printfln("Failed to copy game" + DLL_EXT + " to {0}", to) - return false - } - - return true -} - -GameAPI :: struct { - lib: dynlib.Library, - init_window: proc(), - init: proc(), - update: proc() -> bool, - shutdown: proc(), - shutdown_window: proc(), - memory: proc() -> rawptr, - memory_size: proc() -> int, - hot_reloaded: proc(mem: rawptr), - force_reload: proc() -> bool, - force_restart: proc() -> bool, - modification_time: os.File_Time, - api_version: int, -} - -load_game_api :: proc(api_version: int) -> (api: GameAPI, ok: bool) { - mod_time, mod_time_error := os.last_write_time_by_name("game" + DLL_EXT) - if mod_time_error != os.ERROR_NONE { - fmt.printfln( - "Failed getting last write time of game" + DLL_EXT + ", error code: {1}", - mod_time_error, - ) - return - } - - // NOTE: this needs to be a relative path for Linux to work. - game_dll_name := fmt.tprintf( - "{0}game_{1}" + DLL_EXT, - "./" when ODIN_OS != .Windows else "", - api_version, - ) - copy_dll(game_dll_name) or_return - - _, ok = dynlib.initialize_symbols(&api, game_dll_name, "game_", "lib") - if !ok { - fmt.printfln("Failed initializing symbols: {0}", dynlib.last_error()) - } - - api.api_version = api_version - api.modification_time = mod_time - ok = true - - return -} - -unload_game_api :: proc(api: ^GameAPI) { - if api.lib != nil { - if !dynlib.unload_library(api.lib) { - fmt.printfln("Failed unloading lib: {0}", dynlib.last_error()) - } - } - - if os.remove(fmt.tprintf("game_{0}" + DLL_EXT, api.api_version)) != 0 { - fmt.printfln("Failed to remove game_{0}" + DLL_EXT + " copy", api.api_version) - } -} - -main :: proc() { - context.logger = log.create_console_logger() - - default_allocator := context.allocator - tracking_allocator: mem.Tracking_Allocator - mem.tracking_allocator_init(&tracking_allocator, default_allocator) - context.allocator = mem.tracking_allocator(&tracking_allocator) - - reset_tracking_allocator :: proc(a: ^mem.Tracking_Allocator) -> bool { - err := false - - for _, value in a.allocation_map { - fmt.printf("%v: Leaked %v bytes\n", value.location, value.size) - err = true - } - - mem.tracking_allocator_clear(a) - return err - } - - game_api_version := 0 - game_api, game_api_ok := load_game_api(game_api_version) - - if !game_api_ok { - fmt.println("Failed to load Game API") - return - } - - game_api_version += 1 - game_api.init_window() - game_api.init() - - old_game_apis := make([dynamic]GameAPI, default_allocator) - - window_open := true - for window_open { - window_open = game_api.update() - force_reload := game_api.force_reload() - force_restart := game_api.force_restart() - reload := force_reload || force_restart - game_dll_mod, game_dll_mod_err := os.last_write_time_by_name("game" + DLL_EXT) - - if game_dll_mod_err == os.ERROR_NONE && game_api.modification_time != game_dll_mod { - reload = true - } - - if reload { - new_game_api, new_game_api_ok := load_game_api(game_api_version) - - if new_game_api_ok { - if game_api.memory_size() != new_game_api.memory_size() || force_restart { - game_api.shutdown() - reset_tracking_allocator(&tracking_allocator) - - for &g in old_game_apis { - unload_game_api(&g) - } - - clear(&old_game_apis) - unload_game_api(&game_api) - game_api = new_game_api - game_api.init() - } else { - append(&old_game_apis, game_api) - game_memory := game_api.memory() - game_api = new_game_api - game_api.hot_reloaded(game_memory) - } - - game_api_version += 1 - } - } - - for b in tracking_allocator.bad_free_array { - log.error("Bad free at: %v", b.location) - } - - clear(&tracking_allocator.bad_free_array) - free_all(context.temp_allocator) - } - - free_all(context.temp_allocator) - game_api.shutdown() - reset_tracking_allocator(&tracking_allocator) - - for &g in old_game_apis { - unload_game_api(&g) - } - - delete(old_game_apis) - - game_api.shutdown_window() - unload_game_api(&game_api) - mem.tracking_allocator_destroy(&tracking_allocator) -} - -// make game use good GPU on laptops etc - -@(export) -NvOptimusEnablement: u32 = 1 - -@(export) -AmdPowerXpressRequestHighPerformance: i32 = 1 diff --git a/src/main_release/main_release.odin b/src/main_release/main_release.odin deleted file mode 100644 index 1d335d1..0000000 --- a/src/main_release/main_release.odin +++ /dev/null @@ -1,77 +0,0 @@ -// For making a release exe that does not use hot reload. - -package main_release - -import "core:log" -import "core:os" - -import game ".." - -UseTrackingAllocator :: #config(UseTrackingAllocator, false) - -main :: proc() { - when UseTrackingAllocator { - default_allocator := context.allocator - tracking_allocator: Tracking_Allocator - tracking_allocator_init(&tracking_allocator, default_allocator) - context.allocator = allocator_from_tracking_allocator(&tracking_allocator) - } - - mode: int = 0 - when ODIN_OS == .Linux || ODIN_OS == .Darwin { - mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH - } - - logh, logh_err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode) - - if logh_err == os.ERROR_NONE { - os.stdout = logh - os.stderr = logh - } - - logger := - logh_err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger() - context.logger = logger - - game.game_init_window() - game.game_init() - - window_open := true - for window_open { - window_open = game.game_update() - - when UseTrackingAllocator { - for b in tracking_allocator.bad_free_array { - log.error("Bad free at: %v", b.location) - } - - clear(&tracking_allocator.bad_free_array) - } - - free_all(context.temp_allocator) - } - - free_all(context.temp_allocator) - game.game_shutdown() - game.game_shutdown_window() - - if logh_err == os.ERROR_NONE { - log.destroy_file_logger(&logger) - } - - when UseTrackingAllocator { - for key, value in tracking_allocator.allocation_map { - log.error("%v: Leaked %v bytes\n", value.location, value.size) - } - - tracking_allocator_destroy(&tracking_allocator) - } -} - -// make game use good GPU on laptops etc - -@(export) -NvOptimusEnablement: u32 = 1 - -@(export) -AmdPowerXpressRequestHighPerformance: i32 = 1 diff --git a/src/symbol_exports.odin b/src/symbol_exports.odin deleted file mode 100644 index d5ada25..0000000 --- a/src/symbol_exports.odin +++ /dev/null @@ -1,88 +0,0 @@ -package game - -import rl "vendor:raylib" - -@(export) -game_update :: proc() -> bool { - update() - draw() - return !rl.WindowShouldClose() -} - -@(export) -game_init_window :: proc() { - rl.SetConfigFlags({.WINDOW_RESIZABLE}) - rl.InitWindow(1400, 800, "YAAP - Yet Another Atlas Packer") - rl.SetWindowMinSize(1400, 800) -} - -@(export) -game_init :: proc() { - g_mem = new(GameMemory) - - g_mem^ = GameMemory{} - - game_hot_reloaded(g_mem) - - - when !ODIN_DEBUG { - rl.SetExitKey(nil) - } - - current_monitor := rl.GetCurrentMonitor() - g_mem.monitor_info = MonitorInformation { - max_width = auto_cast rl.GetMonitorWidth(current_monitor), - max_height = auto_cast rl.GetMonitorHeight(current_monitor), - } - - g_mem.window_info = WindowInformation { - w = 1280, - h = 720, - } - - g_mem.atlas_render_texture_target = rl.LoadRenderTexture(256, 256) - g_mem.atlas_render_size = 256 - - checkered_img := rl.GenImageChecked(256, 256, 256 / 4, 256 / 4, rl.GRAY, rl.DARKGRAY) - defer rl.UnloadImage(checkered_img) - g_mem.atlas_checked_background.texture = rl.LoadTextureFromImage(checkered_img) - - rl.SetTargetFPS(rl.GetMonitorRefreshRate(current_monitor)) - rl.GuiLoadStyle("./styles/style_candy.rgs") -} - -@(export) -game_shutdown :: proc() { - free(g_mem) -} - -@(export) -game_shutdown_window :: proc() { - rl.CloseWindow() -} - -@(export) -game_memory :: proc() -> rawptr { - return g_mem -} - -@(export) -game_memory_size :: proc() -> int { - return size_of(GameMemory) -} - -@(export) -game_hot_reloaded :: proc(mem: rawptr) { - g_mem = (^GameMemory)(mem) - rl.GuiLoadStyle("./styles/style_candy.rgs") -} - -@(export) -game_force_reload :: proc() -> bool { - return rl.IsKeyPressed(.F5) -} - -@(export) -game_force_restart :: proc() -> bool { - return rl.IsKeyPressed(.F6) -} diff --git a/src/utils/animation.odin b/src/utils/animation.odin deleted file mode 100644 index 4a55c6e..0000000 --- a/src/utils/animation.odin +++ /dev/null @@ -1,53 +0,0 @@ -// This implements simple animations using sprite sheets. The texture in the -// `Animation` struct is assumed to contain a horizontal strip of the frames -// in the animation. Call `animation_update` to update and then call -// `animation_rect` when you wish to know the source rect to use in the texture -// With the source rect you can run rl.DrawTextureRec to draw the current frame. - -package utils - -import "core:log" - -Animation :: struct { - texture: Texture, - num_frames: int, - current_frame: int, - frame_timer: f32, - frame_length: f32, -} - -animation_create :: proc(tex: Texture, num_frames: int, frame_length: f32) -> Animation { - return( - Animation { - texture = tex, - num_frames = num_frames, - frame_length = frame_length, - frame_timer = frame_length, - } \ - ) -} - -animation_update :: proc(a: ^Animation, dt: f32) { - a.frame_timer -= dt - - if a.frame_timer <= 0 { - a.frame_timer = a.frame_length + a.frame_timer - a.current_frame += 1 - - if a.current_frame >= a.num_frames { - a.current_frame = 0 - } - } -} - -animation_rect :: proc(a: Animation) -> Rect { - if a.num_frames == 0 { - log.error("Animation has zero frames") - return RectEmpty - } - - w := f32(a.texture.width) / f32(a.num_frames) - h := f32(a.texture.height) - - return {x = f32(a.current_frame) * w, y = 0, width = w, height = h} -} diff --git a/src/utils/handle_array.odin b/src/utils/handle_array.odin deleted file mode 100644 index b6b59d1..0000000 --- a/src/utils/handle_array.odin +++ /dev/null @@ -1,141 +0,0 @@ -// This handle-based array gives you a statically allocated array where you can -// use index based handles instead of pointers. The handles have a generation -// that makes sure you don't get bugs when slots are re-used. -// Read more about it here: https://floooh.github.io/2018/06/17/handles-vs-pointers.html */ - -package utils - -Handle :: struct($T: typeid) { - // idx 0 means unused. Note that slot 0 is a dummy slot, it can never be used. - idx: u32, - gen: u32, -} - -HandleArrayItem :: struct($T: typeid) { - item: T, - handle: Handle(T), -} - -// TODO: Add a freelist that uses some kind of bit array... We should be able to -// check 64 item slots at a time that way, but without any dynamic array. -HandleArray :: struct($T: typeid, $N: int) { - items: #soa[N]HandleArrayItem(T), - num_items: u32, -} - -ha_add :: proc(a: ^HandleArray($T, $N), v: T) -> (Handle(T), bool) #optional_ok { - for idx in 1 ..< a.num_items { - i := &a.items[idx] - - if idx != 0 && i.handle.idx == 0 { - i.handle.idx = u32(idx) - i.item = v - return i.handle, true - } - } - - // Index 0 is dummy - if a.num_items == 0 { - a.num_items += 1 - } - - if a.num_items == len(a.items) { - return {}, false - } - - idx := a.num_items - i := &a.items[a.num_items] - a.num_items += 1 - i.handle.idx = idx - i.handle.gen = 1 - i.item = v - return i.handle, true -} - -ha_get :: proc(a: HandleArray($T, $N), h: Handle(T)) -> (T, bool) { - if h.idx == 0 { - return {}, false - } - - if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h { - return a.items[h.idx].item, true - } - - return {}, false -} - -ha_get_ptr :: proc(a: HandleArray($T, $N), h: Handle(T)) -> ^T { - if h.idx == 0 { - return nil - } - - if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h { - return &ha.items[h.idx].item - } - - return nil -} - -ha_remove :: proc(a: ^HandleArray($T, $N), h: Handle(T)) { - if h.idx == 0 { - return - } - - if int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h { - a.items[h.idx].handle.idx = 0 - a.items[h.idx].handle.gen += 1 - } -} - -ha_valid :: proc(a: HandleArray($T, $N), h: Handle(T)) -> bool { - if h.idx == 0 { - return false - } - - return int(h.idx) < len(a.items) && h.idx < a.num_items && a.items[h.idx].handle == h -} - -HandleArrayIter :: struct($T: typeid, $N: int) { - a: ^HandleArray(T, N), - index: int, -} - -ha_make_iter :: proc(a: ^HandleArray($T, $N)) -> HandleArrayIter(T, N) { - return HandleArrayIter(T, N){a = a} -} - -ha_iter :: proc(it: ^HandleArrayIter($T, $N)) -> (val: T, h: Handle(T), cond: bool) { - cond = it.index < int(it.a.num_items) - - for ; cond; cond = it.index < int(it.a.num_items) { - if it.a.items[it.index].handle.idx == 0 { - it.index += 1 - continue - } - - val = it.a.items[it.index].item - h = it.a.items[it.index].handle - it.index += 1 - break - } - - return -} - -ha_iter_ptr :: proc(it: ^HandleArrayIter($T, $N)) -> (val: ^T, h: Handle(T), cond: bool) { - cond = it.index < int(it.a.num_items) - - for ; cond; cond = it.index < int(it.a.num_items) { - if it.a.items[it.index].handle.idx == 0 { - it.index += 1 - continue - } - - val = &it.a.items[it.index].item - h = it.a.items[it.index].handle - it.index += 1 - break - } - - return -} diff --git a/src/utils/helpers.odin b/src/utils/helpers.odin deleted file mode 100644 index 07a5a86..0000000 --- a/src/utils/helpers.odin +++ /dev/null @@ -1,55 +0,0 @@ -// generic odin helpers - -package utils - -import "core:intrinsics" -import "core:reflect" -import "core:strings" -import rl "vendor:raylib" - -Texture :: rl.Texture -Color :: rl.Color - -Rect :: rl.Rectangle -RectEmpty :: Rect{} - -increase_or_wrap_enum :: proc(e: $T) -> T { - ei := int(e) + 1 - - if ei >= len(T) { - ei = 0 - } - - return T(ei) -} - -union_type :: proc(a: any) -> typeid { - return reflect.union_variant_typeid(a) -} - -temp_cstring :: proc(s: string) -> cstring { - return strings.clone_to_cstring(s, context.temp_allocator) -} - -// There is a remap in core:math but it doesn't clamp in the new range, which I -// always want. -remap :: proc "contextless" ( - old_value, old_min, old_max, new_min, new_max: $T, -) -> ( - x: T, -) where intrinsics.type_is_numeric(T), - !intrinsics.type_is_array(T) { - old_range := old_max - old_min - new_range := new_max - new_min - if old_range == 0 { - return new_range / 2 - } - return clamp(((old_value - old_min) / old_range) * new_range + new_min, new_min, new_max) -} - -Vec2i :: [2]int -Vec2 :: [2]f32 - -vec2_from_vec2i :: proc(p: Vec2i) -> Vec2 { - return {f32(p.x), f32(p.y)} -} diff --git a/src/aseprite b/vendors/aseprite similarity index 100% rename from src/aseprite rename to vendors/aseprite diff --git a/src/dialog/build.bat b/vendors/dialog/build.bat similarity index 100% rename from src/dialog/build.bat rename to vendors/dialog/build.bat diff --git a/src/dialog/build.sh b/vendors/dialog/build.sh old mode 100755 new mode 100644 similarity index 100% rename from src/dialog/build.sh rename to vendors/dialog/build.sh diff --git a/src/dialog/libtinyfiledialogs b/vendors/dialog/libtinyfiledialogs similarity index 100% rename from src/dialog/libtinyfiledialogs rename to vendors/dialog/libtinyfiledialogs diff --git a/src/dialog/tinyfiledialog.odin b/vendors/dialog/tinyfiledialog.odin similarity index 100% rename from src/dialog/tinyfiledialog.odin rename to vendors/dialog/tinyfiledialog.odin