#define DQN_ASAN_POISON 1 #define DQN_ASAN_VET_POISON 1 #define DQN_ONLY_RECT #define DQN_ONLY_WIN #define DQN_ONLY_V2 #define DQN_ONLY_SLICE #define DQN_ONLY_SARRAY #define DQN_ONLY_DSMAP #define DQN_ONLY_LIST #define DQN_ONLY_FS #define _CRT_SECURE_NO_WARNINGS #define DQN_IMPLEMENTATION #include "External/tely/External/dqn/dqn.h" DQN_MSVC_WARNING_PUSH DQN_MSVC_WARNING_DISABLE(4244) // warning C4244: 'argument': conversion from 'int' to 'short', possible loss of data #define STB_RECT_PACK_IMPLEMENTATION #include "External/tely/external/stb/stb_rect_pack.h" #define STB_IMAGE_WRITE_IMPLEMENTATION #include "External/tely/external/stb/stb_image_write.h" #define STB_IMAGE_IMPLEMENTATION #include "External/tely/external/stb/stb_image.h" DQN_MSVC_WARNING_POP DQN_MSVC_WARNING_PUSH DQN_MSVC_WARNING_DISABLE(4244) // External\strnatcmp.c|58| warning C4244: 'return': conversion from 'int' to 'nat_char', possible loss of data DQN_MSVC_WARNING_DISABLE(4702) // External\strnatcmp.c|110 warning| C4702: unreachable code #include "External/strnatcmp.h" #include "External/strnatcmp.c" DQN_MSVC_WARNING_POP struct SpriteSpecification { uint16_t frame_count; uint16_t frames_per_second; }; static Dqn_Str8 SpriteAnimNameFromFilePath(Dqn_Str8 path) { // NOTE: Enumerate the number of frames for this animation ================================= Dqn_Str8 file_name = Dqn_Str8_FileNameFromPath(path); Dqn_Str8 file_name_without_extension = Dqn_Str8_BinarySplit(file_name, DQN_STR8(".")).lhs; // NOTE: If the sprite is a standalone sprite without any frame suffix (e.g. "_1.png") then // we accept the entry as the name of the sprite as is. Dqn_Str8 result = {}; if (Dqn_Char_IsDigit(file_name_without_extension.data[file_name_without_extension.size - 1])) { Dqn_Str8BinarySplitResult split = Dqn_Str8_BinarySplitReverse(file_name_without_extension, DQN_STR8("_")); result = split.lhs; } else { result = file_name_without_extension; } return result; } int main(int argc, char const *argv[]) { Dqn_Library_Init(Dqn_LibraryOnInit_Nil); Dqn_ThreadScratch scratch = Dqn_Thread_GetScratch(nullptr); if (argc != 4) { Dqn_Log_InfoF("USAGE: feely_pona_sprite_packer x "); return -1; } // NOTE: Verify some arguments ================================================================= Dqn_Str8 atlas_dimensions = Dqn_Str8_InitCStr8(argv[1]); Dqn_Str8 sprite_spec_path = Dqn_Str8_InitCStr8(argv[2]); Dqn_Str8 dir = Dqn_Str8_InitCStr8(argv[3]); if (!Dqn_Fs_Exists(sprite_spec_path)) { Dqn_Log_ErrorF("Sprite specification file does not exist, we tried to find \"%.*s\" but it does not exist", DQN_STR_FMT(sprite_spec_path)); return -1; } if (!Dqn_Fs_DirExists(dir)) { Dqn_Log_ErrorF("Directory to load sprites from does not exist, we tried to find \"%.*s\" but it does not exist", DQN_STR_FMT(dir)); return -1; } // NOTE: Parse the atlas size ================================================================== Dqn_V2I atlas_size = {}; { Dqn_Str8BinarySplitResult atlas_dimensions_split = Dqn_Str8_BinarySplit(atlas_dimensions, DQN_STR8("x")); Dqn_Str8ToU64Result width = Dqn_Str8_ToU64(atlas_dimensions_split.lhs, 0); Dqn_Str8ToU64Result height = Dqn_Str8_ToU64(atlas_dimensions_split.rhs, 0); if (!width.success || width.value == 0) { Dqn_Log_ErrorF("Width for the sprite atlas was not a number > 0"); return -1; } if (!height.success || height.value == 0) { Dqn_Log_ErrorF("Width for the sprite atlas was not a number > 0"); return -1; } atlas_size.x = DQN_CAST(uint32_t)width.value; atlas_size.y = DQN_CAST(uint32_t)height.value; } // NOTE: Parse the sprite specifications ======================================================= Dqn_DSMap sprite_spec_table = Dqn_DSMap_Init(1024); { Dqn_ThreadScratch inner_scratch = Dqn_Thread_GetScratch(scratch.arena); Dqn_Str8 sprite_spec_buffer = Dqn_Fs_Read(sprite_spec_path, inner_scratch.allocator); Dqn_Str8SplitAllocResult sprite_spec_lines = Dqn_Str8_SplitAlloc(inner_scratch.allocator, sprite_spec_buffer, DQN_STR8("\n")); DQN_FOR_UINDEX(line_index, sprite_spec_lines.size) { Dqn_Str8 line = Dqn_Str8_TrimWhitespaceAround(sprite_spec_lines.data[line_index]); if (line.size == 0) // NOTE: Support empty lines, just ignore them continue; Dqn_Str8SplitAllocResult line_parts = Dqn_Str8_SplitAlloc(inner_scratch.allocator, line, DQN_STR8(";")); DQN_ASSERTF(line_parts.size == 2, "Line must have 2 parts in the sprite specification\n" "\n" ";\\n\n" "\n" "Line was '%.*s' loaded from '%.*s'", DQN_STR_FMT(line), DQN_STR_FMT(sprite_spec_path)); Dqn_Str8 anim_name = line_parts.data[0]; Dqn_Str8ToU64Result frames_per_second = Dqn_Str8_ToU64(line_parts.data[1], 0); DQN_ASSERTF(frames_per_second.success, "Frames per second was not a convertible number, line was '%.*s'", line); Dqn_DSMapResult slot = Dqn_DSMap_MakeKeyStr8Copy(&sprite_spec_table, scratch.allocator, anim_name); slot.value->frames_per_second = DQN_CAST(uint16_t)frames_per_second.value; } } // NOTE: Get the list of files ================================================================= Dqn_List file_list_raw = Dqn_List_Init(scratch.arena, 128); for (Dqn_Win_FolderIterator it = {}; Dqn_Win_FolderIterate(dir, &it); ) { if (Dqn_Str8_EndsWithInsensitive(it.file_name, DQN_STR8(".png"))) { Dqn_Str8 *item = Dqn_List_Make(&file_list_raw, Dqn_ZeroMem_Yes); *item = Dqn_FsPath_ConvertF(scratch.arena, "%.*s/%.*s", DQN_STR_FMT(dir), DQN_STR_FMT(it.file_name)); } } // NOTE: Sort the list of files ================================================================ if (file_list_raw.count == 0) { Dqn_Log_InfoF("There are no '.png' files in the directory '%.*s' to create an atlas from, exiting", DQN_STR_FMT(dir)); return 0; } // NOTE: Bubble sort the file list naturally =================================================== Dqn_Slice file_list = Dqn_List_ToSliceCopy(&file_list_raw, scratch.arena); for (bool swapped = true; swapped; ) { swapped = false; for (Dqn_usize list_index = 0; list_index < file_list.size - 1; list_index++) { Dqn_Str8 left = file_list.data[list_index + 0]; Dqn_Str8 right = file_list.data[list_index + 1]; if (strnatcmp(left.data, right.data) > 0) { DQN_SWAP(file_list.data[list_index + 0], file_list.data[list_index + 1]); swapped = true; } } } // NOTE: Setup the rect-pack state ============================================================= stbrp_node *nodes = Dqn_Arena_NewArray(scratch.arena, stbrp_node, file_list.size, Dqn_ZeroMem_Yes); stbrp_context pack_context = {}; stbrp_init_target(&pack_context, atlas_size.w, atlas_size.h, nodes, DQN_CAST(int)file_list.size); // NOTE: Load the sprites to determine their dimensions for rect packing ======================= Dqn_SArray rects = Dqn_SArray_Init(scratch.arena, file_list.size, Dqn_ZeroMem_Yes); for (Dqn_Str8 it : file_list) { int x = 0, y = 0, channels_in_file = 0; stbi_uc *pixels = stbi_load(it.data, &x, &y, &channels_in_file, 4 /*desired_channels*/); DQN_ASSERT(pixels); stbi_image_free(pixels); // NOTE: Add a rect to the packing context ================================================= stbrp_rect *rect = Dqn_SArray_Make(&rects, Dqn_ZeroMem_Yes); rect->w = x; rect->h = y; // NOTE: Enumerate the number of frames for this animation ================================= Dqn_Str8 anim_prefix = SpriteAnimNameFromFilePath(it); Dqn_Str8 file_name = Dqn_Str8_FileNameFromPath(it); DQN_ASSERTF(!Dqn_Str8_HasChar(file_name, ';'), "\n\nSprite frame loaded from file\n" " '%.*s'\n" "\n" "however the file name has a semicolon which is not supported because we use a semicolon to delimit our sprite specification", DQN_STR_FMT(file_name)); Dqn_DSMapResult slot = Dqn_DSMap_FindKeyStr8(&sprite_spec_table, anim_prefix); DQN_ASSERTF(slot.found, "\n\nSprite frame loaded from file\n" " '%.*s'\n" "\n" "however there's no corresponding entry in the specification file loaded at\n" " '%.*s'\n" "\n" "Add a line in format of ; to the file, e.g.\n" " %.*s;8\n", DQN_STR_FMT(it), DQN_STR_FMT(sprite_spec_path), DQN_STR_FMT(anim_prefix)); slot.value->frame_count++; Dqn_Log_InfoF("Packing sprite: %.*s", DQN_STR_FMT(it)); } // NOTE: Pack the rects ======================================================================== if (stbrp_pack_rects(&pack_context, rects.data, DQN_CAST(int)rects.size) != 1) { Dqn_Log_ErrorF("STB rect pack failed to pack font rects into rectangle [width=%d, height=%d, num_rects=%d]", atlas_size.w, atlas_size.h, file_list.size); return -1; } // NOTE: Load the files once more and generate the final image ================================= int final_bpp = 4; int final_image_stride = atlas_size.w * final_bpp; char *final_image = Dqn_Arena_NewArray(scratch.arena, char, atlas_size.h * final_image_stride, Dqn_ZeroMem_Yes); Dqn_Str8 atlas_path = Dqn_Str8_InitF(scratch.allocator, "%.*s.png", DQN_STR_FMT(dir)); // NOTE: Generate the meta file ================================================================ // NOTE: Count the number of animations we loaded frames fore Dqn_usize num_anims = 0; Dqn_usize num_anim_rects = 0; for (Dqn_usize index = 1 /*Sentinel*/; index < sprite_spec_table.occupied; index++) { Dqn_DSMapSlot *slot = sprite_spec_table.slots + index; if (slot->value.frame_count) { num_anims++; num_anim_rects += slot->value.frame_count; } } Dqn_Str8 meta_path = Dqn_Str8_InitF(scratch.allocator, "%.*s.txt", DQN_STR_FMT(dir)); Dqn_FsFile meta_file = Dqn_Fs_OpenFile(meta_path, Dqn_FsFileOpen_CreateAlways, Dqn_FsFileAccess_Write); Dqn_Fs_WriteFileF(&meta_file, "@file;%.*s;%d;%d\n", DQN_STR_FMT(Dqn_Str8_FileNameFromPath(atlas_path)), DQN_CAST(int) num_anim_rects, DQN_CAST(int) num_anims); Dqn_Log_InfoF("Generating meta file: %.*s", DQN_STR_FMT(meta_path)); Dqn_Str8 active_anim_prefix = {}; DQN_FOR_UINDEX (file_list_index, file_list.size) { Dqn_Str8 it = file_list.data[file_list_index]; int w, h, channels_in_file; stbi_uc *loaded_image = stbi_load(it.data, &w, &h, &channels_in_file, 4 /*desired_channels*/); stbrp_rect packed_rect = rects.data[file_list_index]; char *src = DQN_CAST(char *)loaded_image; // NOTE: Generate the image ================================================================ for (int y = 0; y < packed_rect.h; y++) { char *row = final_image + ((packed_rect.y + y) * final_image_stride) + (packed_rect.x * final_bpp); for (int x = 0; x < packed_rect.w; x++) { *row++ = *src++; *row++ = *src++; *row++ = *src++; *row++ = *src++; } } stbi_image_free(loaded_image); // NOTE: Detect what animation we are currently processing ================================= Dqn_Str8 anim_prefix = SpriteAnimNameFromFilePath(it); if (active_anim_prefix.size == 0 || active_anim_prefix != anim_prefix) { // NOTE: Anim prefix is different, we are starting a new animation- mark it accordingly active_anim_prefix = anim_prefix; Dqn_DSMapResult slot = Dqn_DSMap_FindKeyStr8(&sprite_spec_table, active_anim_prefix); Dqn_Fs_WriteFileF(&meta_file, "@anim;%.*s;%u;%u\n", DQN_STR_FMT(active_anim_prefix), slot.value->frames_per_second, slot.value->frame_count); } // NOTE: Write the sprite rectangles in ==================================================== Dqn_Str8 file_name = Dqn_Str8_FileNameFromPath(it); Dqn_Fs_WriteFileF(&meta_file, "%d;%d;%d;%d;%.*s", packed_rect.x, packed_rect.y, packed_rect.w, packed_rect.h, DQN_STR_FMT(file_name)); if (file_list_index != (file_list.size - 1)) Dqn_Fs_WriteFileF(&meta_file, "\n"); } Dqn_Log_InfoF("Generating atlas: %.*s", DQN_STR_FMT(atlas_path)); stbi_write_png(atlas_path.data, atlas_size.w, atlas_size.h, 4, final_image, final_image_stride); return 0; }