这一章我们要来教如何获取cp后面跟随的${classpath}值。

想必,大家在看过了启动参数后,就会发现怎么这么长?

其实,启动参数中绝大部分的篇幅都被${classpath}这个字符串模板的替换给占掉了。

让我们细看一下1.19.4的启动参数吧!【ps:下面我只列举出cp后面的东西。我自己用手格式化了一遍。】

-cp "
<MC原路径>\libraries\com\github\oshi\oshi-core\6.2.2\oshi-core-6.2.2.jar;
<MC原路径>\libraries\com\google\code\gson\gson\2.10\gson-2.10.jar;
<MC原路径>\libraries\com\google\guava\failureaccess\1.0.1\failureaccess-1.0.1.jar;
<MC原路径>\libraries\com\google\guava\guava\31.1-jre\guava-31.1-jre.jar;
<MC原路径>\libraries\com\ibm\icu\icu4j\71.1\icu4j-71.1.jar;
<MC原路径>\libraries\com\mojang\authlib\3.18.38\authlib-3.18.38.jar;
<MC原路径>\libraries\com\mojang\blocklist\1.0.10\blocklist-1.0.10.jar;
<MC原路径>\libraries\com\mojang\brigadier\1.0.18\brigadier-1.0.18.jar;
<MC原路径>\libraries\com\mojang\datafixerupper\6.0.6\datafixerupper-6.0.6.jar;
<MC原路径>\libraries\com\mojang\logging\1.1.1\logging-1.1.1.jar;
<MC原路径>\libraries\com\mojang\patchy\2.2.10\patchy-2.2.10.jar;
<MC原路径>\libraries\com\mojang\text2speech\1.13.9\text2speech-1.13.9.jar;
<MC原路径>\libraries\com\mojang\text2speech\1.13.9\text2speech-1.13.9-natives-windows.jar;
<MC原路径>\libraries\commons-codec\commons-codec\1.15\commons-codec-1.15.jar;
<MC原路径>\libraries\commons-io\commons-io\2.11.0\commons-io-2.11.0.jar;
<MC原路径>\libraries\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;
<MC原路径>\libraries\io\netty\netty-buffer\4.1.82.Final\netty-buffer-4.1.82.Final.jar;
<MC原路径>\libraries\io\netty\netty-codec\4.1.82.Final\netty-codec-4.1.82.Final.jar;
<MC原路径>\libraries\io\netty\netty-common\4.1.82.Final\netty-common-4.1.82.Final.jar;
<MC原路径>\libraries\io\netty\netty-handler\4.1.82.Final\netty-handler-4.1.82.Final.jar;
<MC原路径>\libraries\io\netty\netty-resolver\4.1.82.Final\netty-resolver-4.1.82.Final.jar;
<MC原路径>\libraries\io\netty\netty-transport-classes-epoll\4.1.82.Final\netty-transport-classes-epoll-4.1.82.Final.jar;
<MC原路径>\libraries\io\netty\netty-transport-native-unix-common\4.1.82.Final\netty-transport-native-unix-common-4.1.82.Final.jar;
<MC原路径>\libraries\io\netty\netty-transport\4.1.82.Final\netty-transport-4.1.82.Final.jar;
<MC原路径>\libraries\it\unimi\dsi\fastutil\8.5.9\fastutil-8.5.9.jar;
<MC原路径>\libraries\net\java\dev\jna\jna-platform\5.12.1\jna-platform-5.12.1.jar;
<MC原路径>\libraries\net\java\dev\jna\jna\5.12.1\jna-5.12.1.jar;
<MC原路径>\libraries\net\sf\jopt-simple\jopt-simple\5.0.4\jopt-simple-5.0.4.jar;
<MC原路径>\libraries\org\apache\commons\commons-compress\1.21\commons-compress-1.21.jar;
<MC原路径>\libraries\org\apache\commons\commons-lang3\3.12.0\commons-lang3-3.12.0.jar;
<MC原路径>\libraries\org\apache\httpcomponents\httpclient\4.5.13\httpclient-4.5.13.jar;
<MC原路径>\libraries\org\apache\httpcomponents\httpcore\4.4.15\httpcore-4.4.15.jar;
<MC原路径>\libraries\org\apache\logging\log4j\log4j-api\2.19.0\log4j-api-2.19.0.jar;
<MC原路径>\libraries\org\apache\logging\log4j\log4j-core\2.19.0\log4j-core-2.19.0.jar;
<MC原路径>\libraries\org\apache\logging\log4j\log4j-slf4j2-impl\2.19.0\log4j-slf4j2-impl-2.19.0.jar;
<MC原路径>\libraries\org\joml\joml\1.10.5\joml-1.10.5.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-glfw\3.3.1\lwjgl-glfw-3.3.1.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-glfw\3.3.1\lwjgl-glfw-3.3.1-natives-windows.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-glfw\3.3.1\lwjgl-glfw-3.3.1-natives-windows-x86.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-jemalloc\3.3.1\lwjgl-jemalloc-3.3.1.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-jemalloc\3.3.1\lwjgl-jemalloc-3.3.1-natives-windows.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-jemalloc\3.3.1\lwjgl-jemalloc-3.3.1-natives-windows-x86.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-openal\3.3.1\lwjgl-openal-3.3.1.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-openal\3.3.1\lwjgl-openal-3.3.1-natives-windows.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-openal\3.3.1\lwjgl-openal-3.3.1-natives-windows-x86.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-opengl\3.3.1\lwjgl-opengl-3.3.1.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-opengl\3.3.1\lwjgl-opengl-3.3.1-natives-windows.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-opengl\3.3.1\lwjgl-opengl-3.3.1-natives-windows-x86.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-stb\3.3.1\lwjgl-stb-3.3.1.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-stb\3.3.1\lwjgl-stb-3.3.1-natives-windows.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-stb\3.3.1\lwjgl-stb-3.3.1-natives-windows-x86.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-tinyfd\3.3.1\lwjgl-tinyfd-3.3.1.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-tinyfd\3.3.1\lwjgl-tinyfd-3.3.1-natives-windows.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl-tinyfd\3.3.1\lwjgl-tinyfd-3.3.1-natives-windows-x86.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl\3.3.1\lwjgl-3.3.1.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl\3.3.1\lwjgl-3.3.1-natives-windows.jar;
<MC原路径>\libraries\org\lwjgl\lwjgl\3.3.1\lwjgl-3.3.1-natives-windows-x86.jar;
<MC原路径>\libraries\org\slf4j\slf4j-api\2.0.1\slf4j-api-2.0.1.jar;
<MC原路径>\versions\1.19.4\1.19.4.jar //请注意,我们看到这里有一个在versions,也就是版本文件夹下面的主类jar也被包含进了cp-libraries库,在下面我会和大家具体说明这种情况的。
"

