I found a way to have more than 300 titles at once. (WIP!)

Duamutef_MC

Well-Known Member
OP
Newcomer
Joined
Mar 15, 2018
Messages
78
Trophies
0
Age
41
XP
198
Country
United Kingdom
...and keep the titles in order!

I'm currently developing a modified version of the 'Wii_Menu_Sort' of Yardape8000 - if successful enough this could allow us to switch from one 300-games list to another one almost seamlessly.

My objective is mainly not lose all the folders/title sorting once a disk is removed and another one is connected.

Let me have your thoughts. It's currently text-based but I'd like it to be graphical instead. Any of you got an example of an easy-to-modify homebrew that has an interface?

Thanks!
 
Last edited by Duamutef_MC,

V10lator

Well-Known Member
Member
Joined
Apr 21, 2019
Messages
2,676
Trophies
1
Age
36
XP
5,657
Country
Germany
Any of you got an example of an easy-to-modify homebrew that has an interface?
Not an example but there are multiple ways you could go:
  • Continue using OSScreen*. It's completely software rendered and has basic functions only but it is possible to build simple GUIs with it.
  • Use https://github.com/wiiu-env/libgui - It's damn fast but also a bit buggy.
  • Use SDL2. There are many how-tos available. Font rendering is damn slow through (might change soon) so you have to deal with this somehow.
  • Utilise GX2 directly. That requires a lot of graphics programming experience so not many deveopers are able to do this and the ones who are are actually libgui and/or SDL2 developers, too.
 

V10lator

Well-Known Member
Member
Joined
Apr 21, 2019
Messages
2,676
Trophies
1
Age
36
XP
5,657
Country
Germany
Just had a look at menu_sort and pretty sure this is outdated and needs to be ported to newer WUT. Basically the whole folders src/common, src/dynamic_libs and src/system need to be removed / beeing replaced with libwut. Also the devoptab it ships needs to be removed and libfats used instead.
Maybe more code changes will be needed untill this correctly links with WUT, libiosuhax and libfat.
 

Duamutef_MC

Well-Known Member
OP
Newcomer
Joined
Mar 15, 2018
Messages
78
Trophies
0
Age
41
XP
198
Country
United Kingdom
Just had a look at menu_sort and pretty sure this is outdated and needs to be ported to newer WUT. Basically the whole folders src/common, src/dynamic_libs and src/system need to be removed / beeing replaced with libwut. Also the devoptab it ships needs to be removed and libfats used instead.
Maybe more code changes will be needed untill this correctly links with WUT, libiosuhax and libfat.

I suspected something to that effect.

But on the other hand I can still compile on Borland C 3.0 from 30 years ago, with the same libraries of 1991.

By the way, my miserable attempt at making a branch of menu_sort is here:

#include <string.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <malloc.h>
#include <math.h>
#include <gctypes.h>
#include <fat.h>
#include <iosuhax.h>
#include <iosuhax_devoptab.h>
#include <iosuhax_disc_interface.h>
#include "dynamic_libs/os_functions.h"
#include "dynamic_libs/fs_functions.h"
#include "dynamic_libs/gx2_functions.h"
#include "dynamic_libs/sys_functions.h"
#include "dynamic_libs/vpad_functions.h"
#include "dynamic_libs/padscore_functions.h"
#include "dynamic_libs/socket_functions.h"
#include "dynamic_libs/ax_functions.h"
#include "dynamic_libs/act_functions.h"
#include "fs/fs_utils.h"
#include "fs/sd_fat_devoptab.h"
#include "system/memory.h"
#include "utils/logger.h"
#include "utils/utils.h"
#include "utils/file.h"
#include "utils/screen.h"
#include "utils/dict.h"
#include "common/common.h"
#include <dirent.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libxml/xpath.h>

#define TITLE_TEXT "Wii U Title Database Switch v0.1.0 - Duamutef_MC"
#define HBL_TITLE_ID 0x13374842
#define MAX_ITEMS_COUNT 300

static const char *systemXmlPath = "storage_slc:/config/system.xml";
static const char *syshaxXmlPath = "storage_slc:/config/syshax.xml";
static const char *cafeXmlPath = "storage_slc:/proc/prefs/cafe.xml";
static const char *gamemapPath = "sd:/wiiu/apps/db_switch/titlesmap";
static const char *backupPath1 = "sd:/wiiu/apps/db_switch/BaristaAccountSaveFile1.dat";
static const char *backupPath2 = "sd:/wiiu/apps/db_switch/BaristaAccountSaveFile2.dat";
static const char *languages[] = {"JA", "EN", "FR", "DE", "IT", "ES", "ZHS", "KO", "NL", "PT", "RU", "ZHT"};
static char languageText[14] = "longname_en";

