Skip to content

Commit ccd1d7a

Browse files
feat(favourites): VCD favourites -- Favourites tab gets its own L3 ISO/VCD view
PS1/.VCD games can now be favourited and launched from the Favourites tab. The tab gains its own L3 toggle (like every device page) that swaps between disc favourites and PS1 favourites; favouriting a game while a device is in its VCD view records a VCD favourite that resolves, renders, and launches as POPSTARTER independent of the source device's current view. Format: favourites.bin OFAV bumped v1->v2 with a per-record isVcd byte. v1 files still load (every record isVcd=0) and the uOPL/wOPL import (no OFAV magic) sets isVcd=0, so no user loses favourites on upgrade; the first write upgrades in place. Resolution is by NAME (stored text == .VCD basename), not by a submenu id -- the source device may be in ISO view (its games array holds discs, not VCDs) yet the favourite must still surface. favResolve binds a VCD fav only to a loaded device that provides the new itemLaunchVcd callback; favGetImage/favGetConfig drive the VCD art (vcdLoadArt) + PS1 config (sbPopulateConfig) off the owner's itemGetPrefix; favLaunchItem dispatches to itemLaunchVcd. bdm/eth/mmce implement it by refactoring their existing in-view VCD launch branch into a launch-by-name helper (no behaviour change to the in-view path; reuses the exact, freeze-tested device code). FAV joins vcdModeSupported; favUpdateItemList filters by vcdViewActive(FAV_MODE); the menusys render reorder puts the VCD view ahead of the FAV branch so VCD favourites render with the PS1 theme family. The APA HDD (HDD_MODE) is deferred: its __.POPS partition model isn't name-resolvable here, so it has no itemLaunchVcd and R3 on an HDD VCD item is an honest no-op rather than storing a permanently-hidden record. The common internal exFAT HDD is a BDM massN: device and is fully covered. Adversarially verified (16-agent workflow: GO, 0 blocker / 0 major); the 4 confirmed minors/nits are fixed here -- source-page star re-mark, the HDD dead-end guard, favGetImage isRelative parity, and an unconditional FAV dirty-flag consume. REMAINING: HW validation (BDM/MMCE/SMB VCD favourite capture + launch); APA-HDD VCD favourites as a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a10c841 commit ccd1d7a

9 files changed

Lines changed: 270 additions & 110 deletions

File tree

include/favsupport.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#define FAV_TEXT_MAX 256 // cap on a stored favourite's text length (incl. NUL)
88
#define FAV_MAX_ITEMS 512 // cap on records read from favourites.bin
99
#define FAV_MAGIC 0x4641464F // 'OFAV' (little-endian on the EE)
10-
#define FAV_VERSION 1
10+
#define FAV_VERSION 2 // v2 adds a per-record isVcd byte; v1 is still read (isVcd=0)
1111

1212
// Defined in favsupport.c; consumed by opl.c/gui.c (config load/save/default).
1313
extern int gFAVStartMode;
@@ -24,9 +24,11 @@ int favGetItemSourceMode(int id);
2424

2525
// R3-toggle helpers (called from opl.c). add/remove rewrite favourites.bin and return 1 on a
2626
// successful write, 0 on failure (so the caller won't set a lying star). add returns 1 if the
27-
// item is already present. removeFavouriteByIdAndText matches mode (BDM-lenient) + id + text.
28-
int addFavouriteItem(int mode, int id, int icon_id, int text_id, const char *text);
29-
int removeFavouriteByIdAndText(int mode, int id, const char *text);
27+
// item is already present. removeFavouriteByIdAndText matches mode (BDM-lenient) + id + text + isVcd.
28+
// isVcd = 1 when the favourited item was a PS1/.VCD entry (device was in its L3 VCD view); such
29+
// favourites resolve + launch as POPSTARTER rather than as a disc image. See favsupport.c.
30+
int addFavouriteItem(int mode, int id, int icon_id, int text_id, const char *text, int isVcd);
31+
int removeFavouriteByIdAndText(int mode, int id, const char *text, int isVcd);
3032

