diff --git a/README.md b/README.md index aedd792..923fd8f 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Each release of this application targets a specific version or versions of BlueM |--|--| | 0.1 | 8.1 ("Spirit of Adventure") | | 0.1.1 | 8.2 ("Approaching Planet Nine") | -| 0.2, 0.3 | 8.1, 8.2+ | +| 0.2+ | 8.1+ | Using a version of CLIFp that does not target the version of Flashpoint you wish to use it with is highly discouraged as some features may not work correctly or at all and in some cases the utility may fail to function entirely; **however since 0.2 compatibility with newer versions is quite likely even if they aren't explicit listed yet** (usually because I haven't had time to check if an update is needed). @@ -34,7 +34,7 @@ CLIFp was primarily created for use with its sister project [OFILb (Obby's Flas That being said, it is perfectly possible to use CLIFp with Flashpoint alone in any manner you see fit. ### General -While CLIFp has multiple functions, typical usage involves one of two main methods: +While CLIFp has multiple functions, typical usage involves one of three main methods: **Auto:** This is the most straightforward and hassle free approach. Simply use the **--auto** or **-a** switch followed by the GUID/UUID of the game/animation you wish to start, as seen in the following example: @@ -44,6 +44,9 @@ You can find a title's ID by right clicking on an entry in Flashpoint and select If the ID belongs to a main game/animation entry, any additional apps that are marked as auto-run before will also be started ahead of time just as they would be when using the Flashpoint Launcher. +**Random:** +This mode works exactly the same as Auto mode except that the title ID is selected at random (uniformly) for you from the pool of all playable games/animations. A title qualifies as playable if it is a main title who's status is not "Not Working" or if it is an additional-app that is not a message, extra, or otherwise Autostart-before entry. + **App/Param:** This method can only start one application at a time. Use the **-x/--exe** switch to specify the relative (from Flashpoint's directory) path to the application to launch and the **-p/--param** switch to provide the launch arguments that will be passed to the target application. @@ -56,12 +59,13 @@ The applications and arguments that are used for each game/animation can be foun - **-x | --exe:** Relative (to Flashpoint Directory) of primary application to launch - **-p | --param:** Command-line parameters to use when starting the primary application - **-a | --auto:** Finds a game/additional-app by UUID and runs it if found, including run-before additional apps in the case of a game + - **-r | --random:** Selects a random game UUID from the database and starts it in the same manner as using the --auto switch. - **-m | --msg:** Displays an pop-up dialog with the supplied message. Used primarily for some additional apps - **-e | --extra:** Opens an explorer window to the specified extra. Used primarily for some additional apps - **-q | --quiet:** Silences all non-critical error messages - **-s | --silent:** Silences all error messages (takes precedence over quiet mode) - -Use **'exe'** and **'param'** for normal operation, use **'auto'** by itself for automatic operation, use **'msg'** to display a popup message, use **'extra'** to view an extra, or use **'help'** and/or **'version'** for information. + +Use **'exe'** and **'param'** for normal operation, use **'auto'** by itself for automatic operation, use **'random'** by itself for random operation, use **'msg'** to display a popup message, use **'extra'** to view an extra, or use **'help'** and/or **'version'** for information. **NOTE:** When using the **--exe** and **--param** switches all quotes that are part of the input itself must be escaped for the command to be passed correctly. For example, the launch command diff --git a/src/flashpointinstall.cpp b/src/flashpointinstall.cpp index ac13dd3..b86ed4a 100644 --- a/src/flashpointinstall.cpp +++ b/src/flashpointinstall.cpp @@ -788,7 +788,7 @@ QSqlError Install::initialPlaylistGameQuery(QList& resultBuffer, return QSqlError(); } -QSqlError Install::queryEntryID(DBQueryBuffer& resultBuffer, QUuid appID) const +QSqlError Install::queryEntryByID(DBQueryBuffer& resultBuffer, QUuid appID) const { // Ensure return buffer is effectively null resultBuffer = DBQueryBuffer(); @@ -842,6 +842,44 @@ QSqlError Install::queryEntryAddApps(DBQueryBuffer& resultBuffer, QUuid appID) c return makeNonBindQuery(resultBuffer, &fpDB, mainQueryCommand, sizeQueryCommand); } +QSqlError Install::queryAllGameIDs(DBQueryBuffer& resultBuffer) +{ + // Ensure return buffer is effectively null + resultBuffer = DBQueryBuffer(); + + // Get database + QSqlDatabase fpDB = getThreadedDatabaseConnection(); + + // Make query + QString baseQueryCommand = "SELECT %1 FROM " + DBTable_Game::NAME + " WHERE " + + DBTable_Game::COL_STATUS + " != '" + DBTable_Game::ENTRY_NOT_WORK + "'"; + QString mainQueryCommand = baseQueryCommand.arg("`" + DBTable_Game::COL_ID + "`"); + QString sizeQueryCommand = baseQueryCommand.arg(GENERAL_QUERY_SIZE_COMMAND); + + resultBuffer.source = DBTable_Game::NAME; + return makeNonBindQuery(resultBuffer, &fpDB, mainQueryCommand, sizeQueryCommand); +} + +QSqlError Install::queryAllMainAddAppIDs(DBQueryBuffer& resultBuffer) +{ + // Ensure return buffer is effectively null + resultBuffer = DBQueryBuffer(); + + // Get database + QSqlDatabase fpDB = getThreadedDatabaseConnection(); + + // Make query + QString baseQueryCommand = "SELECT %1 FROM " + DBTable_Add_App::NAME + " WHERE " + + DBTable_Add_App::COL_APP_PATH + " NOT IN ('" + DBTable_Add_App::ENTRY_EXTRAS + + "','" + DBTable_Add_App::ENTRY_MESSAGE + "') AND " + DBTable_Add_App::COL_AUTORUN + + " != 1"; + QString mainQueryCommand = baseQueryCommand.arg("`" + DBTable_Add_App::COL_ID + "`"); + QString sizeQueryCommand = baseQueryCommand.arg(GENERAL_QUERY_SIZE_COMMAND); + + resultBuffer.source = DBTable_Game::NAME; + return makeNonBindQuery(resultBuffer, &fpDB, mainQueryCommand, sizeQueryCommand); +} + QString Install::getPath() const { return mRootDirectory.absolutePath(); } QStringList Install::getPlatformList() const { return mPlatformList; } QStringList Install::getPlaylistList() const { return mPlaylistList; } diff --git a/src/flashpointinstall.h b/src/flashpointinstall.h index b873aac..9c479da 100644 --- a/src/flashpointinstall.h +++ b/src/flashpointinstall.h @@ -99,6 +99,7 @@ class Install COL_VERSION, COL_ORIGINAL_DESC, COL_LANGUAGE, COL_LIBRARY, COL_ORDER_TITLE}; static inline const QString GAME_LIBRARY = "arcade"; + static inline const QString ENTRY_NOT_WORK = "Not Working"; }; class DBTable_Add_App @@ -338,8 +339,10 @@ class Install QSqlError initialPlaylistGameQuery(QList& resultBuffer, const QList& knownPlaylistsToQuery) const; // Queries - CLIFp - QSqlError queryEntryID(DBQueryBuffer& resultBuffer, QUuid appID) const; + QSqlError queryEntryByID(DBQueryBuffer& resultBuffer, QUuid appID) const; QSqlError queryEntryAddApps(DBQueryBuffer& resultBuffer, QUuid appID) const; + QSqlError queryAllGameIDs(DBQueryBuffer& resultBuffer); + QSqlError queryAllMainAddAppIDs(DBQueryBuffer& resultBuffer); // Data access QString getPath() const; diff --git a/src/main.cpp b/src/main.cpp index 348cf14..f9d5160 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -39,7 +39,7 @@ enum ErrorCode CANT_READ_BAT_FILE = 0x14 }; -enum class OperationMode { Invalid, Normal, Auto, Message, Extra, Information }; +enum class OperationMode { Invalid, Normal, Auto, Random, Message, Extra, Information }; enum class TaskType { Startup, Primary, Auxiliary, Wait, Shutdown }; enum class ProcessType { Blocking, Deferred, Detached }; enum class ErrorVerbosity { Full, Quiet, Silent }; @@ -87,6 +87,10 @@ const QString CL_OPT_AUTO_S_NAME = "a"; const QString CL_OPT_AUTO_L_NAME = "auto"; const QString CL_OPT_AUTO_DESC = "Finds a game/additional-app by UUID and runs it if found, including run-before additional apps in the case of a game."; +const QString CL_OPT_RAND_S_NAME = "r"; +const QString CL_OPT_RAND_L_NAME = "random"; +const QString CL_OPT_RAND_DESC = "Selects a random game UUID from the database and starts it in the same manner as using the --" + CL_OPT_AUTO_L_NAME + " switch."; + const QString CL_OPT_QUIET_S_NAME = "q"; const QString CL_OPT_QUIET_L_NAME = "quiet"; const QString CL_OPT_QUIET_DESC = "Silences all non-critical error messages."; @@ -104,14 +108,15 @@ const QString CL_HELP_MESSAGE = "-" + CL_OPT_APP_S_NAME + " | --" + CL_OPT_APP_L_NAME + ":  " + CL_OPT_APP_DESC + "
" "-" + CL_OPT_PARAM_S_NAME + " | --" + CL_OPT_PARAM_L_NAME + ":  " + CL_OPT_PARAM_DESC + "
" "-" + CL_OPT_AUTO_S_NAME + " | --" + CL_OPT_AUTO_L_NAME + ":  " + CL_OPT_AUTO_DESC + "
" + "-" + CL_OPT_RAND_S_NAME + " | --" + CL_OPT_RAND_L_NAME + ":  " + CL_OPT_RAND_DESC + "
" "-" + CL_OPT_MSG_S_NAME + " | --" + CL_OPT_MSG_L_NAME + ":  " + CL_OPT_MSG_DESC + "
" "-" + CL_OPT_EXTRA_S_NAME + " | --" + CL_OPT_EXTRA_L_NAME + ":  " + CL_OPT_EXTRA_DESC + "
" "-" + CL_OPT_QUIET_S_NAME + " | --" + CL_OPT_QUIET_L_NAME + ":  " + CL_OPT_QUIET_DESC + "
" "-" + CL_OPT_SILENT_S_NAME + " | --" + CL_OPT_SILENT_L_NAME + ":  " + CL_OPT_SILENT_DESC + "
" - "
" "Use '" + CL_OPT_APP_L_NAME + "' and '" + CL_OPT_PARAM_L_NAME + "' for normal operation, use '" + CL_OPT_AUTO_L_NAME + - "' by itself for automatic operation, use '" + CL_OPT_MSG_L_NAME + "' to display a popup message, use '" + CL_OPT_EXTRA_L_NAME + - "' to view an extra, or use '" + CL_OPT_HELP_L_NAME + "' and/or '" + CL_OPT_VERSION_L_NAME + "' for information."; + "' by itself for automatic operation, use '" + CL_OPT_MSG_L_NAME + "' by itself for random operation, use '" + CL_OPT_MSG_L_NAME + + "' to display a popup message, use '" + CL_OPT_EXTRA_L_NAME + "' to view an extra, or use '" + CL_OPT_HELP_L_NAME + + "' and/or '" + CL_OPT_VERSION_L_NAME + "' for information."; const QString CL_VERSION_MESSAGE = "CLI Flashpoint version " VER_PRODUCTVERSION_STR ", designed for use with BlueMaxima's Flashpoint " VER_PRODUCTVERSION_STR "+"; @@ -149,20 +154,22 @@ const QString WRN_WAIT_PROCESS_NOT_HOOKED_S = "The title may not work correctly" const QCommandLineOption CL_OPTION_APP({CL_OPT_APP_S_NAME, CL_OPT_APP_L_NAME}, CL_OPT_APP_DESC, "application"); // Takes value const QCommandLineOption CL_OPTION_PARAM({CL_OPT_PARAM_S_NAME, CL_OPT_PARAM_L_NAME}, CL_OPT_PARAM_DESC, "parameters"); // Takes value const QCommandLineOption CL_OPTION_AUTO({CL_OPT_AUTO_S_NAME, CL_OPT_AUTO_L_NAME}, CL_OPT_AUTO_DESC, "id"); // Takes value +const QCommandLineOption CL_OPTION_RAND({CL_OPT_RAND_S_NAME, CL_OPT_RAND_L_NAME}, CL_OPT_RAND_DESC); // Boolean option const QCommandLineOption CL_OPTION_MSG({CL_OPT_MSG_S_NAME, CL_OPT_MSG_L_NAME}, CL_OPT_MSG_DESC, "message"); // Takes value const QCommandLineOption CL_OPTION_EXTRA({CL_OPT_EXTRA_S_NAME, CL_OPT_EXTRA_L_NAME}, CL_OPT_EXTRA_DESC, "extra"); // Takes value const QCommandLineOption CL_OPTION_HELP({CL_OPT_HELP_S_NAME, CL_OPT_HELP_L_NAME, CL_OPT_HELP_E_NAME}, CL_OPT_HELP_DESC); // Boolean option const QCommandLineOption CL_OPTION_VERSION({CL_OPT_VERSION_S_NAME, CL_OPT_VERSION_L_NAME}, CL_OPT_VERSION_DESC); // Boolean option const QCommandLineOption CL_OPTION_QUIET({CL_OPT_QUIET_S_NAME, CL_OPT_QUIET_L_NAME}, CL_OPT_QUIET_DESC); // Boolean option const QCommandLineOption CL_OPTION_SILENT({CL_OPT_SILENT_S_NAME, CL_OPT_SILENT_L_NAME}, CL_OPT_SILENT_DESC); // Boolean option -const QList CL_OPTIONS_MAIN{&CL_OPTION_APP, &CL_OPTION_PARAM, &CL_OPTION_AUTO, - &CL_OPTION_MSG, &CL_OPTION_EXTRA, &CL_OPTION_HELP, &CL_OPTION_VERSION}; +const QList CL_OPTIONS_MAIN{&CL_OPTION_APP, &CL_OPTION_PARAM, &CL_OPTION_AUTO, &CL_OPTION_MSG, + &CL_OPTION_EXTRA, &CL_OPTION_HELP, &CL_OPTION_VERSION, &CL_OPTION_RAND}; const QList CL_OPTIONS_ALL = CL_OPTIONS_MAIN + QList{&CL_OPTION_QUIET, &CL_OPTION_SILENT}; // CLI Option Operation Mode Map TODO: Submit a patch for Qt6 to make QCommandLineOption directly hashable (implement == and qHash) const QHash, OperationMode> CL_MAIN_OPTIONS_OP_MODE_MAP{ {{CL_OPT_APP_S_NAME, CL_OPT_PARAM_S_NAME}, OperationMode::Normal}, {{CL_OPT_AUTO_S_NAME}, OperationMode::Auto}, + {{CL_OPT_RAND_S_NAME}, OperationMode::Random}, {{CL_OPT_MSG_S_NAME}, OperationMode::Message}, {{CL_OPT_EXTRA_S_NAME}, OperationMode::Extra}, {{CL_OPT_HELP_S_NAME}, OperationMode::Information}, @@ -190,6 +197,7 @@ const int LOG_MAX_ENTRIES = 50; // Logging - Messages const QString LOG_ERR_INVALID_PARAM = "Invalid combination of parameters used"; const QString LOG_ERR_CRITICAL = "Aborting execution due to previous critical errors"; +const QString LOG_WRN_INVALID_RAND_ID = "A UUID found in the database during Random operation is invalid (%1)"; const QString LOG_EVENT_FLASHPOINT_LINK = "Linked to Flashpoint install at: %1"; const QString LOG_EVENT_OP_MODE = "Operation Mode: %1"; const QString LOG_EVENT_APP_TASK = "Enqueued App Task: {.type = %1, .path = \"%2\", .filename = \"%3\", " @@ -200,6 +208,9 @@ const QString LOG_EVENT_HELP_SHOWN = "Displayed help information"; const QString LOG_EVENT_VER_SHOWN = "Displayed version information"; const QString LOG_EVENT_INIT = "Initializing CLIFp..."; const QString LOG_EVENT_GET_SET = "Reading Flashpoint configuration..."; +const QString LOG_EVENT_SEL_RAND = "Selecting a playable game at random..."; +const QString LOG_EVENT_RAND_ID = "Randomly chose game \"%1\""; +const QString LOG_EVENT_PLAYABLE_COUNT = "Found %1 playable games"; const QString LOG_EVENT_ENQ_START = "Enqueuing startup tasks..."; const QString LOG_EVENT_ENQ_AUTO = "Enqueuing automatic tasks..."; const QString LOG_EVENT_ENQ_STOP = "Enqueuing shutdown tasks..."; @@ -244,6 +255,7 @@ void cleanup(FP::Install& fpInstall, QList& childProcesses); // Prototypes - Helper QString getRawCommandLineParams(); +ErrorCode randomlySelectID(QUuid& idBuffer, FP::Install& fpInstall); Qx::GenericError appInvolvesSecurePlayer(bool& involvesBuffer, QFileInfo appInfo); QString escapeNativeArgsForCMD(QString nativeArgs); void postError(Qx::GenericError error, bool log = true); @@ -417,6 +429,20 @@ int main(int argc, char *argv[]) if((enqueueError = enqueueConditionalWaitTask(appTaskQueue, inputInfo))) return printLogAndExit(enqueueError); break; + case OperationMode::Random: + if((enqueueError = openAndVerifyProperDatabase(flashpointInstall))) + return printLogAndExit(enqueueError); + + if((enqueueError = randomlySelectID(autoID, flashpointInstall))) + return printLogAndExit(enqueueError); + + if((enqueueError = enqueueStartupTasks(appTaskQueue, flashpointConfig, flashpointServices))) + return printLogAndExit(enqueueError); + + if((enqueueError = enqueueAutomaticTasks(appTaskQueue, autoID, flashpointInstall))) + return printLogAndExit(enqueueError); + break; + case OperationMode::Auto: if((autoID = QUuid(clParser.value(CL_OPTION_AUTO))).isNull()) @@ -567,7 +593,7 @@ ErrorCode enqueueAutomaticTasks(std::queue& taskQueue, QUuid targetID, FP::Install::DBQueryBuffer searchResult; ErrorCode enqueueError; - searchError = fpInstall.queryEntryID(searchResult, targetID); + searchError = fpInstall.queryEntryByID(searchResult, targetID); if(searchError.isValid()) { postError(Qx::GenericError(Qx::GenericError::Critical, ERR_UNEXPECTED_SQL, searchError.text())); @@ -994,6 +1020,75 @@ QString getRawCommandLineParams() return QString::fromStdWString(std::wstring(rawCL)); } +ErrorCode randomlySelectID(QUuid& idBuffer, FP::Install& fpInstall) +{ + logEvent(LOG_EVENT_SEL_RAND); + + // Reset buffer + idBuffer = QUuid(); + + // SQL Error tracker + QSqlError searchError; + + // Query all main games + FP::Install::DBQueryBuffer mainGameIDQuery; + searchError = fpInstall.queryAllGameIDs(mainGameIDQuery); + if(searchError.isValid()) + { + postError(Qx::GenericError(Qx::GenericError::Critical, ERR_UNEXPECTED_SQL, searchError.text())); + return SQL_ERROR; + } + + // Query all main additional apps + FP::Install::DBQueryBuffer mainAddAppIDQuery; + searchError = fpInstall.queryAllGameIDs(mainAddAppIDQuery); + if(searchError.isValid()) + { + postError(Qx::GenericError(Qx::GenericError::Critical, ERR_UNEXPECTED_SQL, searchError.text())); + return SQL_ERROR; + } + + QList playableIDs; + + // Enumerate main game IDs + for(int i = 0; i < mainGameIDQuery.size; i++) + { + // Go to next record + mainGameIDQuery.result.next(); + + // Add ID to list + QString gameIDString = mainGameIDQuery.result.value(FP::Install::DBTable_Game::COL_ID).toString(); + QUuid gameID = QUuid(gameIDString); + if(!gameID.isNull()) + playableIDs.append(gameID); + else + logError(Qx::GenericError(Qx::GenericError::Warning, LOG_WRN_INVALID_RAND_ID.arg(gameIDString))); + } + + // Enumerate main additional app IDs + for(int i = 0; i < mainAddAppIDQuery.size; i++) + { + // Go to next record + mainAddAppIDQuery.result.next(); + + // Create ID and add if valid (should always be) + QString gameIDString = mainAddAppIDQuery.result.value(FP::Install::DBTable_Game::COL_ID).toString(); + QUuid gameID = QUuid(gameIDString); + if(!gameID.isNull()) + playableIDs.append(gameID); + else + logError(Qx::GenericError(Qx::GenericError::Warning, LOG_WRN_INVALID_RAND_ID.arg(gameIDString))); + } + logEvent(LOG_EVENT_PLAYABLE_COUNT.arg(QLocale(QLocale::system()).toString(playableIDs.size()))); + + // Set buffer to random ID + idBuffer = playableIDs.value(QRandomGenerator::global()->bounded(playableIDs.size() - 1)); + logEvent(LOG_EVENT_RAND_ID.arg(idBuffer.toString(QUuid::WithoutBraces))); + + // Return success + return NO_ERR; +} + Qx::GenericError appInvolvesSecurePlayer(bool& involvesBuffer, QFileInfo appInfo) { // Reset buffer diff --git a/src/version.h b/src/version.h index d162817..8a647bb 100644 --- a/src/version.h +++ b/src/version.h @@ -1,8 +1,8 @@ #ifndef VERSION_H #define VERSION_H -#define VER_FILEVERSION 0,3,0,0 -#define VER_FILEVERSION_STR "0.3.0.0" +#define VER_FILEVERSION 0,3,1,0 +#define VER_FILEVERSION_STR "0.3.1.0" #define VER_PRODUCTVERSION 8,2 #define VER_PRODUCTVERSION_STR "8.2"