Exzh_PMGI
本帖最后由 Exzh_PMGI 于 2019-2-1 11:28 编辑



鉴于此类文章的特殊性,我暂且将它放到茶馆上发布,此后这个系列将会搬运到我的个人博客上。
Blog : 我的开发者博客  有任何建议或想法的请联系我 Email : [email protected]



Minecraft 启动器核心改造记 (一)
最近我的 Minecode Studio 项目需要启动器来支持代码的调试和运行,本想着能够在 mcbbs 上找到用 C++ 写成的启动器动态库,可找了一圈就只找到了一个 C++ 静态库:QGettingStarted,于是乎 git clone 大法把它克隆到本地。


库结构如上图所示
翻阅它的 README.md 得知它的启动方法如下:
  1. QString launchCommand;
  2.         QGSLauncher launcher;
  3.         QGSGameDirectory gameDirectory(QDir("./minecraft"));
  4.         QGSLaunchOptionsBuilder launchOptionsBuilder;

  5.         launchOptionsBuilder.setJavaPath("C:/Program Files/Java/jre1.8.0_162/bin/javaw.exe");
  6.         launchOptionsBuilder.setMaxMemory(1024);
  7.         launchOptionsBuilder.setMinMemory(128);

  8.         QGSAuthInfo authInfo;
  9.         QEventLoop eventLoop;
  10.         //正版登录替换为QGSYggdrasilAccountFactory
  11.         QGSIAccount * account(QGSOfflineAccountFactory().createAccount());
  12.         QObject::connect(account, &QGSIAccount::finished, &eventLoop, &QEventLoop::quit);
  13.         QObject::connect(account, &QGSIAccount::finished, [&authInfo](QGSAuthInfo _authInfo)
  14.         {
  15.                 authInfo = _authInfo;
  16.         });
  17.         /*正版登录的错误检查
  18.         QObject::connect(account, &QGSIAccount::error, [](QGSNetworkError networkError)
  19.         {
  20.                 qDebug() << "QGSNetworkError:" << networkError.getCode() << networkError.getErrorString();
  21.         });
  22.         */
  23.         account->authenticate("player");
  24.         //account->authenticate("[email protected]","123456"); 正版登录
  25.         eventLoop.exec();

  26.         launchOptionsBuilder.setAuthInfo(authInfo);
  27.         launchOptionsBuilder.setJVMArguments("-XX:+UseG1GC -XX:-UseAdaptiveSizePolicy -XX:-OmitStackTraceInFastThrow");

  28.         launcher.generateLaunchCommand(gameDirectory.getVersion("1.11.2"), gameDirectory, launchOptionsBuilder.getLaunchOptions(), launchCommand);
复制代码
我的天,光是启动就要这么多的对象,还不支持自动识别 Java 路径... 显然这样的代码我是不能接受的,好,开始动手改造!
由于我对启动器的参数浑然不知,所以先来从它的启动参数生成类下手。
从 main.cpp 中可以看出,启动步骤先是在栈上创建了一个 QGSLauncher 对象,那么我们去看看 QGSLauncher 是如何实现的。
  1. class QGSLauncher : public QObject
  2. {
  3.         Q_OBJECT
  4. public:
  5.         QGSLauncher(QObject * parent = nullptr);
  6.         QGSLauncher(const QGSLauncher & right) = delete;
  7.         QGSLauncher(QGSLauncher && right) = delete;
  8.         QGSLauncher & operator=(const QGSLauncher & right) = delete;
  9.         QGSLauncher & operator=(QGSLauncher && right) = delete;
  10.         virtual ~QGSLauncher();
  11.         virtual QGSLauncherError::ErrorFlags generateLaunchCommand(const QGSGameVersion & version, QGSGameDirectory & gameDirectory, const QGSLaunchOptions * launchOptions, QString & command);
  12. }
复制代码
从头文件得知 QGSLauncher 是直接继承自 Qt 基类 QObject 的,且它明确指出了不允许同一时间内存在两个完全相同的 QGSLauncher 对象。C++11 标准允许将赋值操作符、构造函数、析构函数删除,这使得编译器不能够为我们生成默认的复制构造函数、构造函数和析构函数。可以看到生成启动参数的虚函数返回类型为错误类型,但由于 Minecode Studio 需要输出完整的启动信息,因此,ErrorFlags 是库中多余的类型,改成 void。Qt Framework 为开发者们提供了极为便捷的 signal/slots 机制,即 信号/槽 机制,相比回调函数,信号/槽 在可读性上更具优势(虽然在编译期间,它还是会被转换为回调函数)。既然如此,那就让它生成完启动参数以后发出一个信号,再将这个信号与我的启动器的主类 mclLauncher 相连。接着,我们可以看到 generateLaunchCommand 这个函数传入了4个参数,前三个被定义为 const ,最后一个却没有加 const 修饰符。
Tips : 根据 Effective C++ (第三版) [ISBN 978-7-121-12332-0] 的条款 03 "Use const whenever possible." 所建议的:const 是个奇妙且非比寻常的东西:在函数参数和返回类型身上 ... 林林总总不一而足。 const 是个威力强大的助手。尽可能使用它。

这是为何原因?我们先将这个疑惑放到一边,来看它的 .cpp 源文件:
  1. QGSLauncherError::ErrorFlags QGSLauncher::generateLaunchCommand(const QGSGameVersion & version, QGSGameDirectory & gameDirectory, const QGSLaunchOptions * launchOptions, QString & command)
  2. {
  3.         QGSGeneralLauncherStrategy launcherStrategy;

  4.         return launcherStrategy.generateLaunchCommand(version, gameDirectory, launchOptions, command);
  5. }