int backup = 0;
int backup_file = 0;
int restore = 0;
int restore_file = 0;
int count = 0;

struct MenuItemStruct
{
u32 ID;
u32 type;
u32 titleIDPrefix;
char name[65];
};

enum itemTypes
{
MENU_ITEM_NAND = 0x01,
MENU_ITEM_USB = 0x02,
MENU_ITEM_DISC = 0x05,
MENU_ITEM_VWII = 0x09,
MENU_ITEM_FLDR = 0x10
};

void someFunc(void *arg)
{
(void)arg;
}

static int mcp_hook_fd = -1;
int MCPHookOpen()
{
mcp_hook_fd = MCP_Open();
if (mcp_hook_fd < 0)
return -1;
IOS_IoctlAsync(mcp_hook_fd, 0x62, (void *)0, 0, (void *)0, 0, someFunc, (void *)0);

sleep(1);
if (IOSUHAX_Open("/dev/mcp") < 0)
{
MCP_Close(mcp_hook_fd);
mcp_hook_fd = -1;
return -1;
}
return 0;
}

void MCPHookClose()
{
if (mcp_hook_fd < 0)
return;
IOSUHAX_Close();
sleep(1);
MCP_Close(mcp_hook_fd);
mcp_hook_fd = -1;
}

int fsa_read(int fsa_fd, int fd, void *buf, int len)
{
int done = 0;
uint8_t *buf_u8 = (uint8_t *)buf;
while (done < len)
{
size_t read_size = len - done;
int result = IOSUHAX_FSA_ReadFile(fsa_fd, buf_u8 + done, 0x01, read_size, fd, 0);
if (result < 0)
return result;
else
done += result;
}
return done;
}

int smartStrcmp(const char *a, const char *b, const u32 a_id, const u32 b_id)
{
char *ac = malloc(strlen(a) + 1);
char *bc = malloc(strlen(b) + 1);

strcpy(ac, a);
strcpy(bc, b);

int result = strcasecmp(ac, bc);
free(ac);
free(bc);
return result;
}

int fSortCond(const void *c1, const void *c2)
{
return smartStrcmp(((struct MenuItemStruct *)c1)->name,
((struct MenuItemStruct *)c2)->name,
((struct MenuItemStruct *)c1)->ID,
((struct MenuItemStruct *)c2)->ID);
}

void getXMLelement(const char *buff, size_t buffSize, const char *url, const char *elementName, char *text, size_t textSize)
{
text[0] = 0;
xmlDocPtr doc = xmlReadMemory(buff, buffSize, url, "utf-8", 0);
xmlNode *root_element = xmlDocGetRootElement(doc);
xmlNode *cur_node = NULL;
xmlChar *nodeText = NULL;

if (root_element != NULL && root_element->children != NULL)
{
for (cur_node = root_element->children; cur_node; cur_node = cur_node->next)
{
if (cur_node->type == XML_ELEMENT_NODE)
{
if (strcasecmp((const char *)cur_node->name, elementName) == 0)
{
nodeText = xmlNodeListGetString(doc, cur_node->xmlChildrenNode, 1);
if ((nodeText != NULL) && (textSize > 1))
strncpy(text, (const char *)nodeText, textSize - 1);
xmlFree(nodeText);
break;
}
}
}
}
xmlFreeDoc(doc);
}

int getXMLelementInt(const char *buff, size_t buffSize, const char *url, const char *elementName, int base)
{
int ret;
char text[40] = "";
getXMLelement(buff, buffSize, url, elementName, text, 40);
ret = (int)strtol((char *)text, NULL, base);
return ret;
}

int readToBuffer(char **ptr, size_t *bufferSize, const char *path)
{
FILE *fp;
size_t size;
fp = fopen(path, "rb");
if (!fp)
return -1;
fseek(fp, 0L, SEEK_END);
size = ftell(fp);
rewind(fp);
*ptr = malloc(size);
memset(*ptr, 0, size);
fread(*ptr, 1, size, fp);
fclose(fp);
*bufferSize = size;
return 0;
}

