本章,我们将对Forge、Fabric、Quilt特定的JSON文件进行支持。

首先,众所周知,在Forge、Fabric、Quilt【以下简称模组加载器】中,原版的下载MC时,部分启动器可能会直接将原有的Json文件给Get下来,而不是将模组加载器的Json与原版Json进行合并。

这时,我们就要好好的研究一下,对于模组加载器原版的Json文件到底是怎样的获取了。

然后,在这一章节,我们便可以了解为什么上一章中获取库文件我们不是直接获取downloads->artifact->path而是获取name了。

首先,让我们看看1.12.2Forge的部分Json文件吧!【ps:我选取的是1.12.2forge-2847的】

{
	"id": "1.12.2-forge1.12.2-14.23.5.2847",
	"time": "2019-09-10T01:22:54+0000",
	"releaseTime": "1960-01-01T00:00:00-0700",
	"type": "release",
	"minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --tweakClass net.minecraftforge.fml.common.launcher.FMLTweaker --versionType Forge",
	"mainClass": "net.minecraft.launchwrapper.Launch",
	"inheritsFrom": "1.12.2",
	"jar": "1.12.2",
	"logging": {},
	"libraries": [
		{
			"name": "net.minecraftforge:forge:1.12.2-14.23.5.2847",
			"url": "https://maven.minecraftforge.net/"
		},
		{
			"name": "net.minecraft:launchwrapper:1.12",
			"serverreq": true
		}
        ... //一大堆类库
	]
}

首先,我们可以看到在这个Json文件上面,有几个非常关键的信息,首先就是我们的id键,这个键需要替换掉原版的id键,确信。

然后我们还可以看到里面有minecraftArguments键,如果大家看过Minecraft原版的1.12.2的Json文件的话,应该能理解这个的意思。但是在这里,这个键需要替换掉原版的键,因为这个键已经拥有了Minecraft在1.12.2里面原版的所有键值了。

然后嘛,mainClass也是需要替换掉原版的mainClass的,

最后,就是我们的libraries键了,这些键并不是替换掉原版的libraries键哦,而是附加在原版的libraries键后面。将这个类库所有的全部附加到原版的libraries键里面。

同时,这个libraries只有一个url键,甚至连downloads键都没有,因此,我们需要根据name的值来判断其文件目录。

那么,我们需要做的目的是什么呢?答案显而易见,我们通过查看PCL2下载的json文件,就可以得知:PCL2其实已经将我们的forge-2847文件附加到了原版的1.12.2里面了,也就是PCL2下载的是一个杂合体

那我们应该怎么找到原版的1.12.2键呢?如果我们下载到了类似于这样的json而不是像PCL2那样的json,那该怎么办呢?

答案也很显而易见,我们需要找它的inheritsFrom键下面的原版json。其实嘛,假如我们只像PCL2那样,只是遍历一次父文件夹versions下面的所有文件夹名称,然后找到与inheritsFrom键值相同的,即可判断成功,那可真是太low了。

如上面所做,这样只是判断versions如果没有原版文件夹,那这个forge版本将会检测失败,从而抛出报错。提示玩家需要下载一次原版后,再尝试启动游戏。

我们需要做的,是不仅遍历一次versions下面的所有文件夹,而且需要检测里面版本json文件里的的id键值是否为forge的inheritsFrom值。这样,我们就可以做到精确的查询到获取原版的json文件进行拼接了。总而言之,我们做的工作,是要在兼容PCL2、HMCL、BakaXL的情况下,尽量做到更加精确,更加准确。

哦对了,还有一个地方需要判断,我们仔细的看看这个文件,里面似乎还有一个jar键,这个键指的是什么意思呢?看起来似乎与inheritsFrom键一样欸!

这个键的意思指的是原版的jar键,我们需要在当前json同级目录下,下载一个原版的主jar文件作为forge的加载项,同样是添加到启动参数cp的末尾versions的那个主jar文件。是的,就是这么简单!

好了,废话少说,我们开始看代码:

首先,第一步,我们需要写一个函数,函数名可以叫做【GetInheritsFrom】,用于判断json是否有指定键值,如果有,则返回父versions键,开始遍历文件夹,然后查询是否有符合条件的id。该函数接收两个参数,第一个是【path: String】,意思是传入我们存放json的那个文件夹路径。第二个是【suffix: String】,指的是我们需要判断的键,可以填入inheritsFrom,也可以填入jar。返回值是:【String】如果找到了inheritsFrom,则返回有这个键的父文件夹,就是【versions/[inherits]】这个文件夹。

由于这个函数是隶属于Launcher类下的,因此,我无需再写声明函数部分,大家在下文,假如看见了任何一个方法名前面加上了之前定义好的类名的话,那就在这个类里面写上该方法的声明即可。

function Launcher.GetInheritsFrom(path, suffix: string): string;
var
  Dirs: TArray<String>; //定义一个查询versions下的所有子目录的数组
  Files: TArray<String>; //定义一个随时可以删掉的上面子目录下的所有文件数组。