复制代码
函数的实现体内又出现了一个没有见到过的类 QGSGeneralLauncherStrategy ,继续跳到 QGSGeneralLauncherStrategy 的定义和实现:
  1. class QGSGeneralLauncherStrategy : public QGSILauncherStrategy
  2. {
  3. public:
  4.         QGSGeneralLauncherStrategy();
  5.         QGSGeneralLauncherStrategy(const QGSGeneralLauncherStrategy & right) = delete;
  6.         QGSGeneralLauncherStrategy(QGSGeneralLauncherStrategy && right) = delete;
  7.         QGSGeneralLauncherStrategy & operator=(const QGSGeneralLauncherStrategy & right) = delete;
  8.         QGSGeneralLauncherStrategy & operator=(QGSGeneralLauncherStrategy && right) = delete;
  9.         virtual ~QGSGeneralLauncherStrategy();

  10.         virtual QGSLauncherError::ErrorFlags generateLaunchCommand(const QGSGameVersion & version,
  11.                 QGSGameDirectory & gameDirectory,
  12.                 const QGSLaunchOptions * launchOptions,
  13.                 QString & command) override;

  14. };
复制代码
QGSGeneralLauncherStrategy 是继承自 QGSILauncherStrategy 基类的派生类,这显然会为后续的开发带来便捷:方便定义和插件化系统,但这也带来一个问题:虽然这有着很大的优势,但继承自 QGSILauncherStrategy 这个基类的派生类只有 QGSGeneralLauncherStrategy 这么一个,为了展现 C++ 继承特性而去写基类虽说不会带来内存上的消耗但是却对代码可读性带来很大影响。(试想你为了查找一个函数翻阅了无数个基类的定义文件才知道它的真正实现在哪,这必然会对开发效率带来影响)Mark 一下,加入到改造计划的 ToDo 列表里。
基类定义的虚函数如下:
  1. virtual QGSLauncherError::ErrorFlags generateLaunchCommand(const QGSGameVersion & version,
  2.                 QGSGameDirectory & gameDirectory,
  3.                 const QGSLaunchOptions * launchOptions,
  4.                 QString & command) override;