我们仔细的看看这双引号内的内容,我们会发现这其中包含了许许多多的类库。其中全部都是来自原版Json文件中的【libraries】键值下的类库。

反观libraries,这是一个列表元素,里面包含了很多对象,我们可以使用for循环遍历这个列表,然后挨个取出里面的对象,然后挨个进行分析即可了!

首先,依旧的,我们写一个函数,这个函数名字叫做:【GetCPLibraries】,放在public里面

  Launcher = class
  private
  public
    function SelectParam(json: TJsonObject): string;
    function GetCPLibraries(json: TJsonObject; mcpath, slpath: string): string;
  end;

让我来解释一下函数的三个参数分别代表什么意思【Delphi使用分号分隔参数而非逗号。】

参数功能
参数1Json文件。
参数2mc源文件的路径。
参数3mc-version-具体某个版本的路径

然后,我们就开始写咯,依旧的,在implementation下方写上我们的实现函数。实现函数写的行数可以在上面也可以在下面,随便在哪都可以。

注意,这里的mcpath和slpath的末尾都不允许有【/】或者【\】,否则可能导致拼接失败。

function Launcher.GetCPLibraries(json: TJsonObject; mcpath, slpath: string): string;
begin
  var sb := TStringBuilder.Create; //定义一个可变长度字符串
  var Yuan := TStringList.Create; //定义一个原本拼接的字符串集合
  var LibNo := TStringList.Create; //定义一个去除所有重复的字符串组合。
  var NoRe := TStringList.Create; //定义一个去除所有版本号低的字符串集合。
  var ReTemp := TStringList.Create; //定义一个去除版本号底时的临时字符串集合。
  try //将libraries内的元素转成Json列表格式然后遍历。
    for var i in (json.GetValue('libraries') as TJsonArray) do begin
      var key := i as TJsonObject; //将每个取出的元素定义成Json对象格式。
      var judge := true; //定义一个判断
      try
        var rl := key.GetValue('rules') as TJsonArray; //获取某一个元素的rule值。
        for var J in rl do begin  //下面开始判断rule值里面的action的os是否支持windows
          var r1 := J as TJsonObject;
          var an := r1.GetValue('action').Value; //获取action值
          if an = 'allow' then begin //如果是allow,则执行
            var r2 := r1.GetValue('os') as TJsonObject;
            var r3 := r2.GetValue('name').Value;
            if r3 <> 'windows' then begin judge := false; end; //如果支持windows,则没有continue,反之则为false
          end else if an = 'disallow' then begin //如果是disallow,则执行
            var r2 := r1.GetValue('os') as TJsonObject;
            var r3 := r2.GetValue('name').Value;
            if r3 = 'windows' then begin judge := false; end;
          end;
        end;
      except
      end;
      try
        var r1 := key.GetValue('natives').ToString; //判断里面是否有natives键。
        judge := false; //如果有,则judge变为false,如果没有,则触发报错,并且judge保持为true。
      except end;
      try
        var r1 := key.GetValue('downloads') as TJsonObject; //判断其downloads键中是否有classifiers键,如果有,则这个类库默认判断成natives类库。
        var r2 := r1.GetValue('classifiers').ToString;
        judge := false; //judge变为false表示此类库不能被添加
        var r3 := r1.GetValue('artifact').ToString;//如果其downloads键中不仅有classifiers键,还有artifact键,则这个类库也可以被拼接。
        judge := true; //judge重新变为true
      except end;
      if not judge then continue; //此时开始判断judge是否决定跳过此类。
      try
        Yuan.Add(key.GetValue('name').Value); 
          //为最后的Yuan添加Json中的name值。为什么不直接添加downloads->artifact->path呢?这个我们晚点再说。
      except
        messagebox(0, '你的Json中的某一个键值甚至连name键都没有,已触发报错!', 'json中不包含name键', MB_ICONERROR);
        exit;//上述代码,如果Json中连name都不包含,则报错。【多半是用于处理空大括号的。】
      end;
    end;//给Yuan去重后装进LibNo中。【由于我懒,因此没有给原函数直接去重,不过反正最后都要free释放掉资源的,不怕内存泄漏!】
    for var i in Yuan do
      if LibNo.IndexOf(i) = -1 then // 去除重复
        LibNo.Add(i);
    for var i in LibNo do begin //在这里将去除版本号较低的类库
      var KN := i.Replace('.', '').Replace(':', '').Replace('-', '').Replace('@jar', '').Replace('@zip', ''); //添加一个去除了【. : -】字符后的名称。
      var KW := ExtractNumber(KN, false); //摘取字符【这里用到了一个自制函数,晚点我们再实现这个函数。】
      var KM := ExtractNumber(KN, true);  //摘取数字
      if ReTemp.IndexOf(KW) = -1 then begin //与上面的判断重复一致,只不过这里是ReTemp和已经摘取字符后的LibNo元素。
        ReTemp.Add(KW);
        NoRe.Add(i); //这里需要给NoRe字符串集合直接添加原类库名称。
        //下方,如果存在字符一样的类库名称,则执行。并且判断此类库名称摘取数字后与上方的摘取数字的类库名称是否比它大,如果大,则执行,反之则跳过。
      end else if strtoint64(ExtractNumber(NoRe[ReTemp.IndexOf(KW)], true)) <= strtoint64(KM) then begin
        NoRe.Delete(ReTemp.IndexOf(KW)); //删除旧元素
        NoRe.Insert(ReTemp.IndexOf(KW), i); // 添加新元素
      end;
    end;
    //此时此刻,这个NoRe字符串类库列表,就是我们最终获取到的需要拼接到cp参数的列表了。
    //我们只需要将其添加进我们的可变字符串里就好了。
    for var I in NoRe do sb.Append(Concat(mcpath, '\\libraries\\', ConvertNameToPath(I), ';')); // 这里用到了自制函数:将名称转换为路径。具体名称格式怎么转换成路径,晚点我会说的。Concat的意思是将所有参数拼接起来,与字符串的+号一致,是Delphi内置函数。
    //【说一句,由于我用的是cs让我的代码有高亮显示(pascal在html没有),因此对于字符串内的右划号可能比较敏感。因此我会添加双右划号。大家在实际用Delphi写代码的时候,必须要把双右划号改成单右划号。】
    //下方是拼接最后一个版本文件夹中的jar文件,这个在cp函数里经常使用得到。
    sb.Append(GetRealPath(slpath, '.jar')); // 这里依旧用了一个自制函数,获取<第一个参数>路径下的第一个<文件标志=例如文件后缀名>文件。
    result := sb.ToString; //将最终的资源返回。
  finally
    sb.Free; //在finally模块里释放资源。
    Yuan.Free;
    LibNo.Free;
    NoRe.Free;
    ReTemp.Free;
  end;