begin
  result := ''; //返回值定义为空
  if DirectoryExists(path) then begin //判断path是否存在,如果存在则执行。
    var ph := GetRealPath(path, '.json'); //获取该path下的真实json路径。
    if FileExists(ph) then begin //如果json的确存在,则执行。反之则不执行。
      var Rt := TJsonObject.ParseJSONValue(GetOutsideDocument(ph)) as TJsonObject;
      try //获取该json的内容,并转换成Json对象形式。
        var ihtf := Rt.GetValue(suffix).Value; //判断里面是否有【第二个参数】键,如果有,则不抛出报错。如果没有,则抛出报错,返回值为原path。
        if ihtf = '' then begin result := path; exit; end; //如果inheritsFrom键为空值,也是如此。
        var vdir := ExtractFileDir(path); //获取path的父文件夹【也就是versions文件夹】
        Dirs := TDirectory.GetDirectories(vdir); //获取该文件夹下的所有子目录,是的,这里是获取文件夹而非文件。
        for var I in Dirs do begin //使用for循环遍历versions下的子目录
          Files := TDirectory.GetFiles(I); //获取子目录中的所有文件
          for var J in Files do begin //开始循环判断子目录中的所有文件
            if RightStr(J, 5) = '.json' then begin //如果后缀为json
              try //执行,将文件转换成json对象
                var Rt2 := TJsonObject.ParseJSONValue(GetOutsideDocument(J)) as TJsonObject; 
                var jid := Rt2.GetValue('id').Value; //开始判断里面是否有id键
                var tmp := Rt2.GetValue('libraries').ToString; //判断里面是否有libraries键
                var ttt := Rt2.GetValue('mainClass').Value; //判断里面是否有mainClass键。
                if jid = ihtf then begin //此时,开始判断如果id键等于inheritsFrom键值,则将该文件夹目录进行返回。否则跳过。
                  result := I;
                  exit; //记住了,这是返回文件夹的目录,而不是返回Json文件的路径
                end;
                continue; //如果不满足,则跳过。
              except
                continue; //如果不满足,则跳过。
              end;
            end;
          end;
        end;
      except
        result := path; //如果上述代码开头发生了报错,则返回值为原值。
      end;
    end;
  end;
end;

然后呢,在这里我们使用了一个函数为RightStr,这个函数的意思是从右取字符串,第一个参数填入原字符串,第二个参数填入从右往左数的第几个之右的所有字符串。

这个是Delphi内置的一个函数,我们可以引用以下头文件使用它

uses
  StrUtils;

我们可以看到,上面我们用了几次GetOutsideDocument,这是否说明了上一章说的函数可以被我们随时调用了呢?

然后,此时我们就应该可以通过inheritsFrom键获取原版的文件夹路径了。

然后嘛,我们还得再看一个东西,那就是在1.13版本以上的forge的json文件。为什么要看这个呢?因为forge在里面新增了许许多多的默认jvm和默认game参数,以及许多的类库。上面我们看的是1.12.2的,那么下面我们就要看的是1.19.4的forge的json文件了:

{
    "_comment_": [
        "Please do not automate the download and installation of Forge.",
        "Our efforts are supported by ads from the download page.",
        "If you MUST automate this, please consider supporting the project through https://www.patreon.com/LexManos/"
    ],
    "id": "1.19.4-forge-45.0.43",
    "time": "2023-04-07T06:24:13+00:00",
    "releaseTime": "2023-04-07T06:24:13+00:00",
    "type": "release",
    "mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher",
    "inheritsFrom": "1.19.4",
    "logging": {
        
    }, //此处为默认的JVM、game参数【由于此处没有rules规则的参数,因此此处没有额外的JVM参数,一切按照原版指示来做。】
    "arguments": {
        "game": [ //一大堆的有关于forge的默认game参数
            "--launchTarget",
            "forgeclient",
            "--fml.forgeVersion",
            "45.0.43",
            "--fml.mcVersion",
            "1.19.4",
            "--fml.forgeGroup",
            "net.minecraftforge",
            "--fml.mcpVersion",
            "20230314.122934"
        ],
        "jvm": [ //有关于forge的一大堆jvm参数。
            "-Djava.net.preferIPv6Addresses=system",
            "-DignoreList=bootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,JarJarFileSystems,client-extra,fmlcore,javafmllanguage,lowcodelanguage,mclanguage,forge-,${version_name}.jar",
            "-DmergeModules=jna-5.10.0.jar,jna-platform-5.10.0.jar",
            "-DlibraryDirectory=${library_directory}",
            "-p",
            "${library_directory}/cpw/mods/bootstraplauncher/1.1.2/bootstraplauncher-1.1.2.jar${classpath_separator}${library_directory}/cpw/mods/securejarhandler/2.1.6/securejarhandler-2.1.6.jar${classpath_separator}${library_directory}/org/ow2/asm/asm-commons/9.3/asm-commons-9.3.jar${classpath_separator}${library_directory}/org/ow2/asm/asm-util/9.3/asm-util-9.3.jar${classpath_separator}${library_directory}/org/ow2/asm/asm-analysis/9.3/asm-analysis-9.3.jar${classpath_separator}${library_directory}/org/ow2/asm/asm-tree/9.3/asm-tree-9.3.jar${classpath_separator}${library_directory}/org/ow2/asm/asm/9.3/asm-9.3.jar${classpath_separator}${library_directory}/net/minecraftforge/JarJarFileSystems/0.3.19/JarJarFileSystems-0.3.19.jar",
            "--add-modules",
            "ALL-MODULE-PATH",
            "--add-opens",
            "java.base/java.util.jar=cpw.mods.securejarhandler",
            "--add-opens",
            "java.base/java.lang.invoke=cpw.mods.securejarhandler",
            "--add-exports",
            "java.base/sun.security.util=cpw.mods.securejarhandler",
            "--add-exports",
            "jdk.naming.dns/com.sun.jndi.dns=java.naming"
        ]
    },
    "libraries": [
        {
            "name": "cpw.mods:securejarhandler:2.1.6",
            "downloads": {
                "artifact": {
                    "path": "cpw/mods/securejarhandler/2.1.6/securejarhandler-2.1.6.jar",
                    "url": "https://maven.minecraftforge.net/cpw/mods/securejarhandler/2.1.6/securejarhandler-2.1.6.jar",
                    "sha1": "66c15fc1f522b586476e9e4cccd0cbe192554e8a",
                    "size": 87783
                }
            }
        },
        ... //众多类库。
    ]
}