void getIDname(u32 id, u32 titleIDPrefix, char *name, size_t nameSize, u32 type)
{
char *xBuffer = NULL;
u32 xSize = 0;
char path[255] = "";
name[0] = 0;
sprintf(path, "storage_%s:/usr/title/%08x/%08x/meta/meta.xml", (type == MENU_ITEM_USB) ? "usb" : "mlc", titleIDPrefix, id);
log_printf("%s\n", path);
if (readToBuffer(&xBuffer, &xSize, path) < 0)
log_printf("Could not open %08x meta.xml\n", id);
else
{
if (xBuffer != NULL)
{
getXMLelement(xBuffer, xSize, "meta.xml", languageText, name, nameSize);
free(xBuffer);
log_printf(" %s\n %s\n", path, name);
}
else
{
log_printf("Memory not allocated for %08x meta.xml\n", id);
}
}
}

/* Entry point */
int Menu_Main(void)
{

InitOSFunctionPointers();
InitSysFunctionPointers();
InitACTFunctionPointers();
InitSocketFunctionPointers();

log_init("192.168.1.16");
log_print("\n\nStarting Main\n");

InitFSFunctionPointers();
InitVPadFunctionPointers();

log_print("Function exports loaded\n");

int fsaFd = -1;
int failed = 1;
char failError[65] = "";
char *fBuffer = NULL;
size_t fSize = 0;
int usb_Connected = 0;
u32 sysmenuId = 0;
u32 cbhcID = 0;
int userPersistentId = 0;

VPADInit();

screenInit();
screenClear();
screenPrint(TITLE_TEXT);
screenPrint("Choose Title Database operation:");
screenPrint("Press 'A' to sort.");
screenPrint("Press 'B' to count elements.");
screenPrint("Press 'L' to backup in savefile #1.");
screenPrint("Press 'R' to backup in savefile #2.");
screenPrint("Press 'ZL' to restore savefile #1.");
screenPrint("Press 'ZR' to restore savefile #2.");

int vpadError;
VPADData vpad;

char modeText[25] = "";
char ignoreTheText[25] = "";

while (1)
{

VPADRead(0, &vpad, 1, &vpadError);
u32 pressedBtns = 0;

if (!vpadError)
pressedBtns = vpad.btns_d | vpad.btns_h;

if (pressedBtns & VPAD_BUTTON_A)
{
strcpy(modeText, "standard sorting");
break;
}

if (pressedBtns & VPAD_BUTTON_B)
{
count = 1;
strcpy(modeText, "count");
break;
}

if (pressedBtns & VPAD_BUTTON_L)
{
backup = 1;
backup_file = 1;
strcpy(modeText, "backup in savefile #1");
strcpy(ignoreTheText, "");
break;
}

if (pressedBtns & VPAD_BUTTON_R)
{
backup = 1;
backup_file = 2;
strcpy(modeText, "backup in savefile #2");
strcpy(ignoreTheText, "");
break;
}

if (pressedBtns & VPAD_BUTTON_ZL)
{
restore = 1;
restore_file = 1;
strcpy(modeText, "restore backup #1");
strcpy(ignoreTheText, "");
break;
}

if (pressedBtns & VPAD_BUTTON_ZR)
{
restore = 1;
restore_file = 2;
strcpy(modeText, "restore backup #2");
strcpy(ignoreTheText, "");
break;
}

usleep(1000);
}

screenClear();
screenSetPrintLine(0);
screenPrint(TITLE_TEXT);
char modeSelectedText[50] = "";
sprintf(modeSelectedText, "Starting %s%s...", modeText, ignoreTheText);
screenPrint(modeSelectedText);
log_printf(modeSelectedText);

uint64_t sysmenuIdUll = _SYSGetSystemApplicationTitleId(0);
if ((sysmenuIdUll & 0xffffffff00000000) == 0x0005001000000000)
sysmenuId = sysmenuIdUll & 0x00000000ffffffff;
else
{
strcpy(failError, "Failed to get Wii U Menu Title!");
goto prgEnd;
}
log_printf("Wii U Menu Title = %08X\n", sysmenuId);

nn_act_initialize();
unsigned char userSlot = nn_act_getslotno();
userPersistentId = nn_act_GetPersistentIdEx(userSlot);
log_printf("User Slot = %d, ID = %08x\n", userSlot, userPersistentId);
nn_act_finalize();

log_printf("Mount partitions\n");

int res = IOSUHAX_Open(NULL);
if (res < 0)
res = MCPHookOpen();
if (res < 0)
{
strcpy(failError, "IOSUHAX_open failed\n");
goto prgEnd;
}
else
{
fatInitDefault();
fsaFd = IOSUHAX_FSA_Open();
if (fsaFd < 0)
{
strcpy(failError, "IOSUHAX_FSA_Open failed\n");
goto prgEnd;
}

if (mount_fs("storage_slc", fsaFd, NULL, "/vol/system") < 0)
{
strcpy(failError, "Failed to mount SLC!");
goto prgEnd;
}
if (mount_fs("storage_mlc", fsaFd, NULL, "/vol/storage_mlc01") < 0)
{
strcpy(failError, "Failed to mount MLC!");
goto prgEnd;
}
res = mount_fs("storage_usb", fsaFd, NULL, "/vol/storage_usb01");
usb_Connected = res >= 0;
}

int language = 0;

if (readToBuffer(&fBuffer, &fSize, cafeXmlPath) < 0)
{
strcpy(failError, "Could not open cafe.xml\n");
goto prgEnd;
}

if (fBuffer != NULL)
{
language = getXMLelementInt(fBuffer, fSize, "cafe.xml", "language", 10);
sprintf(languageText, "longname_%s", languages[language]);
log_printf("cafe.xml size = %d, language = %d %s\n", fSize, language, languages[language]);
free(fBuffer);
}
else
{
strcpy(failError, "Memory not allocated for cafe.xml\n");
goto prgEnd;
}

FILE *fp = fopen(syshaxXmlPath, "rb");
if (fp)
{
fclose(fp);
if (readToBuffer(&fBuffer, &fSize, systemXmlPath) < 0)
{
strcpy(failError, "Could not open system.xml\n");
goto prgEnd;
}
if (fBuffer != NULL)
{
cbhcID = (u32)getXMLelementInt(fBuffer, fSize, "system.xml", "default_title_id", 10);
free(fBuffer);
log_printf("system.xml size = %d, cbhcID = %d\n", fSize, cbhcID);
}
else
{
strcpy(failError, "Memory not allocated for system.xml\n");
goto prgEnd;
}
}

struct MenuItemStruct menuItem[300];
bool folderExists[61] = {false};
bool moveableItem[300];
char baristaPath[255] = "";
folderExists[0] = true;
sprintf(baristaPath, "storage_mlc:/usr/save/00050010/%08x/user/%08x/BaristaAccountSaveFile.dat", sysmenuId, userPersistentId);
log_printf("%s\n", baristaPath);

int itemsCount = 0;

if (backup)
{
if (backup_file == 1){
fcopy(baristaPath, backupPath1);
}
else if (backup_file == 2){
fcopy(baristaPath, backupPath2);
}
}
else if (restore)
{
if (restore_file == 1){
fcopy(backupPath1, baristaPath);
}
else if (restore_file == 2){
fcopy(backupPath2, baristaPath);
}
}
else
{
if (readToBuffer(&fBuffer, &fSize, baristaPath) < 0)
{
strcpy(failError, "Could not open BaristaAccountSaveFile.dat\n");
goto prgEnd;
}
if (fBuffer == NULL)
{
strcpy(failError, "Memory not allocated for BaristaAccountSaveFile.dat\n");
goto prgEnd;
}

log_printf("BaristaAccountSaveFile.dat size = %d\n", fSize);

for (int fNum = 0; fNum <= 60; fNum++)
{
if (!folderExists[fNum])
continue;
log_printf("\nReading - Folder %d\n", fNum);
int currItemNum = 0;
int movableItemsCount = 0;
int maxItemsCount = MAX_ITEMS_COUNT;
int folderOffset = 0;
if (fNum != 0)
{
maxItemsCount = 60;
folderOffset = 0x002D24 + ((fNum - 1) * (60 * 16 * 2 + 56));
}
int usbOffset = maxItemsCount * 16;
for (int i = 0; i < maxItemsCount; i++)
{
moveableItem = true;
int itemOffset = i * 16 + folderOffset;
u32 id = 0;
u32 type = 0;
memcpy(&id, fBuffer + itemOffset + 4, sizeof(u32));
memcpy(&type, fBuffer + itemOffset + 8, sizeof(u32));

if ((id == HBL_TITLE_ID)
|| (cbhcID && (id == cbhcID))
|| (type == MENU_ITEM_DISC)
|| (type == MENU_ITEM_VWII))
{
moveableItem = false;
itemsCount++;
continue;
}

if ((fNum == 0) && (type == MENU_ITEM_FLDR))
{
if ((id > 0) && (id <= 60))
folderExists[id] = true;
moveableItem = false;
continue;
}

if (type == MENU_ITEM_NAND)
{
u32 idH = 0;
memcpy(&idH, fBuffer + itemOffset, sizeof(u32));

if ((idH != 0x00050000) && (idH != 0x00050002) && (idH != 0))
{
moveableItem = false;
continue;
}

if (id == 0)
{
if (!usb_Connected)
continue;
itemOffset += usbOffset;
id = 0;
memcpy(&id, fBuffer + itemOffset + 4, sizeof(u32));
type = fBuffer[itemOffset + 0x0b];
if ((id == 0) || (type != MENU_ITEM_USB))
continue;
}

itemsCount++;

if (!moveableItem)
continue;

memcpy(&menuItem[currItemNum].titleIDPrefix, fBuffer + itemOffset, sizeof(u32));
getIDname(id, menuItem[currItemNum].titleIDPrefix, menuItem[currItemNum].name, 65, type);
menuItem[currItemNum].ID = id;
menuItem[currItemNum].type = type;
currItemNum++;
}
}
movableItemsCount = currItemNum;
log_printf("\nDone reading folders \n");

if (!count)
{
qsort(menuItem, movableItemsCount, sizeof(struct MenuItemStruct), fSortCond);

log_printf("\nNew Order - Folder %d\n", fNum);
currItemNum = 0;
for (int i = 0; i < maxItemsCount; i++)
{
if (!moveableItem)
continue;
int itemOffset = i * 16 + folderOffset;
u32 idNAND = 0;
u32 idNANDh = 0;
u32 idUSB = 0;
u32 idUSBh = 0;
if (currItemNum < movableItemsCount)
{
if (menuItem[currItemNum].type == MENU_ITEM_NAND)
{
idNAND = menuItem[currItemNum].ID;
idNANDh = menuItem[currItemNum].titleIDPrefix;
}
else
{
idUSB = menuItem[currItemNum].ID;
idUSBh = menuItem[currItemNum].titleIDPrefix;
}
log_printf("[%d][%08x] %s\n", i, menuItem[currItemNum].ID, menuItem[currItemNum].name);
currItemNum++;
}

memcpy(fBuffer + itemOffset, &idNANDh, sizeof(u32));
memcpy(fBuffer + itemOffset + 4, &idNAND, sizeof(u32));
memset(fBuffer + itemOffset + 8, 0, 8);
fBuffer[itemOffset + 0x0b] = 1;

itemOffset += usbOffset;

memcpy(fBuffer + itemOffset, &idUSBh, sizeof(u32));
memcpy(fBuffer + itemOffset + 4, &idUSB, sizeof(u32));
memset(fBuffer + itemOffset + 8, 0, 8);
fBuffer[itemOffset + 0x0b] = 2;
}
}
}

if (!count)
{
fp = fopen(baristaPath, "wb");
if (fp)
{
fwrite(fBuffer, 1, fSize, fp);
fclose(fp);
}
else
{
strcpy(failError, "Could not write to BaristaAccountSaveFile.dat\n");
goto prgEnd;
}
}

free(fBuffer);
}

screenPrintAt(strlen(modeSelectedText), screenGetPrintLine(), "done.");

char text[20] = "";
sprintf(text, "User ID: %1x", userPersistentId & 0x0000000f);
screenPrint(text);
if (itemsCount != 0)
{
char countText[21] = "";
sprintf(countText, "Items count: %d/%d", itemsCount, MAX_ITEMS_COUNT);
screenPrint(countText);
}
screenPrint("Press Home to exit");

while (1)
{
VPADRead(0, &vpad, 1, &vpadError);
u32 pressedBtns = 0;

if (!vpadError)
pressedBtns = vpad.btns_d | vpad.btns_h;

if (pressedBtns & VPAD_BUTTON_HOME)
break;

usleep(1000);
}
failed = 0;

prgEnd:
if (failed)
{
screenPrint(failError);
sleep(5);
}
log_printf("Unmount\n");

fatUnmount("sd");
fatUnmount("usb");
IOSUHAX_sdio_disc_interface.shutdown();
IOSUHAX_usb_disc_interface.shutdown();
unmount_fs("storage_slc");
unmount_fs("storage_mlc");
unmount_fs("storage_usb");
IOSUHAX_FSA_Close(fsaFd);
if (mcp_hook_fd >= 0)
MCPHookClose();
else
IOSUHAX_Close();

log_printf("Exiting\n");
log_deinit();

return EXIT_SUCCESS;
}
 