end;

好了,以上便是我们如何获取类库。其中,我们在里面使用了几个函数,分别是【ConvertNameToPath、GetMCRealPath、ExtractNumbers】

getRealPath是什么意思呢?其意思就是指传入一个文件夹名称,然后通过遍历文件夹内容从而找到文件名中对于后一个参数的标志性名称【例如文件后缀】。因为我们在后期进行下载MC的时候,可能会在无法检测到jar、json的时候对此报错。而我们要支持玩家对json、jar进行改名而不用修改父文件夹名称后再修改jar、json文件夹名称。

这两个类库是什么意思呢?首先,ConvertNameToPath这个东西,我们需要看看Json文件中,对于downloads->artifact->path,然后再看看name键,它们是怎么拼接的。

为什么我们不直接读取path键呢?因为在Forge、Fabric、Quilt的libraries类库中,是没有downloads键的,只有一个name和一个url键。因此我们需要根据特定的规则来拼接这个name参数。【具体可以自己下载一个forge、fabric、quilt自己看。】

ExtractNumber其实就是填入两个参数,第一个参数填入一个原字符串,第二个参数填入一个布尔值,如果第二个参数为真,则摘取字符串中所有的数字,如果第二个参数为假,则摘取所有字符。

我们开始书写代码:

  Launcher = class
  private
  public
    function SelectParam(json: TJsonObject): string;
    function GetCPLibraries(json: TJsonObject; mcpath, slpath: string): string;
    function ConvertNameToPath(name: string): string;
    function GetRealPath(path, suffix: string): string;
    function ExtractNumber(str: string; bo: Boolean): string;
  end;

