Badge dump overhaul

This commit is contained in:
2024-06-12 18:01:25 -04:00
parent 4669f13d05
commit c3e09adcb2
10 changed files with 262 additions and 19 deletions

View File

@@ -37,6 +37,6 @@
#define BADGE_MNG_SIZE 0xD4A8 #define BADGE_MNG_SIZE 0xD4A8
Result install_badges(void); Result install_badges(void);
Result dump_badge_extdata(void); Result extract_badges(void);
#endif #endif

View File

@@ -6,5 +6,6 @@
size_t bin_to_abgr(char ** bufp, size_t size); size_t bin_to_abgr(char ** bufp, size_t size);
size_t png_to_abgr(char ** bufp, size_t size, u32 *height); size_t png_to_abgr(char ** bufp, size_t size, u32 *height);
int pngToRGB565(char *png_buf, u64 fileSize, u16 *rgb_buf_64x64, u8 *alpha_buf_64x64, u16 *rgb_buf_32x32, u8 *alpha_buf_32x32, bool set_icon); int pngToRGB565(char *png_buf, u64 fileSize, u16 *rgb_buf_64x64, u8 *alpha_buf_64x64, u16 *rgb_buf_32x32, u8 *alpha_buf_32x32, bool set_icon);
int rgb565ToPngFile(char *filename, u16 *rgb_buf, u8 *alpha_buf, int width, int height);
#endif #endif

View File