Duamutef_MC

Well-Known Member
OP
Newcomer
Joined
Mar 15, 2018
Messages
78
Trophies
0
Age
41
XP
198
Country
United Kingdom
One would think compiling this 'babby's first code' thing would take an instant. Alas, I've been trying for 2 days!

HELLO WORLD my ***!
 

V10lator

Well-Known Member
Member
Joined
Apr 21, 2019
Messages
2,676
Trophies
1
Age
36
XP
5,657
Country
Germany
But on the other hand I can still compile on Borland C 3.0 from 30 years ago, with the same libraries of 1991
The point here is "with the same libraries". So you need to use versions of the libraries (at least libwut, libiouhax and libfat) used when the code was written. That means you most likely need whole DevkitPro from that time.

That's a bad way through as newer library versions contain improvements and bugfixes, so normally you want to use the latest versions.

//EDIT: BTW: With modern WUT there's no real need for libiosuhax/libfat anyway as libwut mounts the SD card with CafeOS API calls only for you.
 
Last edited by V10lator,

MikaDubbz

Well-Known Member
Member
Joined
Dec 12, 2017
Messages
3,880
Trophies
1
Age
36
XP
7,377
Country
United States
Wouldn’t it be easier to make a homebrew that displays all installed titles and lets you select which one to launch? It could be done with a few lines of code, I could make it if enought people want it
I think a big part of it, is maintaining the actual Wii U menu. A giant list of titles is a little less immersive, like that just feels like you're using a computer and nothing special, keeping the Wii U menu's look and functionality while still showcasing many titles just feels more satisfying, like yeah, this is a Nintendo Wii U, and a beefy one at that.