上述我们在类里面又多声明了几个函数。

function Launcher.ConvertNameToPath(name: string): string;
begin
  var c1 := TStringList.Create; // 给所有的变量划初始值。此均为临时变量。
  var c2 := TStringList.Create; 
  var all := TStringList.Create;
  var sb := TStringBuilder.Create;
  try //以上四个字符串列表均为临时变量,大家只需要看我接下来怎么写的就好了
    var hou: TArray<String> := SplitString(name, '@'); //先按照@切割一遍
    name := hou[0];
    var n1 := name.Substring(0, name.IndexOf(':')); //切割字符串,找到第一个冒号以前的字符。
    var n2 := name.Substring(name.IndexOf(':') + 1, name.Length);//切割字符串,找到第一个冒号以后的字符
    ExtractStrings(['.'], [], pchar(n1), c1); //切割字符串,将前者按照.切割。这里用到了一个Delphi内置函数ExtractStrings。其函数内容专门用于切割字符串。
    //第一个参数是切割的符号,第二个参数默认为空,第三个参数为需要切割的字符串的指针类型【用pchar写即可】,第四个参数则填入一个StringList字符串列表即可。
    for var I in c1 do all.Add(Concat(I, '\\')); //将前者第一个冒号以前的字符,临时变量添加进all,注意这里我用了双右划号,大家如果是用Delphi写程序,记得将其改成一个即可。以下均如此。
    ExtractStrings([':'], [], pchar(n2), c2); //将后者第一个冒号以后的字符按照:切割
    for var I := 0 to c2.Count - 1 do begin //添加进all,这里需要判断
      if c2.Count >= 3 then begin //如果冒号有3个同样的字符,则执行。此处适配了1.19-pre1以上的版本。在此版本以上,natives将直接装进cp后缀,且无需解压。
        if I < c2.Count - 1 then begin //将最后一个数据脱离出all
          all.Add(Concat(c2[I], '\\'));
        end;
      end else all.Add(Concat(c2[I], '\\')); //否则直接添加
    end;
    for var I := 0 to c2.Count - 1 do begin //接着再按照namt开始拼接。
      if I < c2.Count - 1 then begin  //无论元素是否有3个,将最后一个数据脱离出all
        all.Add(Concat(c2[I], '-'));  //按照-进行连接,具体请看downloads->artifact->path
      end else begin
        try
          all.Add(Concat(c2[I], '.', hou[1])); //最后一个数据本来是jar的,但是由于neoforge部分的部分name末尾有@jar,因此这里需要这样拼接。。
          //自然,在neoforge里,我猜测可能会有@zip等的一些后缀,因此就暂时先这样!        
        except
          all.Add(Concat(c2[I], '.jar')); //如果末尾没有@jar,也就是获取hou[1]时出现报错,就直接将其拼接.jar即可!
        end;
      end;
    end;
    for var I in all do sb.Append(I); //最后,将all中所有的数据添加进sb可变字符串中。
    result := sb.ToString; //最后将sb返回即可。
  finally
    c1.Free; //最终依旧是要把这几个参数给释放掉。
    c2.Free;
    all.Free;
    sb.Free;
  end;