复制代码
很明确得可以看出 QGSGeneralLauncherStrategy::generateLaunchCommand 这个函数是实现了 QGSILauncherStrategy 中定义的生成启动参数函数的。
  1. QGSLauncherError::ErrorFlags QGSGeneralLauncherStrategy::generateLaunchCommand(const QGSGameVersion & version,
  2.         QGSGameDirectory & gameDirectory,
  3.         const QGSLaunchOptions * launchOptions,
  4.         QString & command)
  5. {
  6.         QGSLauncherError::ErrorFlags ret(QGSLauncherError::Ok);
  7.         if (!launchOptions)
  8.         {
  9.                 return ret |= QGSLauncherError::ErrorNullPointer;
  10.         }

  11.         QStringList launchCommandList;//launchCommand
  12.         auto rootVersionId(version.getId());
  13.         QGSGameVersion rootVersion;
  14.         //获取根版本
  15.         try
  16.         {
  17.                 while (!gameDirectory.getVersion(rootVersionId).getInheritsFrom().isEmpty())
  18.                 {
  19.                         rootVersionId = gameDirectory.getVersion(rootVersionId).getInheritsFrom();
  20.                 }
  21.                 rootVersion = gameDirectory.getVersion(rootVersionId);
  22.         }
  23.         catch (const QGSExceptionVersionNotFound & exception)
  24.         {
  25.                 return ret |= QGSLauncherError::ErrorJarFileNotFound;
  26.         }

  27.         //根版本Jar文件
  28.         QSharedPointer<QFile> rootVersionJarFile(gameDirectory.generateGameVersionJarFile(rootVersionId));
  29.         if (!rootVersionJarFile->exists())
  30.         {
  31.                 return ret |= QGSLauncherError::ErrorJarFileNotFound;
  32.         }
  33.         //前置指令
  34.         const auto && wrapper(launchOptions->getWrapper());
  35.         if (!wrapper.isEmpty())
  36.         {
  37.                 launchCommandList.append(wrapper);
  38.         }
  39.         //Java路径
  40.         const auto && JavaPath(launchOptions->getJavaPath());
  41.         if (JavaPath.isEmpty())
  42.         {
  43.                 ret |= QGSLauncherError::WarningJavaPathNotIncluded;
  44.         }
  45.         launchCommandList.append(QString(""%1"").arg(JavaPath));
  46.         //JVM虚拟机参数
  47.         if (launchOptions->getGeneratedJVMArguments())
  48.         {
  49.                 //自定义JVM虚拟机参数
  50.                 const auto && JVMArguments(launchOptions->getJVMArguments());
  51.                 if (!JVMArguments.isEmpty())
  52.                 {
  53.                         launchCommandList.append(JVMArguments);
  54.                 }
  55.                
  56.                 launchCommandList.append(QString(""-Dminecraft.client.jar=%1"").
  57.                         arg(rootVersionJarFile->fileName()));
  58.                 //最大内存(MB)
  59.                 launchCommandList.append(QString("-Xmx%1m").arg(launchOptions->getMaxMemory()));
  60.                 //最小内存(MB)
  61.                 launchCommandList.append(QString("-Xmn%1m").arg(launchOptions->getMinMemory()));
  62.                 //内存永久保存区域(MB)
  63.                 if (launchOptions->getMetaspaceSize() > 0)
  64.                 {
  65.                         launchCommandList.append(QString("-XX:PermSize=%1m").arg(launchOptions->getMetaspaceSize()));
  66.                 }
  67.                 //-Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
  68.                 launchCommandList.append("-Dfml.ignoreInvalidMinecraftCertificates=true");
  69.                 launchCommandList.append("-Dfml.ignorePatchDiscrepancies=true");
  70.                 //logging
  71.                 auto && loggingPath(launchOptions->getLoggingPath());
  72.                 if (!loggingPath.isEmpty())
  73.                 {
  74.                         auto && argument(rootVersion.getLogging().value("client").getArgument());
  75.                         if (!argument.isEmpty())
  76.                         {
  77.                                 launchCommandList.append(argument.replace("${path}", """ + loggingPath + """));
  78.                         }
  79.                 }
  80.         }
  81.         //新版json包含"arguments"属性
  82.         auto && arguments(rootVersion.getArguments());
  83.         auto && JVMList(arguments.getJVM());
  84.         if (!JVMList.isEmpty())
  85.         {
  86.                 for (auto & i : JVMList)
  87.                 {
  88.                         if (isRulesAllowing(i.getRules()))
  89.                         {
  90.                                 auto && valueList(i.getValue());
  91.                                 for (auto & j : valueList)
  92.                                 {
  93.                                         //防止value中等号后的值带有空格所导致的问题
  94.                                         int equalSignLocation = j.indexOf("=");
  95.                                         if (equalSignLocation != -1)
  96.                                         {
  97.                                                 if (j.right(equalSignLocation).contains(" "))
  98.                                                 {
  99.                                                         j = j.left(equalSignLocation + 1) + """ + j.mid(equalSignLocation + 1) + """;
  100.                                                 }
  101.                                         }
  102.                                         launchCommandList.append(j);
  103.                                 }
  104.                         }
  105.                 }
  106.         }
  107.         else
  108.         {
  109.                 launchCommandList.append("-Djava.library.path=${natives_directory}");
  110.                 launchCommandList.append("-Dminecraft.launcher.brand=${launcher_name}");
  111.                 launchCommandList.append("-Dminecraft.launcher.version=${launcher_version}");
  112.                 launchCommandList.append("-cp");
  113.                 launchCommandList.append("${classpath}");
  114.         }
  115.         auto && customMinecraftArguments(launchOptions->getCustomMinecraftArguments());
  116.         //natives目录
  117.         auto nativesDirectory(gameDirectory.generateNativesDirectory(version.getId()));
  118.         nativesDirectory.mkpath(nativesDirectory.absolutePath());
  119.         //launcherName
  120.         const QString launcherName(customMinecraftArguments.contains("${launcher_name}") ? customMinecraftArguments.value("${launcher_name}") : ""QGettingStarted"");
  121.         //launcherVersion
  122.         const QString launcherVersion(customMinecraftArguments.contains("${launcher_version}") ? customMinecraftArguments.value("${launcher_version}") : ""Pre 1.0.0"");
  123.         //libraries
  124.         auto inheritsVersionId(version.getId());
  125.         QStringList libraryPathList;
  126.         do
  127.         {
  128.                 auto & version(gameDirectory.getVersion(inheritsVersionId));
  129.                 auto && libraryList(version.getLibraries());

  130.                 //for (auto & i : libraryList)
  131.                 for (int i = 0; i < libraryList.size(); ++i)
  132.                 {
  133.                         if (!isRulesAllowing(libraryList[i].getRules()))
  134.                         {
  135.                                 continue;
  136.                         }

  137.                         QSharedPointer<QFile> fileLibrary(gameDirectory.generateLibraryFile(libraryList[i]));
  138.                         auto libraryPath(fileLibrary->fileName());
  139.                         if (libraryList[i].getNative())
  140.                         {
  141.                                 //解压natives
  142.                                 auto extractList(QGSFileTools::extractDirectory(libraryPath, nativesDirectory.absolutePath()));
  143.                                 if (extractList.isEmpty())
  144.                                 {
  145.                                         //throw QGSExceptionCompress(libraryPath, nativesDirectory.absolutePath());
  146.                                         ret |= QGSLauncherError::WarningNativesCompressError;
  147.                                 }

  148.                                 auto && excludeList(libraryList[i].getExtract().getExclude());
  149.                                 for (auto & exclude : excludeList)
  150.                                 {
  151.                                         if (!QGSFileTools::removeDirectory(nativesDirectory.absolutePath() + SEPARATOR + exclude))
  152.                                         {
  153.                                                 ret |= QGSLauncherError::WarningNativesCompressError;
  154.                                         }
  155.                                 }

  156.                                 continue;
  157.                         }

  158.                         libraryPathList.append(libraryPath);
  159.                 }

  160.                 QSharedPointer<QFile> fileInheritsVersionJar(gameDirectory.generateGameVersionJarFile(inheritsVersionId));
  161.                 libraryPathList.append(fileInheritsVersionJar->fileName());//版本".jar"文件
  162.         } while (!(inheritsVersionId = gameDirectory.getVersion(inheritsVersionId).getInheritsFrom()).isEmpty());
  163.         //mainClass
  164.         launchCommandList.append(version.getMainClass());
  165.         /*minecraftArguments*/
  166.         auto && game(arguments.getGame());
  167.         QString minecraftArguments;
  168.         if (!game.isEmpty())
  169.         {
  170.                 QStringList argumentList;
  171.                 for (auto & i : game)
  172.                 {
  173.                         if (i.getRules().getRules().isEmpty())
  174.                         {
  175.                                 argumentList.append(i.getValue());
  176.                         }
  177.                 }
  178.                 minecraftArguments = argumentList.join(" ");
  179.         }
  180.         else
  181.         {
  182.                 minecraftArguments = version.getMinecraftArguments();
  183.         }
  184.         auto && authInfo(launchOptions->getAuthInfo());

  185.         QDir assetsDirectory;
  186.         if (!gameDirectory.generateAssetsDirectory(rootVersionId, rootVersion.getAssetIndex(), assetsDirectory))
  187.         {
  188.                 ret |= QGSLauncherError::WarningAssetDirectoryGenerationFailure;
  189.         }
  190.         auto && assetsDirAbsolutePath(assetsDirectory.absolutePath());

  191.         auto && authPlayerName(authInfo.getSelectedProfile().getName());
  192.         if (authPlayerName.isEmpty())
  193.         {
  194.                 return ret |= QGSLauncherError::ErrorPlayerNameNotIncluded;
  195.         }

  196.         auto && authUuid(authInfo.getSelectedProfile().getId());
  197.         if (authUuid.isEmpty())
  198.         {
  199.                 return ret |= QGSLauncherError::ErrorAuthUuidNotIncluded;
  200.         }

  201.         auto && authAccessToken(authInfo.getAccessToken());
  202.         if (authAccessToken.isEmpty())
  203.         {
  204.                 return ret |= QGSLauncherError::ErrorAuthAccessTokenNotIncluded;
  205.         }

  206.         auto && userType(authInfo.getUserType());
  207.         if (userType.isEmpty())
  208.         {
  209.                 return ret |= QGSLauncherError::ErrorUserTypeNotIncluded;
  210.         }

  211.         minecraftArguments.replace("${auth_player_name}", customMinecraftArguments.contains("${auth_player_name}") ? customMinecraftArguments.value("${auth_player_name}") : authPlayerName)
  212.                 .replace("${version_name}", customMinecraftArguments.contains("${version_name}") ? customMinecraftArguments.value("${version_name}") : version.getId())
  213.                 .replace("${game_directory}", customMinecraftArguments.contains("${game_directory}") ? customMinecraftArguments.value("${game_directory}") : QString(""%1"").arg(gameDirectory.getBaseDir().absolutePath()))
  214.                 .replace("${assets_root}", customMinecraftArguments.contains("${assets_root}") ? customMinecraftArguments.value("${assets_root}") : QString(""%1"").arg(assetsDirAbsolutePath))
  215.                 .replace("${assets_index_name}", customMinecraftArguments.contains("${assets_index_name}") ? customMinecraftArguments.value("${assets_index_name}") : rootVersion.getAssets())
  216.                 .replace("${auth_uuid}", customMinecraftArguments.contains("${auth_uuid}") ? customMinecraftArguments.value("${auth_uuid}") : authUuid)
  217.                 .replace("${auth_access_token}", customMinecraftArguments.contains("${auth_access_token}") ? customMinecraftArguments.value("${auth_access_token}") : authAccessToken)
  218.                 .replace("${user_type}", customMinecraftArguments.contains("${user_type}") ? customMinecraftArguments.value("${user_type}") : userType)
  219.                 .replace("${version_type}", customMinecraftArguments.contains("${version_type}") ? customMinecraftArguments.value("${version_type}") : ""QGettingStarted"")
  220.                 .replace("${user_properties}", customMinecraftArguments.contains("${user_properties}") ? customMinecraftArguments.value("${user_properties}") : authInfo.getTwitchAccessToken())
  221.                 .replace("${auth_session}", customMinecraftArguments.contains("${auth_session}") ? customMinecraftArguments.value("${auth_session}") : authAccessToken)
  222.                 .replace("${game_assets}", customMinecraftArguments.contains("${game_assets}") ? customMinecraftArguments.value("${game_assets}") : QString(""%1"").arg(assetsDirAbsolutePath))
  223.                 .replace("${profile_name}", customMinecraftArguments.contains("${profile_name}") ? customMinecraftArguments.value("${profile_name}") : "QGettingStarted");
  224.         launchCommandList.append(minecraftArguments);
  225.         //窗口大小
  226.         auto && windowSize(launchOptions->getWindowSize());
  227.         if (!windowSize.isEmpty())
  228.         {
  229.                 launchCommandList.append(QString("--height %1").arg(windowSize.height()));
  230.                 launchCommandList.append(QString("--width %1").arg(windowSize.width()));
  231.         }
  232.         //直连服务器
  233.         auto && serverInfo(launchOptions->getServerInfo());
  234.         if (!serverInfo.getAddress().isEmpty() && !serverInfo.getPort().isEmpty())
  235.         {
  236.                 launchCommandList.append(QString("--server %1").arg(serverInfo.getAddress()));
  237.                 launchCommandList.append(QString("--port %1").arg(serverInfo.getPort()));
  238.         }
  239.         //代理
  240.         auto && proxy(launchOptions->getProxy());
  241.         if (proxy != QNetworkProxy::NoProxy && !proxy.hostName().isEmpty())
  242.         {
  243.                 launchCommandList.append(QString("--proxyHost %1").arg(proxy.hostName()));
  244.                 launchCommandList.append(QString("--proxyPort %1").arg(proxy.port()));
  245.                 if (!proxy.user().isEmpty() && !proxy.password().isEmpty())
  246.                 {
  247.                         launchCommandList.append(QString("--proxyUser %1").arg(proxy.user()));
  248.                         launchCommandList.append(QString("--proxyPass %1").arg(proxy.password()));
  249.                 }
  250.         }
  251.         //游戏额**数
  252.         auto gameArguments(launchOptions->getGameArguments());
  253.         if (!gameArguments.isEmpty())
  254.         {
  255.                 launchCommandList.append(gameArguments);
  256.         }

  257.         command = launchCommandList.join(" ")
  258.                 .replace("${natives_directory}", """ + nativesDirectory.absolutePath() + """)
  259.                 .replace("${launcher_name}", launcherName)
  260.                 .replace("${launcher_version}", launcherVersion)
  261.                 .replace("${classpath}", QString(""%1"").arg(libraryPathList.join(";")));
  262.         return ret;
  263. }
