fp: Fix natural sorting order breaking the sprite packer
This commit is contained in:
parent
631579d29b
commit
b6f589a8dc
BIN
Data/Textures/atlas.png
(Stored with Git LFS)
BIN
Data/Textures/atlas.png
(Stored with Git LFS)
Binary file not shown.
BIN
Data/Textures/atlas.txt
(Stored with Git LFS)
BIN
Data/Textures/atlas.txt
(Stored with Git LFS)
Binary file not shown.
177
External/strnatcmp.c
vendored
Normal file
177
External/strnatcmp.c
vendored
Normal file
@ -0,0 +1,177 @@
|
||||
/* -*- mode: c; c-file-style: "k&r" -*-
|
||||
|
||||
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
|
||||
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
|
||||
/* partial change history:
|
||||
*
|
||||
* 2004-10-10 mbp: Lift out character type dependencies into macros.
|
||||
*
|
||||
* Eric Sosman pointed out that ctype functions take a parameter whose
|
||||
* value must be that of an unsigned int, even on platforms that have
|
||||
* negative chars in their default char type.
|
||||
*/
|
||||
|
||||
#include <stddef.h> /* size_t */
|
||||
#include <ctype.h>
|
||||
|
||||
#include "strnatcmp.h"
|
||||
|
||||
|
||||
/* These are defined as macros to make it easier to adapt this code to
|
||||
* different characters types or comparison functions. */
|
||||
static inline int
|
||||
nat_isdigit(nat_char a)
|
||||
{
|
||||
return isdigit((unsigned char) a);
|
||||
}
|
||||
|
||||
|
||||
static inline int
|
||||
nat_isspace(nat_char a)
|
||||
{
|
||||
return isspace((unsigned char) a);
|
||||
}
|
||||
|
||||
|
||||
static inline nat_char
|
||||
nat_toupper(nat_char a)
|
||||
{
|
||||
return toupper((unsigned char) a);
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
compare_right(nat_char const *a, nat_char const *b)
|
||||
{
|
||||
int bias = 0;
|
||||
|
||||
/* The longest run of digits wins. That aside, the greatest
|
||||
value wins, but we can't know that it will until we've scanned
|
||||
both numbers to know that they have the same magnitude, so we
|
||||
remember it in BIAS. */
|
||||
for (;; a++, b++) {
|
||||
if (!nat_isdigit(*a) && !nat_isdigit(*b))
|
||||
return bias;
|
||||
if (!nat_isdigit(*a))
|
||||
return -1;
|
||||
if (!nat_isdigit(*b))
|
||||
return +1;
|
||||
if (*a < *b) {
|
||||
if (!bias)
|
||||
bias = -1;
|
||||
} else if (*a > *b) {
|
||||
if (!bias)
|
||||
bias = +1;
|
||||
} else if (!*a && !*b)
|
||||
return bias;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
compare_left(nat_char const *a, nat_char const *b)
|
||||
{
|
||||
/* Compare two left-aligned numbers: the first to have a
|
||||
different value wins. */
|
||||
for (;; a++, b++) {
|
||||
if (!nat_isdigit(*a) && !nat_isdigit(*b))
|
||||
return 0;
|
||||
if (!nat_isdigit(*a))
|
||||
return -1;
|
||||
if (!nat_isdigit(*b))
|
||||
return +1;
|
||||
if (*a < *b)
|
||||
return -1;
|
||||
if (*a > *b)
|
||||
return +1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
strnatcmp0(nat_char const *a, nat_char const *b, int fold_case)
|
||||
{
|
||||
int ai, bi;
|
||||
nat_char ca, cb;
|
||||
int fractional, result;
|
||||
|
||||
ai = bi = 0;
|
||||
while (1) {
|
||||
ca = a[ai]; cb = b[bi];
|
||||
|
||||
/* skip over leading spaces or zeros */
|
||||
while (nat_isspace(ca))
|
||||
ca = a[++ai];
|
||||
|
||||
while (nat_isspace(cb))
|
||||
cb = b[++bi];
|
||||
|
||||
/* process run of digits */
|
||||
if (nat_isdigit(ca) && nat_isdigit(cb)) {
|
||||
fractional = (ca == '0' || cb == '0');
|
||||
|
||||
if (fractional) {
|
||||
if ((result = compare_left(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
} else {
|
||||
if ((result = compare_right(a+ai, b+bi)) != 0)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ca && !cb) {
|
||||
/* The strings compare the same. Perhaps the caller
|
||||
will want to call strcmp to break the tie. */
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (fold_case) {
|
||||
ca = nat_toupper(ca);
|
||||
cb = nat_toupper(cb);
|
||||
}
|
||||
|
||||
if (ca < cb)
|
||||
return -1;
|
||||
|
||||
if (ca > cb)
|
||||
return +1;
|
||||
|
||||
++ai; ++bi;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
strnatcmp(nat_char const *a, nat_char const *b) {
|
||||
return strnatcmp0(a, b, 0);
|
||||
}
|
||||
|
||||
|
||||
/* Compare, recognizing numeric string and ignoring case. */
|
||||
int
|
||||
strnatcasecmp(nat_char const *a, nat_char const *b) {
|
||||
return strnatcmp0(a, b, 1);
|
||||
}
|
38
External/strnatcmp.h
vendored
Normal file
38
External/strnatcmp.h
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
/* -*- mode: c; c-file-style: "k&r" -*-
|
||||
|
||||
strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
|
||||
Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* CUSTOMIZATION SECTION
|
||||
*
|
||||
* You can change this typedef, but must then also change the inline
|
||||
* functions in strnatcmp.c */
|
||||
typedef char nat_char;
|
||||
|
||||
int strnatcmp(nat_char const *a, nat_char const *b);
|
||||
int strnatcasecmp(nat_char const *a, nat_char const *b);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
2
External/tely
vendored
2
External/tely
vendored
@ -1 +1 @@
|
||||
Subproject commit f4ab2fbd147623ac38be6f92fedf0e0049aeafb5
|
||||
Subproject commit c63954d31b0c99203c5dc752494bbaefd59128f0
|
@ -102,7 +102,7 @@ TELY_AssetSpriteSheet FP_LoadSpriteSheetFromSpec(TELY_Platform *platform, TELY_A
|
||||
anim->ms_per_frame = DQN_CAST(uint32_t)(1000.f / frames_per_second.value);
|
||||
DQN_ASSERT(anim->ms_per_frame != 0);
|
||||
} else {
|
||||
DQN_ASSERTF(line_splits.size == 4, "Expected 4 splits for sprite frame lines");
|
||||
DQN_ASSERTF(line_splits.size == 5, "Expected 5 splits for sprite frame lines");
|
||||
Dqn_String8ToU64Result x = Dqn_String8_ToU64(line_splits.data[0], 0);
|
||||
Dqn_String8ToU64Result y = Dqn_String8_ToU64(line_splits.data[1], 0);
|
||||
Dqn_String8ToU64Result w = Dqn_String8_ToU64(line_splits.data[2], 0);
|
||||
@ -2224,14 +2224,20 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
|
||||
if (entity->action.sprite.anim) {
|
||||
FP_GameEntityAction const *action = &entity->action;
|
||||
TELY_AssetAnimatedSprite const sprite = action->sprite;
|
||||
uint64_t const elapsed_ms = game->clock_ms - action->started_at_clock_ms;
|
||||
uint16_t const raw_anim_frame = DQN_CAST(uint16_t)(elapsed_ms / sprite.anim->ms_per_frame);
|
||||
DQN_ASSERTF(sprite.anim->count, "We will modulo by 0 or overflow to UINT64_MAX");
|
||||
|
||||
// TODO(doyle): So many ways to create and get sprite data .. its a mess
|
||||
// I want to override per sprite anim height, we currently use the one
|
||||
// in the entity which is not correct.
|
||||
FP_EntityRenderData render_data = FP_Entity_GetRenderData(game, entity->type, action->state, entity->direction);
|
||||
|
||||
uint64_t elapsed_ms = game->clock_ms - action->started_at_clock_ms;
|
||||
uint16_t anim_frame = 0;
|
||||
if (action->sprite_play_once) {
|
||||
anim_frame = DQN_MIN(DQN_CAST(uint16_t)(elapsed_ms / sprite.anim->ms_per_frame), sprite.anim->count);
|
||||
} else {
|
||||
anim_frame = DQN_CAST(uint16_t)(elapsed_ms / sprite.anim->ms_per_frame) % sprite.anim->count;
|
||||
}
|
||||
if (action->sprite_play_once)
|
||||
anim_frame = DQN_MIN(raw_anim_frame, (sprite.anim->count - 1));
|
||||
else
|
||||
anim_frame = raw_anim_frame % sprite.anim->count;
|
||||
|
||||
Dqn_usize sprite_index = sprite.anim->index + anim_frame;
|
||||
Dqn_Rect src_rect = {};
|
||||
@ -2252,7 +2258,7 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
|
||||
}
|
||||
|
||||
Dqn_f32 sprite_in_meters = FP_Game_PixelsToMetersNx1(game, src_rect.size.y);
|
||||
Dqn_f32 size_scale = entity->sprite_height.meters / sprite_in_meters;
|
||||
Dqn_f32 size_scale = render_data.height.meters / sprite_in_meters;
|
||||
|
||||
Dqn_Rect dest_rect = {};
|
||||
dest_rect.size = src_rect.size * size_scale;
|
||||
|
@ -261,7 +261,10 @@ FP_EntityRenderData FP_Entity_GetRenderData(FP_Game *game, FP_EntityType type, u
|
||||
switch (state) {
|
||||
case FP_EntityMobSpawnerState_Nil: break;
|
||||
case FP_EntityMobSpawnerState_Idle: result.anim_name = g_anim_names.portal; break;
|
||||
case FP_EntityMobSpawnerState_Shutdown: result.anim_name = g_anim_names.portal_break; break;
|
||||
case FP_EntityMobSpawnerState_Shutdown: {
|
||||
result.anim_name = g_anim_names.portal_break;
|
||||
result.height.meters = 3.5f;
|
||||
} break;
|
||||
}
|
||||
} break;
|
||||
|
||||
|
@ -24,6 +24,13 @@ DQN_MSVC_WARNING_DISABLE(4244) // warning C4244: 'argument': conversion from 'in
|
||||
#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;
|
||||
@ -123,24 +130,44 @@ int main(int argc, char const *argv[])
|
||||
}
|
||||
|
||||
// NOTE: Get the list of files =================================================================
|
||||
Dqn_List<Dqn_String8> file_list = Dqn_List_Init<Dqn_String8>(scratch.arena, 128);
|
||||
Dqn_List<Dqn_String8> file_list_raw = Dqn_List_Init<Dqn_String8>(scratch.arena, 128);
|
||||
for (Dqn_Win_FolderIterator it = {}; Dqn_Win_FolderIterate(dir, &it); ) {
|
||||
if (Dqn_String8_EndsWithInsensitive(it.file_name, DQN_STRING8(".png"))) {
|
||||
Dqn_String8 *item = Dqn_List_Make(&file_list, Dqn_ZeroMem_Yes);
|
||||
Dqn_String8 *item = Dqn_List_Make(&file_list_raw, Dqn_ZeroMem_Yes);
|
||||
*item = Dqn_FsPath_ConvertF(scratch.arena, "%.*s/%.*s", DQN_STRING_FMT(dir), DQN_STRING_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_STRING_FMT(dir));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// NOTE: Bubble sort the file list naturally ===================================================
|
||||
Dqn_Slice<Dqn_String8> 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_String8 left = file_list.data[list_index + 0];
|
||||
Dqn_String8 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.count, Dqn_ZeroMem_Yes);
|
||||
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.count);
|
||||
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<stbrp_rect> rects = Dqn_SArray_Init<stbrp_rect>(scratch.arena, file_list.count, Dqn_ZeroMem_Yes);
|
||||
for (Dqn_ListIterator<Dqn_String8> it = {}; Dqn_List_Iterate<Dqn_String8>(&file_list, &it, 0);) {
|
||||
Dqn_SArray<stbrp_rect> rects = Dqn_SArray_Init<stbrp_rect>(scratch.arena, file_list.size, Dqn_ZeroMem_Yes);
|
||||
for (Dqn_String8 it : file_list) {
|
||||
int x = 0, y = 0, channels_in_file = 0;
|
||||
stbi_uc *pixels = stbi_load(it.data->data, &x, &y, &channels_in_file, 4 /*desired_channels*/);
|
||||
stbi_uc *pixels = stbi_load(it.data, &x, &y, &channels_in_file, 4 /*desired_channels*/);
|
||||
DQN_ASSERT(pixels);
|
||||
stbi_image_free(pixels);
|
||||
|
||||
@ -150,7 +177,15 @@ int main(int argc, char const *argv[])
|
||||
rect->h = y;
|
||||
|
||||
// NOTE: Enumerate the number of frames for this animation =================================
|
||||
Dqn_String8 anim_prefix = SpriteAnimNameFromFilePath(*it.data);
|
||||
Dqn_String8 anim_prefix = SpriteAnimNameFromFilePath(it);
|
||||
Dqn_String8 file_name = Dqn_String8_FileNameFromPath(it);
|
||||
DQN_ASSERTF(!Dqn_String8_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_STRING_FMT(file_name));
|
||||
|
||||
Dqn_DSMapResult<SpriteSpecification> slot = Dqn_DSMap_FindKeyString8(&sprite_spec_table, anim_prefix);
|
||||
DQN_ASSERTF(slot.found,
|
||||
"\n\nSprite frame loaded from file\n"
|
||||
@ -161,12 +196,12 @@ int main(int argc, char const *argv[])
|
||||
"\n"
|
||||
"Add a line in format of <animation name>;<frames_per_second> to the file, e.g.\n"
|
||||
" %.*s;8\n",
|
||||
DQN_STRING_FMT(*it.data),
|
||||
DQN_STRING_FMT(it),
|
||||
DQN_STRING_FMT(sprite_spec_path),
|
||||
DQN_STRING_FMT(anim_prefix));
|
||||
|
||||
slot.value->frame_count++;
|
||||
Dqn_Log_InfoF("Packing sprite: %.*s", DQN_STRING_FMT(*it.data));
|
||||
Dqn_Log_InfoF("Packing sprite: %.*s", DQN_STRING_FMT(it));
|
||||
}
|
||||
|
||||
// NOTE: Pack the rects ========================================================================
|
||||
@ -174,7 +209,7 @@ int main(int argc, char const *argv[])
|
||||
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.count);
|
||||
file_list.size);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@ -206,11 +241,12 @@ int main(int argc, char const *argv[])
|
||||
|
||||
Dqn_Log_InfoF("Generating meta file: %.*s", DQN_STRING_FMT(meta_path));
|
||||
Dqn_String8 active_anim_prefix = {};
|
||||
for (Dqn_ListIterator<Dqn_String8> it = {}; Dqn_List_Iterate<Dqn_String8>(&file_list, &it, 0);) {
|
||||
DQN_FOR_UINDEX (file_list_index, file_list.size) {
|
||||
Dqn_String8 it = file_list.data[file_list_index];
|
||||
int w, h, channels_in_file;
|
||||
stbi_uc *loaded_image = stbi_load(it.data->data, &w, &h, &channels_in_file, 4 /*desired_channels*/);
|
||||
stbi_uc *loaded_image = stbi_load(it.data, &w, &h, &channels_in_file, 4 /*desired_channels*/);
|
||||
|
||||
stbrp_rect packed_rect = rects.data[it.index];
|
||||
stbrp_rect packed_rect = rects.data[file_list_index];
|
||||
char *src = DQN_CAST(char *)loaded_image;
|
||||
|
||||
// NOTE: Generate the image ================================================================
|
||||
@ -227,7 +263,7 @@ int main(int argc, char const *argv[])
|
||||
stbi_image_free(loaded_image);
|
||||
|
||||
// NOTE: Detect what animation we are currently processing =================================
|
||||
Dqn_String8 anim_prefix = SpriteAnimNameFromFilePath(*it.data);
|
||||
Dqn_String8 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;
|
||||
@ -236,8 +272,9 @@ int main(int argc, char const *argv[])
|
||||
}
|
||||
|
||||
// NOTE: Write the sprite rectangles in ====================================================
|
||||
Dqn_Fs_WriteFileF(&meta_file, "%d;%d;%d;%d", packed_rect.x, packed_rect.y, packed_rect.w, packed_rect.h);
|
||||
if (it.index != (file_list.count - 1))
|
||||
Dqn_String8 file_name = Dqn_String8_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_STRING_FMT(file_name));
|
||||
if (file_list_index != (file_list.size - 1))
|
||||
Dqn_Fs_WriteFileF(&meta_file, "\n");
|
||||
}
|
||||
|
||||
|
BIN
project.rdbg
BIN
project.rdbg
Binary file not shown.
Loading…
Reference in New Issue
Block a user