好了,那么在这个json文件里面,我们唯一需要多注意一点的就是在这里面没有minecraftArguments键,取而代之的是arguments->jvm和arguments->game两个键值,因此在实际操作的时候,我们需要按照这两个逐步进行判断。

然后嘛,我们要做的第二步,就是将这个键下的原版MC的json与我们的Forge版json进行合并,记住,合并这一步是在内存当中合并,我们暂时没必要将其输出到文件中,因为我们已经拥有了该启动的所有类,这一步是为了迎合PCL2,当然,如果你想要输出到文件,那我也没辙。。

现在开始看代码吧: 该函数传入两个参数,其中yuanjson参数代表着我们有inheritsFrom的forge的json字符串形式的数据。然后gaijson指的是我们的原版json文件。返回值则返回一个拼接后的字符串【需要自己转成Json对象格式】。

function Launcher.ReplaceInheritsFrom(yuanjson, gaijson: string): string;
begin
  if yuanjson = '' then begin result := ''; exit; end;  //如果任意一个json为空,则返回空。
  if gaijson = '' then begin result := ''; exit; end; //同上。
  if yuanjson = gaijson then begin result := yuanjson; exit; end; //如果两个json一样,则返回原值。
  var Rty := TJsonObject.ParseJSONValue(yuanjson) as TJsonObject; //将两个字符串转成json
  var Rtg := TJsonObject.ParseJSONValue(gaijson) as TJsonObject; //同上
  Rtg.RemovePair('mainClass'); //删掉原版里面的mainClass数据
  Rtg.AddPair('mainClass', Rty.GetValue('mainClass').Value); //将模组加载器的json中的mainClass添加进去。
  Rtg.RemovePair('id'); //删掉原版id键
  Rtg.AddPair('id', Rty.GetValue('id').Value); //将模组加载器中的id键添加进去。
  for var I in (Rty.GetValue('libraries') as TJsonArray) do (Rtg.GetValue ('libraries') as TJsonArray).Add(I as TJsonObject); //开始判断libraries,此时应该是增添到原版里面,而不是替换。
  try //使用try语句判断gaijson里面是否有arguments->game键,如果有,则执行,反之则不执行。
    for var I in ((Rty.GetValue('arguments') as TJsonObject).GetValue('game') as TJsonArray) do //开始判断
      ((Rtg.GetValue('arguments') as TJsonObject).GetValue('game') as TJsonArray).Add(I.GetValue<String>); //这里直接用GetValue<String>,且从长开始拼接。
  except end;
  try  //再以同样的方式判断是否有jvm参数,如果有,则开始增添而不是替换。
    for var I in ((Rty.GetValue('arguments') as TJsonObject).GetValue('jvm') as TJsonArray) do
      ((Rtg.GetValue('arguments') as TJsonObject).GetValue('jvm') as TJsonArray).Add(I.GetValue<String>);
  except end;
  try //最后一步,开始判断里面是否有minecraftArguments键,用于适配1.12.2
    var ma := Rty.GetValue('minecraftArguments').Value;
    Rtg.RemovePair('minecraftArguments'); //如果有,则删掉原键增添新键,反之则跳过。
    Rtg.AddPair('minecraftArguments', ma);
  except end;
  result := Rtg.ToString; //将最后的gaijson转成的Json对象使用ToString变成字符串后直接返回。
end;

当然了,上面这一步代码,我感觉我是有点当时的年少有为,因为我直到编写这套教程的时候,我才忽然意识到,其实上面的两个参数的代码可以直接以TJsonObject类型传入,而不是用字符串形式传入,然后返回值其实也是可以用Json对象返回的,但既然写都写了,也懒得改了。

好了,那么我们写完这几个函数之后,我们就应该开始思考,我们应该怎么去使用这两个函数了,那么如何使用这两个函数呢?我们下章教程再见分晓。