复制代码
从上面的实现代码中我们就可以得到先前所遗留下来的疑问:为什么 command 这个形参前没有加 const 修饰符 —— 因为函数的内部实现在生成参数的时候将参数输出到了 QString command 对象上。

由此我们可以得到启动参数的普遍规律:
<JVM 虚拟机路径> -Dminecraft.client.jar= <版本的 jar 文件> -Xmx<最大内存><数据单位> -Xmn<最小内存><数据单位> -XX:PermSize=<最大内存><数据单位> -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true -Djava.library.path=<库路径> -Dminecraft.launcher.brand=<启动器名称> -Dminecraft.launcher.version=<启动器版本> -cp <依赖库路径> --height <游戏初始窗口高度> --width <游戏初始窗口高度> --server <直连服务器 IP> --port <直连服务器端口> ...

当然可选的参数还有很多,但对启动器的开发来说,已经足够了。
关于参数的模板格式可以参照 http://www.mcbbs.net/thread-704424-1-1.html 来看。
今天的改造记先写到这里,下一章我会来讲述如何实现更高阶的启动器功能(库文件格式、Json 文件格式、版本大小判断 [不用 json] 等等)
欢迎大家继续关注我的 Minecraft 启动器核心改造记 连载系列。

打开有惊喜↓


2021.12 数据,可能有更多内容



鉴于此类文章的特殊性,我暂且将它放到茶馆上发布,此后这个系列将会搬运到我的个人博客上。
Blog : 我的开发者博客有任何建议或想法的请联系我 Email : [email protected]