I get that's pretty dumb in the big scheme of things, but I do know that I'm gonna feel more invited to play on a standard Wii U menu than a blank screen with a long list of games. Hell, I'd prefer to use Loadiine over just a long list of titles, and that has unfortunate side effect of taking quite awhile to load a single title.
 

Duamutef_MC

Well-Known Member
OP
Newcomer
Joined
Mar 15, 2018
Messages
78
Trophies
0
Age
41
XP
198
Country
United Kingdom
I think a big part of it, is maintaining the actual Wii U menu. A giant list of titles is a little less immersive, like that just feels like you're using a computer and nothing special, keeping the Wii U menu's look and functionality while still showcasing many titles just feels more satisfying, like yeah, this is a Nintendo Wii U, and a beefy one at that.

I get that's pretty dumb in the big scheme of things, but I do know that I'm gonna feel more invited to play on a standard Wii U menu than a blank screen with a long list of games. Hell, I'd prefer to use Loadiine over just a long list of titles, and that has unfortunate side effect of taking quite awhile to load a single title.
EXACTLY my point. That's why I'm trying everything to keep the original Wii U Menu.
 

viewmonger

Member
Newcomer
Joined
May 7, 2021
Messages
6
Trophies
0
XP
134
Country
United States
Not sure if you've ever looked at it, but there is a solution to the 300 title limit for 3DS called 3DSBank that basically loads into a new environment folder to give you a new set of 300 titles. It would be great to have a version for the Wii U.

Would be great to have a solution to the 300 title limit, especially with all the Virtual Console titles
 

Evilengine

Well-Known Member
Member
Joined
Jan 30, 2008
Messages
249
Trophies
1
XP
1,147
Country
Gambia, The
Wouldn’t it be easier to make a homebrew that displays all installed titles and lets you select which one to launch? It could be done with a few lines of code, I could make it if enought people want it
Definitely a good idea! At least something simple, that works. Would appreciate.
 

Site & Scene News

Popular threads in this forum

General chit-chat
Help Users
    BigOnYa @ BigOnYa: We all should have free health, dental, vision care, and make the college kids pay for the...