3133
// Remove the favourite at FAV-list index favIndex (R3 pressed on the Favourites tab).
3234
void favRemoveByIndex(int favIndex);

include/iosupport.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,13 @@ typedef struct _item_list_t
154154
int (*itemCheckVMC)(item_list_t *itemList, char *name, int createSize);
155155

156156
int (*itemIconId)(item_list_t *itemList);
157+
158+
/// Launch a PS1/.VCD entry BY NAME via POPSTARTER, independent of the device's current ISO/VCD
159+
/// view. NULL for devices without a VCD view (and for the APA HDD, whose __.POPS partition model
160+
/// is not name-resolvable here). Used by the Favourites tab to launch a VCD favourite while its
161+
/// source device page may be in ISO view -- the id-based itemLaunch's VCD branch is gated on the
162+
/// device being live in VCD view, which the Favourites tab cannot guarantee.
163+
void (*itemLaunchVcd)(item_list_t *itemList, const char *vcdName, config_set_t *configSet);
157164
} item_list_t;
158165

159166
#endif

src/bdmsupport.c

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,26 @@ static void bdmRenameGame(item_list_t *itemList, int id, char *newName)
625625
pDeviceData->ForceRefresh = 1;
626626
}
627627

628+
// Launch a PS1/.VCD entry BY NAME via POPSTARTER (the view-independent entry point used by both the
629+
// in-view menu launch below and the Favourites tab). Every BDM device is a massN: mount at runtime,
630+
// so the ELF + selector both use the live prefix; UNMOUNT_EXCEPTION keeps it mounted across the reset.
631+
static void bdmLaunchVcd(item_list_t *itemList, const char *vcdName, config_set_t *configSet)
632+
{
633+
bdm_device_data_t *pDeviceData = (bdm_device_data_t *)itemList->priv;
634+
char vcdPrefix[64], vcdElf[256], vcdSelector[320];
635+
636+
if (pDeviceData == NULL || vcdName == NULL || vcdName[0] == '\0' || !strcmp(vcdName, "POPSTARTER"))
637+
return;
638+
snprintf(vcdPrefix, sizeof(vcdPrefix), "%s", pDeviceData->bdmPrefix);
639+
if (!vcdResolvePopstarter(vcdPrefix, vcdElf, sizeof(vcdElf))) {
640+
guiMsgBox(_l(_STR_POPSTARTER_NOT_FOUND), 0, NULL);
641+
return;
642+
}
643+
vcdBuildSelector(vcdPrefix, VCD_PREFIX_MASS, vcdName, vcdSelector, sizeof(vcdSelector));
644+
deinit(UNMOUNT_EXCEPTION, itemList->mode); // keep the VCD device mounted across the IOP reset
645+
sysLaunchPopstarter(vcdElf, vcdSelector, "");
646+
}
647+
628648
void bdmLaunchGame(item_list_t *itemList, int id, config_set_t *configSet)
629649
{
630650
int i, fd, iop_fd, index, compatmask = 0;
@@ -648,33 +668,12 @@ void bdmLaunchGame(item_list_t *itemList, int id, config_set_t *configSet)
648668
game = gAutoLaunchBDMGame;
649669
}
650670