Minecraft 启动器核心改造记 (一)
最近我的 Minecode Studio 项目需要启动器来支持代码的调试和运行,本想着能够在 mcbbs 上找到用 C++ 写成的启动器动态库,可找了一圈就只找到了一个 C++ 静态库:QGettingStarted,于是乎 git clone 大法把它克隆到本地。



库结构如上图所示
翻阅它的 README.md 得知它的启动方法如下:

代码:

  1. QString launchCommand;
  2.         QGSLauncher launcher;
  3.         QGSGameDirectory gameDirectory(QDir("./minecraft"));
  4.         QGSLaunchOptionsBuilder launchOptionsBuilder;

  5.         launchOptionsBuilder.setJavaPath("C:/Program Files/Java/jre1.8.0_162/bin/javaw.exe");
  6.         launchOptionsBuilder.setMaxMemory(1024);
  7.         launchOptionsBuilder.setMinMemory(128);

  8.         QGSAuthInfo authInfo;
  9.         QEventLoop eventLoop;
  10.         //正版登录替换为QGSYggdrasilAccountFactory
  11.         QGSIAccount * account(QGSOfflineAccountFactory().createAccount());
  12.         QObject::connect(account, &QGSIAccount::finished, &eventLoop, &QEventLoop::quit);
  13.         QObject::connect(account, &QGSIAccount::finished, [&authInfo](QGSAuthInfo _authInfo)
  14.         {
  15.                 authInfo = _authInfo;
  16.         });
  17.         /*正版登录的错误检查
  18.         QObject::connect(account, &QGSIAccount::error, [](QGSNetworkError networkError)
  19.         {
  20.                 qDebug() << "QGSNetworkError:" << networkError.getCode() << networkError.getErrorString();
  21.         });
  22.         */
  23.         account->authenticate("player");
  24.         //account->authenticate("[email protected]","123456"); 正版登录
  25.         eventLoop.exec();

  26.         launchOptionsBuilder.setAuthInfo(authInfo);
  27.         launchOptionsBuilder.setJVMArguments("-XX:+UseG1GC -XX:-UseAdaptiveSizePolicy -XX:-OmitStackTraceInFastThrow");

  28.         launcher.generateLaunchCommand(gameDirectory.getVersion("1.11.2"), gameDirectory, launchOptionsBuilder.getLaunchOptions(), launchCommand);
我的天,光是启动就要这么多的对象,还不支持自动识别 Java 路径... 显然这样的代码我是不能接受的,好,开始动手改造!
由于我对启动器的参数浑然不知,所以先来从它的启动参数生成类下手。
从 main.cpp 中可以看出,启动步骤先是在栈上创建了一个 QGSLauncher 对象,那么我们去看看 QGSLauncher 是如何实现的。

代码:

  1. class QGSLauncher : public QObject
  2. {
  3.         Q_OBJECT
  4. public:
  5.         QGSLauncher(QObject * parent = nullptr);
  6.         QGSLauncher(const QGSLauncher & right) = delete;
  7.         QGSLauncher(QGSLauncher && right) = delete;
  8.         QGSLauncher & operator=(const QGSLauncher & right) = delete;
  9.         QGSLauncher & operator=(QGSLauncher && right) = delete;
  10.         virtual ~QGSLauncher();
  11.         virtual QGSLauncherError::ErrorFlags generateLaunchCommand(const QGSGameVersion & version, QGSGameDirectory & gameDirectory, const QGSLaunchOptions * launchOptions, QString & command);
  12. }
从头文件得知 QGSLauncher 是直接继承自 Qt 基类 QObject 的,且它明确指出了不允许同一时间内存在两个完全相同的 QGSLauncher 对象。C++11 标准允许将赋值操作符、构造函数、析构函数删除,这使得编译器不能够为我们生成默认的复制构造函数、构造函数和析构函数。可以看到生成启动参数的虚函数返回类型为错误类型,但由于 Minecode Studio 需要输出完整的启动信息,因此,ErrorFlags 是库中多余的类型,改成 void。Qt Framework 为开发者们提供了极为便捷的 signal/slots 机制,即 信号/槽 机制,相比回调函数,信号/槽 在可读性上更具优势(虽然在编译期间,它还是会被转换为回调函数)。既然如此,那就让它生成完启动参数以后发出一个信号,再将这个信号与我的启动器的主类 mclLauncher 相连。接着,我们可以看到 generateLaunchCommand 这个函数传入了4个参数,前三个被定义为 const ,最后一个却没有加 const 修饰符。
Tips : 根据 Effective C++ (第三版) [ISBN 978-7-121-12332-0] 的条款 03 &quot;Use const whenever possible.&quot; 所建议的:const 是个奇妙且非比寻常的东西:在函数参数和返回类型身上 ... 林林总总不一而足。 const 是个威力强大的助手。尽可能使用它。

这是为何原因?我们先将这个疑惑放到一边,来看它的 .cpp 源文件:

代码:

  1. QGSLauncherError::ErrorFlags QGSLauncher::generateLaunchCommand(const QGSGameVersion & version, QGSGameDirectory & gameDirectory, const QGSLaunchOptions * launchOptions, QString & command)
  2. {
  3.         QGSGeneralLauncherStrategy launcherStrategy;

  4.         return launcherStrategy.generateLaunchCommand(version, gameDirectory, launchOptions, command);
  5. }
函数的实现体内又出现了一个没有见到过的类 QGSGeneralLauncherStrategy ,继续跳到 QGSGeneralLauncherStrategy 的定义和实现:

代码:

  1. class QGSGeneralLauncherStrategy : public QGSILauncherStrategy
  2. {
  3. public:
  4.         QGSGeneralLauncherStrategy();
  5.         QGSGeneralLauncherStrategy(const QGSGeneralLauncherStrategy & right) = delete;
  6.         QGSGeneralLauncherStrategy(QGSGeneralLauncherStrategy && right) = delete;
  7.         QGSGeneralLauncherStrategy & operator=(const QGSGeneralLauncherStrategy & right) = delete;
  8.         QGSGeneralLauncherStrategy & operator=(QGSGeneralLauncherStrategy && right) = delete;
  9.         virtual ~QGSGeneralLauncherStrategy();

  10.         virtual QGSLauncherError::ErrorFlags generateLaunchCommand(const QGSGameVersion & version,
  11.                 QGSGameDirectory & gameDirectory,
  12.                 const QGSLaunchOptions * launchOptions,
  13.                 QString & command) override;

  14. };
