fp: Fix natural sorting order breaking the sprite packer

This commit is contained in:
doyle 2023-10-08 12:26:36 +11:00
parent 631579d29b
commit b6f589a8dc
9 changed files with 296 additions and 35 deletions

BIN
Data/Textures/atlas.png (Stored with Git LFS)

Binary file not shown.

BIN
Data/Textures/atlas.txt (Stored with Git LFS)

Binary file not shown.

177
External/strnatcmp.c vendored Normal file
View 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
View 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

@ -1 +1 @@
Subproject commit f4ab2fbd147623ac38be6f92fedf0e0049aeafb5
Subproject commit c63954d31b0c99203c5dc752494bbaefd59128f0

View File

@ -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);
@ -2222,16 +2222,22 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
// NOTE: Render entity sprites =============================================================
if (entity->action.sprite.anim) {
FP_GameEntityAction const *action = &entity->action;
TELY_AssetAnimatedSprite const sprite = action->sprite;
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;
@ -2796,8 +2802,8 @@ void FP_Render(FP_Game *game, TELY_Platform *platform, TELY_Renderer *renderer)
phone_icon_rect.pos = Dqn_V2_InitNx2(next_pos.x - phone_icon_rect.size.x * .25f, next_pos.y - (phone_icon_rect.size.y * .35f));
}
Dqn_f32 pixels_per_kb = 15.5f;
Dqn_f32 bar_width = DQN_CAST(Dqn_f32)player->terry_mobile_data_plan_cap / DQN_KILOBYTES(1) * pixels_per_kb;
Dqn_f32 pixels_per_kb = 15.5f;
Dqn_f32 bar_width = DQN_CAST(Dqn_f32)player->terry_mobile_data_plan_cap / DQN_KILOBYTES(1) * pixels_per_kb;
Dqn_f32 bar_x = next_pos.x + (phone_icon_rect.size.x * .25f);
Dqn_f32 data_plan_t = player->terry_mobile_data_plan / DQN_CAST(Dqn_f32)player->terry_mobile_data_plan_cap;

View File

@ -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;

View File

@ -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");
}

Binary file not shown.