end;

首先,我并不知道我这样写是否是最简单的写法,但我敢肯定,这样写一定是对的。

然后是ExtractNumber:

function Launcher.ExtractNumber(str: String; bo: Boolean): String;
begin
  var Temp := ''; // 设置temp
  if str = '' then // 判断长度
  begin
    result := ''; // 如果长度等于0,则返回空
    exit;
  end;
  for var I in str do // for循环判断长度
  begin
    if bo then // 如果参数bo为真,则执行,否则执行以下
    begin
      if I.IsNumber then // 判断是否为数字
        Temp := Concat(Temp, I); // 是则添加
    end
    else
    begin
      if not I.IsNumber then // 判断是否不为数字
        Temp := Concat(Temp, I); // 不是则添加
    end;
  end;
  result := Temp;
end;

这个函数算是一个非常简单的字符串处理了,使用了I.IsNumber判断是否为数字。

好了,然后我们再看看GetRealPath的写法吧!自然,既然有获取文件的真实路径,那在将来肯定会有获取真实文件夹的路径【用于直接在版本文件夹中获取到natives文件夹。】但是现在我们还没教到这部分呢!

//此种方式适用于找寻文件。
function Launcher.GetRealPath(path, suffix: string): string;
var
  Files: TArray<string>;//在方法开头定义一个string数组,用来接收
begin
  //由于Json文件在版本文件夹中可能存在不止一个,并且名称都不一定。因此,我们需要额外对【.json】的suffix后缀进行单独判断。
  result := ''; //将返回值定义为空
  if DirectoryExists(path) then begin// 判断文件夹是否存在
    Files := TDirectory.GetFiles(path); // 找到所有文件
    for var I in Files do begin // 遍历文件
      if I.IndexOf(suffix) <> -1 then begin // 是否符合条件
        if suffix = '.json' then begin //此时开始单独为【.json】文件进行判断。。
          var god := GetOutsideDocument(I); //获取Json文件的内容
          //【这里用到了一个自制函数GetOutsideDocument,这个稍后会与大家说,且我们的教程中将与这个函数密不可分。】
          try //这里给读取到json文件内容转换成Json对象。
            var Root := TJsonObject.ParseJSONValue(god) as TJsonObject;
            var tmp := Root.GetValue('libraries').ToString; //判断json中是否有libraries键值,这里直接用ToString判断。
            var ttt := Root.GetValue('mainClass').Value; //判断json中是否有mainClass键值。
            //如果大家发现了在原版json、Forge-json、Fabric-json、Quilt-json、LiteLoader-json中有任一同样拥有的键值,请立即向我发出issue。
            result := I; //将返回值为I。
            exit; //退出方法
          except //如果没有libraries或者mainClass中的任意一值,则跳过。
            continue;
          end;
        end else begin //如果找到了符合条件的,并且suffix不为【.json】,则直接返回。
        //这里没有对原版的【.jar】后缀进行判断,因为如果真的要判断,需要解压jar,而这样会导致程序异常缓慢。
          result := I;
          exit;
        end;
      end;
    end;
  end;
