diff --git a/Core/CmdLine.cpp b/Core/CmdLine.cpp index d6c037c381..b4bfe8abf0 100644 --- a/Core/CmdLine.cpp +++ b/Core/CmdLine.cpp @@ -16,51 +16,121 @@ #endif +enum class CmdParamType { + Bool, + BoolInverse, + Int, + String, +}; + +struct CommandLineParam { + size_t offsetInStruct; // calculate with offsetof(CommandLineOptions, member). NOTE: Must point to an optional<> of the correct type! + CmdParamType type; + const char *longName; + char shortName; // can be 0 for no short name + const char *docString; +}; + +#define POFF(member) \ + {offsetof(CommandLineOptions, member)} + +enum class ParseParamResult { + Success, + NoMatch, + BadValue, +}; + +ParseParamResult SetValue(CommandLineOptions *options, const CommandLineParam ¶m, const std::string &value) { + switch (param.type) { + case CmdParamType::Bool: + case CmdParamType::BoolInverse: + { + bool v = true; + if (value == "true" || value == "1") { + v = true; + } else if (value == "false" || value == "0") { + v = false; + } else { + PRINT_STDERR("Error: Invalid value for boolean parameter --%s: '%s'. Expected 'true' or 'false'.\n", param.longName, value.c_str()); + return ParseParamResult::BadValue; + } + + if (param.type == CmdParamType::BoolInverse) { + v = !v; + } + *reinterpret_cast *>(reinterpret_cast(options) + param.offsetInStruct) = v; + break; + } + case CmdParamType::Int: + *reinterpret_cast *>(reinterpret_cast(options) + param.offsetInStruct) = std::stoi(value); + break; + case CmdParamType::String: + *reinterpret_cast *>(reinterpret_cast(options) + param.offsetInStruct) = value; + break; + default: + break; + } + return ParseParamResult::Success; +} + // Helper to parse parameters that take an argument. // Supports: --param=value, --param value, -p value // Returns true if argument was consumed, false otherwise. // On success, 'value' contains the argument string and 'i' is incremented if needed. -static bool ParseParameterWithArg(int argc, const char *argv[], size_t &i, - const char *longName, char shortName, - const char **outValue) { +static ParseParamResult ParseParameterStr(int argc, const char *argv[], size_t &i, const CommandLineParam ¶m, CommandLineOptions *options) { const char *arg = argv[i]; const size_t len = strlen(arg); // Check for short form: -p value - if (shortName && len == 2 && arg[0] == '-' && arg[1] == shortName) { + if (param.shortName && len == 2 && arg[0] == '-' && arg[1] == param.shortName) { if (i + 1 < argc) { - *outValue = argv[i + 1]; + ParseParamResult result = SetValue(options, param, argv[i + 1]); i += 2; // Skip both -p and value - return true; + return result; } - PRINT_STDERR("Error: -%c requires an argument.\n", shortName); - return false; + PRINT_STDERR("Error: -%c requires an argument.\n", param.shortName); + return ParseParamResult::BadValue; } // Check for long form with equals: --param=value - if (longName) { - std::string prefix = std::string("--") + longName + "="; + if (param.longName) { + std::string prefix = std::string("--") + param.longName + "="; if (startsWith(arg, prefix)) { - *outValue = arg + prefix.size(); + ParseParamResult result = SetValue(options, param, arg + prefix.size()); i++; // Skip --param=value - return true; + return result; + } + + // Check for boolean/inverse-boolean parameter - these don't take a separate param value. + if (param.type == CmdParamType::Bool && equals(arg, std::string("--") + param.longName)) { + ParseParamResult result = SetValue(options, param, "true"); + i++; // Skip --param + return result; + } else if (param.type == CmdParamType::BoolInverse && equals(arg, std::string("--") + param.longName)) { + ParseParamResult result = SetValue(options, param, "false"); + i++; // Skip --param + return result; } // Check for long form with space: --param value - if (equals(arg, std::string("--") + longName)) { + if (equals(arg, std::string("--") + param.longName)) { if (i + 1 < argc) { - *outValue = argv[i + 1]; + ParseParamResult result = SetValue(options, param, argv[i + 1]); i += 2; // Skip both --param and value - return true; + return result; } - PRINT_STDERR("Error: --%s requires an argument.\n", longName); - return false; + PRINT_STDERR("Error: --%s requires an argument.\n", param.longName); + return ParseParamResult::BadValue; } } - - return false; + return ParseParamResult::NoMatch; } +static const CommandLineParam g_autoParams[] = { + {POFF(fullscreen), CmdParamType::Bool, "fullscreen", 0, "Force full screen mode"}, + {POFF(fullscreen), CmdParamType::BoolInverse, "windowed", 0, "Force windowed mode"}, +}; + static int printUsage(int argc, const char *argv[]) { // NOTE: by convention, --help outputs to stdout, // not to stderr, since it is intended output in this @@ -88,13 +158,17 @@ static int printUsage(int argc, const char *argv[]) { PRINT_STDOUT(" -j use JIT\n"); PRINT_STDOUT(" -J use IR JIT\n"); - PRINT_STDOUT(" --fullscreen force full screen mode, ignoring saved configuration\n"); - PRINT_STDOUT(" --windowed force windowed mode, ignoring saved configuration\n"); + for (const auto ¶m : g_autoParams) { + PRINT_STDOUT(" --%s%s%s\n", param.longName, + param.shortName ? ", -" : "", + param.shortName ? std::string(1, param.shortName).c_str() : ""); + PRINT_STDOUT(" %s\n", param.docString); + } + PRINT_STDOUT(" --xres PIXELS set X resolution\n"); PRINT_STDOUT(" --yres PIXELS set Y resolution\n"); PRINT_STDOUT(" --dpi DPI set DPI\n"); PRINT_STDOUT(" --scale FACTOR set scale\n"); - PRINT_STDOUT(" --ipad set resolution to 1024x768\n"); PRINT_STDOUT(" --portrait portrait mode\n"); PRINT_STDOUT(" --graphics=BACKEND use a different gpu backend\n"); PRINT_STDOUT(" options: gles, software, etc. (also opengl3.1, etc.)\n"); @@ -108,6 +182,7 @@ static int printUsage(int argc, const char *argv[]) { return 0; } + // Logging should be done with plain printf here. // Error reporting is done with PRINT_STDERR(....). // Actually might want to reconsider given Android... @@ -163,19 +238,28 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { // Ignore } #endif - // Simple bool commands - // NOTE: We need to parse --fullscreen early, before we create the window. - if (equals(argv[i], "--help")) { + bool parsedAutoParam = false; + // Loop through all the auto commands, call ParseParameterWithArg to handle them. + for (const auto ¶m : g_autoParams) { + ParseParamResult result = ParseParameterStr(argc, argv, i, param, this); + if (result == ParseParamResult::Success) { + parsedAutoParam = true; + break; + } else if (result == ParseParamResult::BadValue) { + return CommandLineParseResult::Exit; + } // else nomatch + } + + if (parsedAutoParam) { + // We already incremented i. + continue; + } else if (equals(argv[i], "--help")) { printUsage(argc, argv); return CommandLineParseResult::Exit; } else if (equals(argv[i], "--version")) { printf("%s\n", PPSSPP_GIT_VERSION); return CommandLineParseResult::Exit; - } else if (equals(argv[i], "--fullscreen")) { - fullscreen = true; - } else if (equals(argv[i], "--windowed")) { - fullscreen = false; // Commands with parameters. TODO: Should support both space and equals, like --config=foo.ini and --config foo.ini } else if (startsWith(argv[i], gpuBackendStr)) { const std::string_view restOfOption = argv[i] + gpuBackendStr.size(); @@ -203,7 +287,6 @@ CommandLineParseResult CommandLineOptions::Parse(int argc, const char *argv[]) { } else { // Report unknown argument later once this is complete. } - // To the next argument. i++; } diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index b7585f5095..9cf720dd5e 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -1441,8 +1441,6 @@ int main(int argc, char *argv[]) { const int linked = SDL_GetVersion(); int set_xres = -1; int set_yres = -1; - bool portrait = false; - bool set_ipad = false; float set_dpi = 0.0f; float set_scale = 1.0f; @@ -1461,10 +1459,7 @@ int main(int argc, char *argv[]) { Uint32 mode = 0; for (int i = 1; i < argc; i++) { - if (!strcmp(argv[i], "--fullscreen")) { - mode |= SDL_WINDOW_FULLSCREEN; - g_Config.DoNotSaveSetting(&g_Config.bFullScreen); - } else if (set_xres == -2) + if (set_xres == -2) set_xres = parseInt(argv[i]); else if (set_yres == -2) set_yres = parseInt(argv[i]); @@ -1480,10 +1475,6 @@ int main(int argc, char *argv[]) { set_dpi = -2; else if (!strcmp(argv[i], "--scale")) set_scale = -2; - else if (!strcmp(argv[i], "--ipad")) - set_ipad = true; - else if (!strcmp(argv[i], "--portrait")) - portrait = true; else if (!strncmp(argv[i], "--graphics=", strlen("--graphics="))) { const char *restOfOption = argv[i] + strlen("--graphics="); double val=-1.0; // Yes, floating point. @@ -1556,6 +1547,7 @@ int main(int argc, char *argv[]) { g_RefreshRate = displayMode->refresh_rate; SDL_free(displayIDs); + // TODO: Should only call this if we actually intend to use OpenGL. SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); @@ -1565,19 +1557,19 @@ int main(int argc, char *argv[]) { // Force fullscreen if the resolution is too low to run windowed. if (g_DesktopWidth < 480 * 2 && g_DesktopHeight < 272 * 2) { - mode |= SDL_WINDOW_FULLSCREEN; + cmdLineParams.fullscreen = true; } // If we're on mobile, don't try for windowed either. #if defined(MOBILE_DEVICE) && !PPSSPP_PLATFORM(SWITCH) - mode |= SDL_WINDOW_FULLSCREEN; + cmdLineParams.fullscreen = true; #elif defined(USING_FBDEV) || PPSSPP_PLATFORM(SWITCH) - mode |= SDL_WINDOW_FULLSCREEN; + cmdLineParams.fullscreen = true; #else mode |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY; #endif - if (mode & SDL_WINDOW_FULLSCREEN) { + if (cmdLineParams.fullscreen) { g_display.pixel_xres = g_DesktopWidth; g_display.pixel_yres = g_DesktopHeight; g_Config.bFullScreen = true; @@ -1585,16 +1577,9 @@ int main(int argc, char *argv[]) { // set a sensible default resolution (2x) g_display.pixel_xres = 480 * 2 * set_scale; g_display.pixel_yres = 272 * 2 * set_scale; - if (portrait) { - std::swap(g_display.pixel_xres, g_display.pixel_yres); - } g_Config.bFullScreen = false; } - if (set_ipad) { - g_display.pixel_xres = 1024; - g_display.pixel_yres = 768; - } if (!landscape) { std::swap(g_display.pixel_xres, g_display.pixel_yres); } @@ -1636,6 +1621,9 @@ int main(int argc, char *argv[]) { #else const char *external_dir = "/tmp"; #endif + + // After NativeInit, code should no longer look at cmdLineOptions, they should have been translated + // into g_Config settings. This is because NativeInit may modify g_Config settings based on the command line options. NativeInit(remain_argc, (const char **)remain_argv, cmdLineOptions, path, external_dir, nullptr); // Use the setting from the config when initing the window. diff --git a/Windows/EmuThread.cpp b/Windows/EmuThread.cpp index 407792bcf5..09460c0bba 100644 --- a/Windows/EmuThread.cpp +++ b/Windows/EmuThread.cpp @@ -43,8 +43,6 @@ static std::thread mainThread; static std::string g_error_message; static bool g_inLoop; -extern std::vector GetWideCmdLine(); - class GraphicsContext; static GraphicsContext *g_graphicsContext; diff --git a/Windows/main.cpp b/Windows/main.cpp index d0e5b6d2ed..1b46c21268 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -1050,7 +1050,7 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin // args provide argc and argv. CommandLineOptions cmdLineOptions; - CommandLineParseResult parseResult = cmdLineOptions.Parse((int)args.size(), args.data()); + const CommandLineParseResult parseResult = cmdLineOptions.Parse((int)args.size(), args.data()); switch (parseResult) { case CommandLineParseResult::Exit: return 0; diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index 418c3c0e3a..4eb9788c2a 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -84,6 +84,7 @@ #include "Common/File/VFS/VFS.h" #include "Common/File/VFS/DirectoryReader.h" #include "Common/Math/fast/fast_matrix.h" +#include "Core/CmdLine.h" #include "Core/FileSystems/ISOFileSystem.h" #include "Core/MemMap.h" #include "Core/KeyMap.h" @@ -1310,6 +1311,20 @@ bool TestFriendlyPath() { return true; } +bool TestCmdLine() { + const char *argv[] = { + "ppsspp", + "--fullscreen", + "My_Game.iso" + }; + int argc = ARRAY_SIZE(argv); + CommandLineOptions options; + options.Parse(argc, argv); + EXPECT_TRUE(options.fullscreen.value_or(false)); + EXPECT_EQ_STR(options.bootFilename.value_or(""), std::string("My_Game.iso")); + return true; +} + // Check that RTTI is working. bool TestLang() { struct Base { virtual ~Base() = default; }; @@ -1393,6 +1408,7 @@ TestItem availableTests[] = { TEST_ITEM(FriendlyPath), TEST_ITEM(LinAlg), TEST_ITEM(Lang), + TEST_ITEM(CmdLine), }; int main(int argc, const char *argv[]) {