651-
// VCD view: this device is showing PS1 VCDs -> hand off to POPSTARTER instead of the disc /
652-
// Neutrino path below (which is entirely disc-specific). Menu-launch only; build the selector
653-
// + resolve the ELF on stack BEFORE deinit() frees the game list.
671+
// VCD view: this device is showing PS1 VCDs -> hand off to POPSTARTER (by name) instead of the
672+
// disc / Neutrino path below (which is entirely disc-specific). The BDM_TYPE_ATA internal exFAT HDD
673+
// still launches off the live massN: prefix, NOT ata0:/ (OPL never mounts an ata0: filesystem -- a
674+
// re-point there made LoadELFFromFile fail into an already-deinit'd OPL = black-screen freeze).
654675
if (gAutoLaunchBDMGame == NULL && game != NULL && vcdViewActive(itemList->mode)) {
655-
char vcdName[VCD_NAME_MAX];
656-
char vcdPrefix[64];
657-
char vcdElf[256];
658-
char vcdSelector[320];
659-
snprintf(vcdName, sizeof(vcdName), "%s", game->name);
660-
snprintf(vcdPrefix, sizeof(vcdPrefix), "%s", pDeviceData->bdmPrefix);
661-
if (vcdName[0] == '\0' || !strcmp(vcdName, "POPSTARTER"))
662-
return;
663-
if (!vcdResolvePopstarter(vcdPrefix, vcdElf, sizeof(vcdElf))) {
664-
guiMsgBox(_l(_STR_POPSTARTER_NOT_FOUND), 0, NULL);
665-
return;
666-
}
667-
// Every BDM device -- USB, MX4SIO, and the internal exFAT HDD (BDM_TYPE_ATA) -- is a massN: mount
668-
// at OPL runtime, so the ELF and selector BOTH use that one live prefix. OPL opens vcdElf HERE,
669-
// BEFORE the IOP reset, from the live massN: mount (kept alive by UNMOUNT_EXCEPTION); OPL never
670-
// mounts an ata0: filesystem (it only builds mass%d: paths), so the earlier BDM_TYPE_ATA re-point
671-
// to "ata0:/POPS/POPSTARTER.ELF" made LoadELFFromFile fail and return into an already-deinit'd OPL
672-
// with no re-init path = black-screen freeze. POPSTARTER only needs the VCD NAME from the selector
673-
// to know what to boot; the equipped BDMA modules steer it to the exFAT device after the reset, so
674-
// the selector's device prefix is irrelevant -- keep the live massN: one, same as USB/MX4SIO.
675-
vcdBuildSelector(vcdPrefix, VCD_PREFIX_MASS, vcdName, vcdSelector, sizeof(vcdSelector));
676-
deinit(UNMOUNT_EXCEPTION, itemList->mode); // keep the VCD device mounted across the IOP reset
677-
sysLaunchPopstarter(vcdElf, vcdSelector, "");
676+
bdmLaunchVcd(itemList, game->name, configSet);
678677
return;
679678
}
680679

@@ -1109,7 +1108,7 @@ static char *bdmGetPrefix(item_list_t *itemList)
11091108
static item_list_t bdmGameList = {
11101109
BDM_MODE, 2, 0, 0, MENU_MIN_INACTIVE_FRAMES, BDM_MODE_UPDATE_DELAY, NULL, NULL, &bdmGetTextId, &bdmGetPrefix, &bdmInit, &bdmNeedsUpdate,
11111110
&bdmUpdateGameList, &bdmGetGameCount, &bdmGetGame, &bdmGetGameName, &bdmGetGameNameLength, &bdmGetGameStartup, &bdmDeleteGame, &bdmRenameGame,
1112-
&bdmLaunchGame, &bdmGetConfig, &bdmGetImage, &bdmCleanUp, &bdmShutdown, &bdmCheckVMC, &bdmGetIconId};
1111+
&bdmLaunchGame, &bdmGetConfig, &bdmGetImage, &bdmCleanUp, &bdmShutdown, &bdmCheckVMC, &bdmGetIconId, &bdmLaunchVcd};
11131112

11141113
void bdmInitSemaphore()
11151114
{

src/ethsupport.c

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -606,6 +606,29 @@ static void ethRenameGame(item_list_t *itemList, int id, char *newName)
606606
ethULSizePrev = -2;
607607
}
608608