@@ -58,6 +58,7 @@ typedef enum InstallType_e {
INSTALL_DUMPING_THEME, INSTALL_DUMPING_THEME,
INSTALL_DUMPING_ALL_THEMES, INSTALL_DUMPING_ALL_THEMES,
INSTALL_DUMPING_BADGES,
INSTALL_BADGES, INSTALL_BADGES,
INSTALL_NONE, INSTALL_NONE,
@@ -89,6 +90,7 @@ typedef enum {
TEXT_INSTALL_DUMPING_THEME, TEXT_INSTALL_DUMPING_THEME,
TEXT_INSTALL_DUMPING_ALL_THEMES, TEXT_INSTALL_DUMPING_ALL_THEMES,
TEXT_INSTALL_DUMPING_BADGES,
TEXT_INSTALL_BADGES, TEXT_INSTALL_BADGES,
// Other text // Other text

View File

@@ -79,6 +79,7 @@ typedef struct {
const char *download_bgm; const char *download_bgm;
const char *dump_single; const char *dump_single;
const char *dump_all_official; const char *dump_all_official;
const char *dump_badges;
const char *install_badges; const char *install_badges;
float start_pos; float start_pos;
const char *shuffle; const char *shuffle;

View File

@@ -264,7 +264,7 @@ int install_badge_dir(FS_DirectoryEntry set_dir, int *badge_count, int set_id)
u32 total_count = 0xFFFF * badges_in_set; u32 total_count = 0xFFFF * badges_in_set;
for (int i = 0; i < 16; ++i) for (int i = 0; i < 16; ++i)
{ {
FSFILE_Write(badgeDataHandle, NULL, set_index * 0x8A0 + i * 0x8A, set_dir.name, strulen(set_dir.name, 0x45) * 2, 0); FSFILE_Write(badgeDataHandle, NULL, set_index * 0x8A0 + i * 0x8A, set_dir.name, strulen(set_dir.name, 0x45) * 2 + 2, 0);
} }
badgeMngBuffer[0x3D8 + set_index/8] |= 0 << (set_index % 8); badgeMngBuffer[0x3D8 + set_index/8] |= 0 << (set_index % 8);
@@ -361,6 +361,163 @@ Result backup_badges(void)
return 0; return 0;
} }
typedef struct set_node_s {
u32 set_id;
u32 set_index;
struct set_node_s *next;
} SetNode;
u32 get_set_index(SetNode *head, u32 set_id)
{
SetNode *cursor = head;
while (cursor != NULL)
{
if (cursor->set_id == set_id)
return cursor->set_index;
cursor = cursor->next;
}
return 0xFFFFFFFF;
}
void free_list(SetNode *head)
{
SetNode *cursor = head;
while (cursor)
{
SetNode *next = cursor->next;
free(cursor);
cursor = next;
}
}
SetNode * extract_sets(char *badgeMngBuffer, Handle backupDataHandle)
{
u32 setCount = *((u32 *) (badgeMngBuffer + 0x4));
if (setCount == 0) return NULL;
SetNode *head = calloc(1, sizeof(SetNode));
SetNode *cursor = head;
u16 *icon_rgb_buf = malloc(64 * 64 * 2);
u8 *icon_alpha_buf = malloc(64 * 64 * 0.5);
for (u32 i = 0; i < setCount; ++i)
{
memcpy(&(cursor->set_id), &badgeMngBuffer[0xA028 + 0x30 * i + 0x10], 4);
memcpy(&(cursor->set_index), &badgeMngBuffer[0xA028 + 0x30 * i + 0x14], 4);
if (cursor->set_id != 0xEFBE) // 0xEFBE is GYTB Set ID; GYTB doesn't properly create sets, so skip
{
u16 utf16SetName[0x46] = {0};
FSFILE_Read(backupDataHandle, NULL, cursor->set_index * 16 * 0x8A, utf16SetName, 0x8A);
u16 set_path[256] = {0};
struacat(set_path, "/3ds/" APP_TITLE "/BadgeBackups/");
strucat(set_path, utf16SetName);
FSUSER_CreateDirectory(ArchiveSD, fsMakePath(PATH_UTF16, set_path), FS_ATTRIBUTE_DIRECTORY);
memset(icon_alpha_buf, 255, 64 * 64 * 0.5);
FSFILE_Read(backupDataHandle, NULL, 0x250F80 + cursor->set_index * 0x2000, icon_rgb_buf, 0x2000);
char filename[256] = {0};
utf16_to_utf8((u8 *) filename, set_path, 256);
strcat(filename, "/_seticon.png");
rgb565ToPngFile(filename, icon_rgb_buf, icon_alpha_buf, 48, 48);
}
cursor->next = calloc(1, sizeof(SetNode));
cursor = cursor->next;
}
free(icon_rgb_buf);
free(icon_alpha_buf);
return head;
}
Result extract_badges(void)
{
DEBUG("Dumping installed badges...\n");
char *badgeMngBuffer = malloc(BADGE_MNG_SIZE);
u32 size = file_to_buf(fsMakePath(PATH_ASCII, "/BadgeMngFile.dat"), ArchiveBadgeExt, &badgeMngBuffer);
DEBUG("%lu bytes read\n", size);
Result res = 0;
SetNode *head;
Handle backupDataHandle;
u16 *badge_rgb_buf = malloc(64 * 64 * 2);
u8 *badge_alpha_buf = malloc(64 * 64 * 0.5);
u32 badge_count = 0;
memcpy(&badge_count, badgeMngBuffer + 0x8, 4);
DEBUG("%lu badges found\n", badge_count);
if (badge_count > 0)
{
res = FSUSER_OpenFile(&backupDataHandle, ArchiveBadgeExt, fsMakePath(PATH_ASCII, "/BadgeData.dat"), FS_OPEN_READ, 0);
if (R_FAILED(res))
{
throw_error(language.badges.extdata_locked, ERROR_LEVEL_WARNING);
DEBUG("backupDataHandle open failed\n");
return -1;
}
head = extract_sets(badgeMngBuffer, backupDataHandle);
} else {
return 0;
}
for (u32 i = 0; i < badge_count; ++i)
{
u32 badgeId;
memcpy(&badgeId, badgeMngBuffer + 0x3E8 + i * 0x28 + 0x4, 4);
u32 badgeSetId;
memcpy(&badgeSetId, badgeMngBuffer + 0x3E8 + i * 0x28 + 0x8, 4);
u16 badgeSubId;
memcpy(&badgeSubId, badgeMngBuffer + 0x3E8 + i * 0x28 + 0xE, 2);
u32 shortcut;
memcpy(&shortcut, badgeMngBuffer + 0x3E8 + i * 0x28 + 0x18, 4);
char filename[512] = {0};
u16 utf16Name[0x46] = {0};
FSFILE_Read(backupDataHandle, NULL, 0x35E80 + i * 16 * 0x8A, utf16Name, 0x8A);
char utf8Name[256] = {0};
res = utf16_to_utf8((u8 *) utf8Name, utf16Name, 256);
char dir[256] = "/3ds/" APP_TITLE "/BadgeBackups/";
if (badgeSetId != 0xEFBE) // 0xEFBE is GYTB Set ID; GYTB doesn't properly create sets, so skip
{
u32 set_index = get_set_index(head, badgeSetId);
if (set_index == 0xFFFFFFFF) {
sprintf(dir, "/3ds" APP_TITLE "/BadgeBackups/Unknown Set");
} else
{
u16 utf16SetName[0x46] = {0};
FSFILE_Read(backupDataHandle, NULL, set_index * 16 * 0x8A, utf16SetName, 0x8A);
char utf8SetName[128] = {0};
res = utf16_to_utf8((u8 *) utf8SetName, utf16SetName, 128);
DEBUG("UTF-8 Set Name: %s; ID: %lx\n", utf8SetName, badgeSetId);
sprintf(dir, "/3ds/" APP_TITLE "/BadgeBackups/%s", utf8SetName);
}
}
if (shortcut == 0xFFFFFFFF)
{
sprintf(filename, "%s/%s.%lx.%x.png", dir, utf8Name, badgeId, badgeSubId);
} else
{
sprintf(filename, "%s/%s.%08lx.%lx.%x.png", dir, utf8Name, shortcut, badgeId, badgeSubId);
}
DEBUG("Dump filename: %s\n", filename);
FSFILE_Read(backupDataHandle, NULL, 0x318F80 + i * 0x2800, badge_rgb_buf, 0x2000);
FSFILE_Read(backupDataHandle, NULL, 0x318F80 + i * 0x2800 + 0x2000, badge_alpha_buf, 0x800);
rgb565ToPngFile(filename, badge_rgb_buf, badge_alpha_buf, 64, 64);
draw_loading_bar(i + 1, badge_count, INSTALL_DUMPING_BADGES);
}
free(badgeMngBuffer);
free(badge_rgb_buf);
free(badge_alpha_buf);
free_list(head);
return res;
}
Result install_badges(void) Result install_badges(void)
{ {
Handle handle = 0; Handle handle = 0;
@@ -368,14 +525,6 @@ Result install_badges(void)
Result res = 0; Result res = 0;
draw_loading_bar(0, 1, INSTALL_BADGES); draw_loading_bar(0, 1, INSTALL_BADGES);
DEBUG("Backing up badges\n");
res = backup_badges();
if (R_FAILED(res))
{
DEBUG("Backup badges failed\n");
return res;
}
DEBUG("Initializing ACT\n"); DEBUG("Initializing ACT\n");
res = actInit(); res = actInit();
if (R_FAILED(res)) if (R_FAILED(res))

View File

@@ -3,6 +3,66 @@
#include <png.h> #include <png.h>
// don't be fooled - this function always expects 64x64 input buffers. Width/height only
// control output resolution
int rgb565ToPngFile(char *filename, u16 *rgb_buf, u8 *alpha_buf, int width, int height)
{
FILE *fp = fopen(filename, "wb");
u8 *image = malloc(64 * 64 * 4);
if (!image) return -1;
int i, x, y, r, g, b, a;
for (i = 0; i < 64 * 64; ++i)
{
r = (rgb_buf[i] & 0xF800) >> 11;
g = (rgb_buf[i] & 0x07E0) >> 5;
b = (rgb_buf[i] & 0x001F);
a = (alpha_buf[i/2] >> (4*(i%2))) & 0x0F;
r = round(r * 255.0 / 31.0);
g = round(g * 255.0 / 63.0);
b = round(b * 255.0 / 31.0);
a = a * 0x11;
x = 8*((i/64)%8) + (((i%64)&0x01) >> 0) + (((i%64)&0x04) >> 1) + (((i%64)&0x10) >> 2);
y = 8*(i/512) + (((i%64)&0x02) >> 1) + (((i%64)&0x08) >> 2) + (((i%64)&0x20) >> 3);
image[y * 64 * 4 + x * 4 + 0] = r;
image[y * 64 * 4 + x * 4 + 1] = g;
image[y * 64 * 4 + x * 4 + 2] = b;
image[y * 64 * 4 + x * 4 + 3] = a;
}
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
png_infop info = png_create_info_struct(png);
setjmp(png_jmpbuf(png));
png_init_io(png, fp);
png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, info);
png_bytep row = malloc(4 * width * sizeof(png_byte));
for (y = 0; y < height; ++y)
{
for (x = 0; x < width; ++x)
{
row[x * 4 + 0] = image[(y * 64 + x) * 4 + 0];
row[x * 4 + 1] = image[(y * 64 + x) * 4 + 1];
row[x * 4 + 2] = image[(y * 64 + x) * 4 + 2];
row[x * 4 + 3] = image[(y * 64 + x) * 4 + 3];
}
png_write_row(png, row);
}
png_write_end(png, info);
png_free_data(png, info, PNG_FREE_ALL, -1);
png_destroy_write_struct(&png, NULL);
free(row);
free(image);
fflush(fp);
fclose(fp);
return 0;
}
int pngToRGB565(char *png_buf, u64 fileSize, u16 *rgb_buf_64x64, u8 *alpha_buf_64x64, u16 *rgb_buf_32x32, u8 *alpha_buf_32x32, bool set_icon) int pngToRGB565(char *png_buf, u64 fileSize, u16 *rgb_buf_64x64, u8 *alpha_buf_64x64, u16 *rgb_buf_32x32, u8 *alpha_buf_32x32, bool set_icon)
{ {
if (png_buf == NULL) return 0; if (png_buf == NULL) return 0;

View File

@@ -136,6 +136,7 @@ void init_screens(void)
C2D_TextParse(&text[TEXT_INSTALL_LOADING_REMOTE_BGM], staticBuf, language.draw.download_bgm); C2D_TextParse(&text[TEXT_INSTALL_LOADING_REMOTE_BGM], staticBuf, language.draw.download_bgm);
C2D_TextParse(&text[TEXT_INSTALL_DUMPING_THEME], staticBuf, language.draw.dump_single); C2D_TextParse(&text[TEXT_INSTALL_DUMPING_THEME], staticBuf, language.draw.dump_single);
C2D_TextParse(&text[TEXT_INSTALL_DUMPING_ALL_THEMES], staticBuf, language.draw.dump_all_official); C2D_TextParse(&text[TEXT_INSTALL_DUMPING_ALL_THEMES], staticBuf, language.draw.dump_all_official);
C2D_TextParse(&text[TEXT_INSTALL_DUMPING_BADGES], staticBuf, language.draw.dump_badges);
C2D_TextParse(&text[TEXT_INSTALL_BADGES], staticBuf, language.draw.install_badges); C2D_TextParse(&text[TEXT_INSTALL_BADGES], staticBuf, language.draw.install_badges);
for(int i = 0; i < TEXT_AMOUNT; i++) for(int i = 0; i < TEXT_AMOUNT; i++)

View File

@@ -112,6 +112,8 @@ Result open_archives(void)
FSUSER_CreateDirectory(ArchiveSD, fsMakePath(PATH_ASCII, "/3ds"), FS_ATTRIBUTE_DIRECTORY); FSUSER_CreateDirectory(ArchiveSD, fsMakePath(PATH_ASCII, "/3ds"), FS_ATTRIBUTE_DIRECTORY);
FSUSER_CreateDirectory(ArchiveSD, fsMakePath(PATH_ASCII, "/3ds/" APP_TITLE), FS_ATTRIBUTE_DIRECTORY); FSUSER_CreateDirectory(ArchiveSD, fsMakePath(PATH_ASCII, "/3ds/" APP_TITLE), FS_ATTRIBUTE_DIRECTORY);
FSUSER_CreateDirectory(ArchiveSD, fsMakePath(PATH_ASCII, "/3ds/" APP_TITLE "/cache"), FS_ATTRIBUTE_DIRECTORY); FSUSER_CreateDirectory(ArchiveSD, fsMakePath(PATH_ASCII, "/3ds/" APP_TITLE "/cache"), FS_ATTRIBUTE_DIRECTORY);
FSUSER_CreateDirectory(ArchiveSD, fsMakePath(PATH_ASCII, "/3ds/" APP_TITLE "/BadgeBackups"), FS_ATTRIBUTE_DIRECTORY);
FSUSER_CreateDirectory(ArchiveSD, fsMakePath(PATH_ASCII, "/3ds/" APP_TITLE "/BadgeBackups/Unknown Set"), FS_ATTRIBUTE_DIRECTORY);
u32 homeMenuPath[3] = {MEDIATYPE_SD, archive2, 0}; u32 homeMenuPath[3] = {MEDIATYPE_SD, archive2, 0};
home.type = PATH_BINARY; home.type = PATH_BINARY;
@@ -158,16 +160,27 @@ Result open_badge_extdata()
} }
} }
if (R_FAILED(res = FSUSER_OpenFile(&test_handle, ArchiveBadgeExt, fsMakePath(PATH_ASCII, "BadgeData.dat"), FS_OPEN_READ, 0))) if (R_FAILED(res = FSUSER_OpenFile(&test_handle, ArchiveBadgeExt, fsMakePath(PATH_ASCII, "/BadgeData.dat"), FS_OPEN_READ, 0)))
{ {
if (R_SUMMARY(res) == RS_NOTFOUND)
{
FSUSER_CreateFile(ArchiveBadgeExt, fsMakePath(PATH_ASCII, "/BadgeData.dat"), 0, BADGE_DATA_SIZE); FSUSER_CreateFile(ArchiveBadgeExt, fsMakePath(PATH_ASCII, "/BadgeData.dat"), 0, BADGE_DATA_SIZE);
FSUSER_OpenFile(&test_handle, ArchiveBadgeExt, fsMakePath(PATH_ASCII, "/BadgeData.dat"), FS_OPEN_WRITE, 0); FSUSER_OpenFile(&test_handle, ArchiveBadgeExt, fsMakePath(PATH_ASCII, "/BadgeData.dat"), FS_OPEN_WRITE, 0);
FSFILE_Flush(test_handle); FSFILE_Flush(test_handle);
}
DEBUG("Error 0x%08ld opening BadgeData.dat, retrying\n", res);
} }
FSFILE_Close(test_handle); FSFILE_Close(test_handle);
if(R_FAILED(res = FSUSER_OpenFile(&test_handle, ArchiveBadgeExt, fsMakePath(PATH_ASCII, "BadgeMngFile.dat"), FS_OPEN_READ, 0))) if(R_FAILED(res = FSUSER_OpenFile(&test_handle, ArchiveBadgeExt, fsMakePath(PATH_ASCII, "/BadgeMngFile.dat"), FS_OPEN_READ, 0)))
remake_file(fsMakePath(PATH_ASCII, "/BadgeMngFile.dat"), ArchiveBadgeExt, BADGE_MNG_SIZE); {
DEBUG("Error 0x%08ld opening BadgeMngFile.dat, retrying\n", res);
if (R_SUMMARY(res) == RS_NOTFOUND)
{
remake_file(fsMakePath(PATH_ASCII, "/BadgeMngFile.dat"), ArchiveBadgeExt, BADGE_MNG_SIZE);
}
}
FSFILE_Close(test_handle);
if(R_FAILED(res = FSUSER_OpenFile(&test_handle, ArchiveSD, fsMakePath(PATH_ASCII, "/Badges/ThemePlaza Badges/_seticon.png"), FS_OPEN_READ, 0))) if(R_FAILED(res = FSUSER_OpenFile(&test_handle, ArchiveSD, fsMakePath(PATH_ASCII, "/Badges/ThemePlaza Badges/_seticon.png"), FS_OPEN_READ, 0)))
{ {
@@ -214,7 +227,11 @@ u32 file_to_buf(FS_Path path, FS_Archive archive, char ** buf)
{ {
Handle file; Handle file;
Result res = 0; Result res = 0;
if (R_FAILED(res = FSUSER_OpenFile(&file, archive, path, FS_OPEN_READ, 0))) return 0; if (R_FAILED(res = FSUSER_OpenFile(&file, archive, path, FS_OPEN_READ, 0)))
{
DEBUG("file_to_buf failed - 0x%08ld\n", res);
return 0;
}
u64 size; u64 size;
FSFILE_GetSize(file, &size); FSFILE_GetSize(file, &size);

View File

@@ -906,7 +906,15 @@ int main(void)
draw_mode = DRAW_MODE_LIST; draw_mode = DRAW_MODE_LIST;
extra_index = 1; extra_index = 1;
} }
else if(kDown &KEY_B) else if(kDown & KEY_DLEFT)
{
draw_install(INSTALL_DUMPING_BADGES);
extract_badges();
extra_mode = false;
draw_mode = DRAW_MODE_LIST;
extra_index = 1;
}
else if(kDown & KEY_B)
{ {
extra_index = 1; extra_index = 1;
} }

View File

@@ -149,7 +149,7 @@ const Language_s language_english = {
"\uE07A Dump All Themes" "\uE07A Dump All Themes"
}, },
{ {
NULL, "\uE07B Dump Badges",
NULL NULL
}, },
{ {
@@ -213,6 +213,7 @@ const Language_s language_english = {
.download_bgm = "Downloading BGM, please wait...", .download_bgm = "Downloading BGM, please wait...",
.dump_single = "Dumping theme, please wait...", .dump_single = "Dumping theme, please wait...",
.dump_all_official = "Dumping official themes, please wait...", .dump_all_official = "Dumping official themes, please wait...",
.dump_badges = "Dumping installed badges, please wait...",
.install_badges = "Installing badges, please wait...", .install_badges = "Installing badges, please wait...",
.shuffle = "Shuffle: %i/10", .shuffle = "Shuffle: %i/10",
}, },
@@ -554,7 +555,7 @@ const Language_s language_spanish = {
"\uE07A Volcar todos los temas" "\uE07A Volcar todos los temas"
}, },
{ {
NULL, "\uE07B Dump Badges",
NULL NULL
}, },
{ {
@@ -618,6 +619,7 @@ const Language_s language_spanish = {
.download_bgm = "Descargando BGM, por favor espera...", .download_bgm = "Descargando BGM, por favor espera...",
.dump_single = "Volcando tema, por favor espera...", .dump_single = "Volcando tema, por favor espera...",
.dump_all_official = "Volcando temas oficiales, por favor espera...", .dump_all_official = "Volcando temas oficiales, por favor espera...",
.dump_badges = "Dumping installed badges, please wait...",
.install_badges = "Instalando insignias, por favor espera...", .install_badges = "Instalando insignias, por favor espera...",
.shuffle = "Aleatorio: %i/10", .shuffle = "Aleatorio: %i/10",
}, },
@@ -960,7 +962,7 @@ const Language_s language_french = {
"\uE07A Dump tous les thèmes" "\uE07A Dump tous les thèmes"
}, },
{ {
NULL, "\uE07B Dump Badges",
NULL NULL
}, },
{ {
@@ -1024,6 +1026,7 @@ const Language_s language_french = {
.download_bgm = "Téléchargement de la musique,\nveuillez patienter...", .download_bgm = "Téléchargement de la musique,\nveuillez patienter...",
.dump_single = "Extraction du thème installé,\nveuillez patienter...", .dump_single = "Extraction du thème installé,\nveuillez patienter...",
.dump_all_official = "Extraction des thèmes officiels,\nveuillez patienter...", .dump_all_official = "Extraction des thèmes officiels,\nveuillez patienter...",
.dump_badges = "Dumping installed badges, please wait...",
.install_badges = "Installation des badges,\nveuillez patienter...", .install_badges = "Installation des badges,\nveuillez patienter...",
.shuffle = "Aléatoire: %i/10", .shuffle = "Aléatoire: %i/10",
}, },
@@ -1365,7 +1368,7 @@ const Language_s language_portuguese = {
"\uE07A Exportar Todos Temas" "\uE07A Exportar Todos Temas"
}, },
{ {
NULL, "\uE07B Exportar Insígnias",
NULL NULL
}, },
{ {
@@ -1429,6 +1432,7 @@ const Language_s language_portuguese = {
.download_bgm = "Baixando BGM, aguarde...", .download_bgm = "Baixando BGM, aguarde...",
.dump_single = "Exportando tema, aguarde...", .dump_single = "Exportando tema, aguarde...",
.dump_all_official = "Exportando temas oficiais, aguarde...", .dump_all_official = "Exportando temas oficiais, aguarde...",
.dump_badges = "Dumping installed badges, please wait...",
.install_badges = "Instalando insígnias, aguarde...", .install_badges = "Instalando insígnias, aguarde...",
.shuffle = "Shuffle: %i/10", .shuffle = "Shuffle: %i/10",
}, },