end;

好了,以上就是最终的代码了。

在编写的过程中,我们意外的发现了,我们至始至终都没有编写过任何一个可以获取外部文件内容的函数。那我们应该如何编写这个函数呢?顺带一提,这个函数将伴随我们编写代码持续很久时间,因为很多地方都会需要用到它。

首先,哪里我们会用到这个函数呢?在我们读取MC的json文件进入我们的程序时,我们肯定需要用到这个函数,其次,就在上面了!GetRealPath!

这个函数非常简单,我们只需要非常简单的步骤即可编写成功!

然而,这个函数我们需要的是能够在程序的每一个地方都可以调用到这个函数。那怎么办?

我们直接在Form1窗体单元中的implementation上方写上即可。

function GetOutsideDocument(path: string): string;

implementation

众所周知学过Delphi的人都知道,在implementation上方写的所有【无论是type,还是const,还是var,又或者是function、procedure】类、量、函数等,都会在所有新建的单元中被共享因此在很多时候我们都不建议将一些私有化的代码主动放在这里。

大家对于私有化的代码,可以在type中定义一个class,然后在class中的private代码块下面添加我们的私有化代码。这样的话我们只能在该类中适用。又或者是在implementation下方定义我们的私有函数。因为如果在下方定义私有变量、类、函数等,就只能在【当前单元文件】中被适用而无法被共享出去了。

接下来我们开始实现这个代码。首先,如果不是隶属于类的变量、函数等,前方是不需要加<类名>.的。

function GetOutsideDocument(path: string): string;
begin
  result := ''; //将函数初始返回值定义为空。
  var ss := TStringStream.Create('', TEncoding.UTF8, false); //定义一个字符串流。用于存储读取文件后的内容。
  try
    if FileExists(path) then begin //判断文件是否存在,如果存在,则执行,否则不执行。这里使用了Delphi内置函数【FileExists】,可以正常拿出使用。
      ss.LoadFromFile(path); //直接读取
      result := ss.DataString; //返回值设定为字符串流中的数据。
    end;
  finally
    ss.Free; //最后释放资源。
  end;
end;

好了,那么这就是我们的获取外部文件了。现在开始,在之后的教程中,如果我有需要获取外部文件的代码,我将不再多说,直接使用这个函数即可。

那么,以上便是如何获取MC的cp-libraries库,后期在我们拼接libraries库的时候,我们直接就可以填入这个函数名,然后传入三个参数即可正常获取到所有的cp键值。

我们可以很容易就看出,这个函数获取起来异常繁琐,在GetRealPath中,在实际应用中,如果你不能确定json文件到底会被玩家命名成什么,你就应该使用这个函数,否则你不应该像上面那么写。

如果你的启动器需要适配PCL2、HMCL、BakaXL的话,你应该写的是直接读取【<版本文件夹>/<版本文件夹>.json】【以及<版本文件夹>/<版本文件夹>.jar】而不是查询里面出现过的json。

但同时,如果你需要适配PCL2、HMCL、BakaXL等,你可能需要在实现【重命名此版本名称】时注意一下,将json文件名和jar文件名一同修改成你所需要的名称。

而且,我在检测版本时,将会有很大的误会产生,例如如果我的<版本文件夹>中存在两个以上的jar文件,那么我大概率会检测失败,因为无法检测里面含有jar后缀的版本文件到底是不是游戏主jar。

以上就是我在此教程中大约出现的纰漏,这些纰漏我将交给程序员们自己尝试修复了,还是依旧的,我在这份教程里只说逻辑,不做教程。

就这样啦!