609+
// Launch a PS1/.VCD entry BY NAME via POPSTARTER over SMB (view-independent entry point: the in-view
610+
// menu launch below and the Favourites tab both use it). ethPrefix is static and smb: paths use '\'
611+
// (auto-detected by vcdSep); UNMOUNT_EXCEPTION keeps the share mounted across the IOP reset.
612+
static void ethLaunchVcd(item_list_t *itemList, const char *vcdName, config_set_t *configSet)
613+
{
614+
char vcdElf[256], vcdSelector[320];
615+
616+
if (!gPCShareName[0] || vcdName == NULL || vcdName[0] == '\0' || !strcmp(vcdName, "POPSTARTER"))
617+
return;
618+
if (!vcdResolvePopstarter(ethPrefix, vcdElf, sizeof(vcdElf))) {
619+
guiMsgBox(_l(_STR_POPSTARTER_NOT_FOUND), 0, NULL);
620+
return;
621+
}
622+
// POPSTARTER needs its SMB network IRX on the card to read a VCD over the network.
623+
if (!vcdSmbModulesPresent()) {
624+
guiMsgBox(_l(_STR_POPSTARTER_SMB_MISSING), 0, NULL);
625+
return;
626+
}
627+
vcdBuildSelector(ethPrefix, VCD_PREFIX_SMB, vcdName, vcdSelector, sizeof(vcdSelector));
628+
deinit(UNMOUNT_EXCEPTION, itemList->mode); // keep the SMB mount alive across the IOP reset
629+
sysLaunchPopstarter(vcdElf, vcdSelector, "");
630+
}
631+
609632
static void ethLaunchGame(item_list_t *itemList, int id, config_set_t *configSet)
610633
{
611634
int i, compatmask;
@@ -617,28 +640,9 @@ static void ethLaunchGame(item_list_t *itemList, int id, config_set_t *configSet
617640
u32 layer1_start, layer1_offset;
618641
unsigned short int layer1_part;
619642

620-
// VCD view (SMB): hand off to POPSTARTER with the SB. prefix (smb: paths use '\', auto-detected
621-
// by vcdSep). Only once a share is selected; build the selector + resolve the ELF on stack
622-
// BEFORE deinit() frees ethGames. ethPrefix is static.
643+
// VCD view (SMB): hand off to POPSTARTER by name, only once a share is selected.
623644
if (gPCShareName[0] && game != NULL && vcdViewActive(itemList->mode)) {
624-
char vcdName[VCD_NAME_MAX];
625-
char vcdElf[256];
626-
char vcdSelector[320];
627-
snprintf(vcdName, sizeof(vcdName), "%s", game->name);
628-
if (vcdName[0] == '\0' || !strcmp(vcdName, "POPSTARTER"))
629-
return;
630-
if (!vcdResolvePopstarter(ethPrefix, vcdElf, sizeof(vcdElf))) {
631-
guiMsgBox(_l(_STR_POPSTARTER_NOT_FOUND), 0, NULL);
632-
return;
633-
}
634-
// POPSTARTER needs its SMB network IRX on the card to read a VCD over the network.
635-
if (!vcdSmbModulesPresent()) {
636-
guiMsgBox(_l(_STR_POPSTARTER_SMB_MISSING), 0, NULL);
637-
return;
638-
}
639-
vcdBuildSelector(ethPrefix, VCD_PREFIX_SMB, vcdName, vcdSelector, sizeof(vcdSelector));
640-
deinit(UNMOUNT_EXCEPTION, itemList->mode); // keep the SMB mount alive across the IOP reset
641-
sysLaunchPopstarter(vcdElf, vcdSelector, "");
645+
ethLaunchVcd(itemList, game->name, configSet);
642646
return;
643647
}
644648

@@ -861,7 +865,7 @@ static char *ethGetPrefix(item_list_t *itemList)
861865
static item_list_t ethGameList = {
862866
ETH_MODE, 1, 0, 0, MENU_MIN_INACTIVE_FRAMES, ETH_MODE_UPDATE_DELAY, NULL, NULL, &ethGetTextId, &ethGetPrefix, &ethInit, &ethNeedsUpdate,
863867
&ethUpdateGameList, &ethGetGameCount, &ethGetGame, &ethGetGameName, &ethGetGameNameLength, &ethGetGameStartup, &ethDeleteGame, &ethRenameGame,
864-
&ethLaunchGame, &ethGetConfig, &ethGetImage, &ethCleanUp, &ethShutdown, &ethCheckVMC, &ethGetIconId};
868+
&ethLaunchGame, &ethGetConfig, &ethGetImage, &ethCleanUp, &ethShutdown, &ethCheckVMC, &ethGetIconId, &ethLaunchVcd};
865869

866870
static int ethReadNetConfig(void)
867871
{

0 commit comments

Comments
 (0)