QGSGeneralLauncherStrategy 是继承自 QGSILauncherStrategy 基类的派生类,这显然会为后续的开发带来便捷:方便定义和插件化系统,但这也带来一个问题:虽然这有着很大的优势,但继承自 QGSILauncherStrategy 这个基类的派生类只有 QGSGeneralLauncherStrategy 这么一个,为了展现 C++ 继承特性而去写基类虽说不会带来内存上的消耗但是却对代码可读性带来很大影响。(试想你为了查找一个函数翻阅了无数个基类的定义文件才知道它的真正实现在哪,这必然会对开发效率带来影响)Mark 一下,加入到改造计划的 ToDo 列表里。
基类定义的虚函数如下:

代码:

  1. virtual QGSLauncherError::ErrorFlags generateLaunchCommand(const QGSGameVersion & version,
  2.                 QGSGameDirectory & gameDirectory,
  3.                 const QGSLaunchOptions * launchOptions,
  4.                 QString & command) override;
很明确得可以看出 QGSGeneralLauncherStrategy::generateLaunchCommand 这个函数是实现了 QGSILauncherStrategy 中定义的生成启动参数函数的。

代码:

  1. QGSLauncherError::ErrorFlags QGSGeneralLauncherStrategy::generateLaunchCommand(const QGSGameVersion & version,
  2.         QGSGameDirectory & gameDirectory,
  3.         const QGSLaunchOptions * launchOptions,
  4.         QString & command)
  5. {
  6.         QGSLauncherError::ErrorFlags ret(QGSLauncherError::Ok);
  7.         if (!launchOptions)
  8.         {
  9.                 return ret |= QGSLauncherError::ErrorNullPointer;
  10.         }

  11.         QStringList launchCommandList;//launchCommand
  12.         auto rootVersionId(version.getId());
  13.         QGSGameVersion rootVersion;
  14.         //获取根版本
  15.         try
  16.         {
  17.                 while (!gameDirectory.getVersion(rootVersionId).getInheritsFrom().isEmpty())
  18.                 {
  19.                         rootVersionId = gameDirectory.getVersion(rootVersionId).getInheritsFrom();
  20.                 }
  21.                 rootVersion = gameDirectory.getVersion(rootVersionId);
  22.         }
  23.         catch (const QGSExceptionVersionNotFound & exception)
  24.         {
  25.                 return ret |= QGSLauncherError::ErrorJarFileNotFound;
  26.         }

  27.         //根版本Jar文件
  28.         QSharedPointer<QFile> rootVersionJarFile(gameDirectory.generateGameVersionJarFile(rootVersionId));
  29.         if (!rootVersionJarFile->exists())
  30.         {
  31.                 return ret |= QGSLauncherError::ErrorJarFileNotFound;
  32.         }
  33.         //前置指令
  34.         const auto && wrapper(launchOptions->getWrapper());
  35.         if (!wrapper.isEmpty())
  36.         {
  37.                 launchCommandList.append(wrapper);
  38.         }
  39.         //Java路径
  40.         const auto && JavaPath(launchOptions->getJavaPath());
  41.         if (JavaPath.isEmpty())
  42.         {
  43.                 ret |= QGSLauncherError::WarningJavaPathNotIncluded;
  44.         }
  45.         launchCommandList.append(QString(""%1"").arg(JavaPath));
  46.         //JVM虚拟机参数
  47.         if (launchOptions->getGeneratedJVMArguments())
  48.         {
  49.                 //自定义JVM虚拟机参数
  50.                 const auto && JVMArguments(launchOptions->getJVMArguments());
  51.                 if (!JVMArguments.isEmpty())
  52.                 {
  53.                         launchCommandList.append(JVMArguments);
  54.                 }
  55.                
  56.                 launchCommandList.append(QString(""-Dminecraft.client.jar=%1"").
  57.                         arg(rootVersionJarFile->fileName()));
  58.                 //最大内存(MB)
  59.                 launchCommandList.append(QString("-Xmx%1m").arg(launchOptions->getMaxMemory()));
  60.                 //最小内存(MB)
  61.                 launchCommandList.append(QString("-Xmn%1m").arg(launchOptions->getMinMemory()));
  62.                 //内存永久保存区域(MB)
  63.                 if (launchOptions->getMetaspaceSize() > 0)
  64.                 {
  65.                         launchCommandList.append(QString("-XX:PermSize=%1m").arg(launchOptions->getMetaspaceSize()));
  66.                 }
  67.                 //-Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true
  68.                 launchCommandList.append("-Dfml.ignoreInvalidMinecraftCertificates=true");
  69.                 launchCommandList.append("-Dfml.ignorePatchDiscrepancies=true");
  70.                 //logging
  71.                 auto && loggingPath(launchOptions->getLoggingPath());
  72.                 if (!loggingPath.isEmpty())
  73.                 {
  74.                         auto && argument(rootVersion.getLogging().value("client").getArgument());
  75.                         if (!argument.isEmpty())
  76.                         {
  77.                                 launchCommandList.append(argument.replace("${path}", """ + loggingPath + """));
  78.                         }
  79.                 }
  80.         }
  81.         //新版json包含"arguments"属性
  82.         auto && arguments(rootVersion.getArguments());
  83.         auto && JVMList(arguments.getJVM());
  84.         if (!JVMList.isEmpty())
  85.         {
  86.                 for (auto & i : JVMList)
  87.                 {
  88.                         if (isRulesAllowing(i.getRules()))
  89.                         {
  90.                                 auto && valueList(i.getValue());
  91.                                 for (auto & j : valueList)
  92.                                 {
  93.                                         //防止value中等号后的值带有空格所导致的问题
  94.                                         int equalSignLocation = j.indexOf("=");
  95.                                         if (equalSignLocation != -1)
  96.                                         {
  97.                                                 if (j.right(equalSignLocation).contains(" "))
  98.                                                 {
  99.                                                         j = j.left(equalSignLocation + 1) + """ + j.mid(equalSignLocation + 1) + """;
  100.                                                 }
  101.                                         }
  102.                                         launchCommandList.append(j);
  103.                                 }
  104.                         }
  105.                 }
  106.         }
  107.         else
  108.         {
  109.                 launchCommandList.append("-Djava.library.path=${natives_directory}");
  110.                 launchCommandList.append("-Dminecraft.launcher.brand=${launcher_name}");
  111.                 launchCommandList.append("-Dminecraft.launcher.version=${launcher_version}");
  112.                 launchCommandList.append("-cp");
  113.                 launchCommandList.append("${classpath}");
  114.         }
  115.         auto && customMinecraftArguments(launchOptions->getCustomMinecraftArguments());
  116.         //natives目录
  117.         auto nativesDirectory(gameDirectory.generateNativesDirectory(version.getId()));
  118.         nativesDirectory.mkpath(nativesDirectory.absolutePath());
  119.         //launcherName
  120.         const QString launcherName(customMinecraftArguments.contains("${launcher_name}") ? customMinecraftArguments.value("${launcher_name}") : ""QGettingStarted"");
  121.         //launcherVersion
  122.         const QString launcherVersion(customMinecraftArguments.contains("${launcher_version}") ? customMinecraftArguments.value("${launcher_version}") : ""Pre 1.0.0"");
  123.         //libraries
  124.         auto inheritsVersionId(version.getId());
  125.         QStringList libraryPathList;
  126.         do
  127.         {
  128.                 auto & version(gameDirectory.getVersion(inheritsVersionId));
  129.                 auto && libraryList(version.getLibraries());

  130.                 //for (auto & i : libraryList)
  131.                 for (int i = 0; i < libraryList.size(); ++i)
  132.                 {
  133.                         if (!isRulesAllowing(libraryList.getRules()))
  134.                         {
  135.                                 continue;
  136.                         }

  137.                         QSharedPointer<QFile> fileLibrary(gameDirectory.generateLibraryFile(libraryList));
  138.                         auto libraryPath(fileLibrary->fileName());
  139.                         if (libraryList.getNative())
  140.                         {
  141.                                 //解压natives
  142.                                 auto extractList(QGSFileTools::extractDirectory(libraryPath, nativesDirectory.absolutePath()));
  143.                                 if (extractList.isEmpty())
  144.                                 {
  145.                                         //throw QGSExceptionCompress(libraryPath, nativesDirectory.absolutePath());
  146.                                         ret |= QGSLauncherError::WarningNativesCompressError;
  147.                                 }

  148.                                 auto && excludeList(libraryList.getExtract().getExclude());
  149.                                 for (auto & exclude : excludeList)
  150.                                 {
  151.                                         if (!QGSFileTools::removeDirectory(nativesDirectory.absolutePath() + SEPARATOR + exclude))
  152.                                         {
  153.                                                 ret |= QGSLauncherError::WarningNativesCompressError;
  154.                                         }
  155.                                 }

  156.                                 continue;
  157.                         }

  158.                         libraryPathList.append(libraryPath);
  159.                 }

  160.                 QSharedPointer<QFile> fileInheritsVersionJar(gameDirectory.generateGameVersionJarFile(inheritsVersionId));
  161.                 libraryPathList.append(fileInheritsVersionJar->fileName());//版本".jar"文件
  162.         } while (!(inheritsVersionId = gameDirectory.getVersion(inheritsVersionId).getInheritsFrom()).isEmpty());
  163.         //mainClass
  164.         launchCommandList.append(version.getMainClass());
  165.         /*minecraftArguments*/
  166.         auto && game(arguments.getGame());
  167.         QString minecraftArguments;
  168.         if (!game.isEmpty())
  169.         {
  170.                 QStringList argumentList;
  171.                 for (auto & i : game)
  172.                 {
  173.                         if (i.getRules().getRules().isEmpty())
  174.                         {
  175.                                 argumentList.append(i.getValue());
  176.                         }
  177.                 }
  178.                 minecraftArguments = argumentList.join(" ");
  179.         }
  180.         else
  181.         {
  182.                 minecraftArguments = version.getMinecraftArguments();
  183.         }
  184.         auto && authInfo(launchOptions->getAuthInfo());

  185.         QDir assetsDirectory;
  186.         if (!gameDirectory.generateAssetsDirectory(rootVersionId, rootVersion.getAssetIndex(), assetsDirectory))
  187.         {
  188.                 ret |= QGSLauncherError::WarningAssetDirectoryGenerationFailure;
  189.         }
  190.         auto && assetsDirAbsolutePath(assetsDirectory.absolutePath());

  191.         auto && authPlayerName(authInfo.getSelectedProfile().getName());
  192.         if (authPlayerName.isEmpty())
  193.         {
  194.                 return ret |= QGSLauncherError::ErrorPlayerNameNotIncluded;
  195.         }

  196.         auto && authUuid(authInfo.getSelectedProfile().getId());
  197.         if (authUuid.isEmpty())
  198.         {
  199.                 return ret |= QGSLauncherError::ErrorAuthUuidNotIncluded;
  200.         }

  201.         auto && authAccessToken(authInfo.getAccessToken());
  202.         if (authAccessToken.isEmpty())
  203.         {
  204.                 return ret |= QGSLauncherError::ErrorAuthAccessTokenNotIncluded;
  205.         }

  206.         auto && userType(authInfo.getUserType());
  207.         if (userType.isEmpty())
  208.         {
  209.                 return ret |= QGSLauncherError::ErrorUserTypeNotIncluded;
  210.         }

  211.         minecraftArguments.replace("${auth_player_name}", customMinecraftArguments.contains("${auth_player_name}") ? customMinecraftArguments.value("${auth_player_name}") : authPlayerName)
  212.                 .replace("${version_name}", customMinecraftArguments.contains("${version_name}") ? customMinecraftArguments.value("${version_name}") : version.getId())
  213.                 .replace("${game_directory}", customMinecraftArguments.contains("${game_directory}") ? customMinecraftArguments.value("${game_directory}") : QString(""%1"").arg(gameDirectory.getBaseDir().absolutePath()))
  214.                 .replace("${assets_root}", customMinecraftArguments.contains("${assets_root}") ? customMinecraftArguments.value("${assets_root}") : QString(""%1"").arg(assetsDirAbsolutePath))
  215.                 .replace("${assets_index_name}", customMinecraftArguments.contains("${assets_index_name}") ? customMinecraftArguments.value("${assets_index_name}") : rootVersion.getAssets())
  216.                 .replace("${auth_uuid}", customMinecraftArguments.contains("${auth_uuid}") ? customMinecraftArguments.value("${auth_uuid}") : authUuid)
  217.                 .replace("${auth_access_token}", customMinecraftArguments.contains("${auth_access_token}") ? customMinecraftArguments.value("${auth_access_token}") : authAccessToken)
  218.                 .replace("${user_type}", customMinecraftArguments.contains("${user_type}") ? customMinecraftArguments.value("${user_type}") : userType)
  219.                 .replace("${version_type}", customMinecraftArguments.contains("${version_type}") ? customMinecraftArguments.value("${version_type}") : ""QGettingStarted"")
  220.                 .replace("${user_properties}", customMinecraftArguments.contains("${user_properties}") ? customMinecraftArguments.value("${user_properties}") : authInfo.getTwitchAccessToken())
  221.                 .replace("${auth_session}", customMinecraftArguments.contains("${auth_session}") ? customMinecraftArguments.value("${auth_session}") : authAccessToken)
  222.                 .replace("${game_assets}", customMinecraftArguments.contains("${game_assets}") ? customMinecraftArguments.value("${game_assets}") : QString(""%1"").arg(assetsDirAbsolutePath))
  223.                 .replace("${profile_name}", customMinecraftArguments.contains("${profile_name}") ? customMinecraftArguments.value("${profile_name}") : "QGettingStarted");
  224.         launchCommandList.append(minecraftArguments);
  225.         //窗口大小
  226.         auto && windowSize(launchOptions->getWindowSize());
  227.         if (!windowSize.isEmpty())
  228.         {
  229.                 launchCommandList.append(QString("--height %1").arg(windowSize.height()));
  230.                 launchCommandList.append(QString("--width %1").arg(windowSize.width()));
  231.         }
  232.         //直连服务器
  233.         auto && serverInfo(launchOptions->getServerInfo());
  234.         if (!serverInfo.getAddress().isEmpty() && !serverInfo.getPort().isEmpty())
  235.         {
  236.                 launchCommandList.append(QString("--server %1").arg(serverInfo.getAddress()));
  237.                 launchCommandList.append(QString("--port %1").arg(serverInfo.getPort()));
  238.         }
  239.         //代理
  240.         auto && proxy(launchOptions->getProxy());
  241.         if (proxy != QNetworkProxy::NoProxy && !proxy.hostName().isEmpty())
  242.         {
  243.                 launchCommandList.append(QString("--proxyHost %1").arg(proxy.hostName()));
  244.                 launchCommandList.append(QString("--proxyPort %1").arg(proxy.port()));
  245.                 if (!proxy.user().isEmpty() && !proxy.password().isEmpty())
  246.                 {
  247.                         launchCommandList.append(QString("--proxyUser %1").arg(proxy.user()));
  248.                         launchCommandList.append(QString("--proxyPass %1").arg(proxy.password()));
  249.                 }
  250.         }
  251.         //游戏额**数
  252.         auto gameArguments(launchOptions->getGameArguments());
  253.         if (!gameArguments.isEmpty())
  254.         {
  255.                 launchCommandList.append(gameArguments);
  256.         }

  257.         command = launchCommandList.join(" ")
  258.                 .replace("${natives_directory}", """ + nativesDirectory.absolutePath() + """)
  259.                 .replace("${launcher_name}", launcherName)
  260.                 .replace("${launcher_version}", launcherVersion)
  261.                 .replace("${classpath}", QString(""%1"").arg(libraryPathList.join(";")));
  262.         return ret;
  263. }
从上面的实现代码中我们就可以得到先前所遗留下来的疑问:为什么 command 这个形参前没有加 const 修饰符 —— 因为函数的内部实现在生成参数的时候将参数输出到了 QString command 对象上。


由此我们可以得到启动参数的普遍规律:
&lt;JVM 虚拟机路径&gt; -Dminecraft.client.jar= &lt;版本的 jar 文件&gt; -Xmx&lt;最大内存&gt;&lt;数据单位&gt; -Xmn&lt;最小内存&gt;&lt;数据单位&gt; -XX:PermSize=&lt;最大内存&gt;&lt;数据单位&gt; -Dfml.ignoreInvalidMinecraftCertificates=true -Dfml.ignorePatchDiscrepancies=true -Djava.library.path=&lt;库路径&gt; -Dminecraft.launcher.brand=&lt;启动器名称&gt; -Dminecraft.launcher.version=&lt;启动器版本&gt; -cp &lt;依赖库路径&gt; --height &lt;游戏初始窗口高度&gt; --width &lt;游戏初始窗口高度&gt; --server &lt;直连服务器 IP&gt; --port &lt;直连服务器端口&gt; ...


当然可选的参数还有很多,但对启动器的开发来说,已经足够了。
关于参数的模板格式可以参照 http://www.mcbbs.net/thread-704424-1-1.html 来看。
今天的改造记先写到这里,下一章我会来讲述如何实现更高阶的启动器功能(库文件格式、Json 文件格式、版本大小判断 [不用 json] 等等)
欢迎大家继续关注我的 Minecraft 启动器核心改造记 连载系列。


打开有惊喜↓
也请有钱的观众老爷们能赞赏我,开发不易,赏个饭钱吧


   




Abraham511
~~

逆天皮皮虾
楼主太复杂了,我看的迷迷糊糊的

卡尔酱
你这个太复杂了,根本看不懂

虎水小骥

tql sdl awsl

(不明觉厉


二哈大魔王
虽然看不懂 但是好像很厉害的样子 。。

星卡比
看不懂看不懂

@TGL
举个用bat启动客户端的例子,否则不好理解