Minecraft(我的世界)中文论坛

标题: JMCCC使用教程——Java启动器类库,几行代码下载并启动MC

作者: yushijinhun    时间: 2016-4-5 20:37
标题: JMCCC使用教程——Java启动器类库,几行代码下载并启动MC
本帖最后由 yushijinhun 于 2016-4-8 21:55 编辑

JMCCC是由 @yushijinhun@Darkyoooooo 开发的一个Java启动器核心,支持:

并且JMCCC是开源的(MIT许可证),JMCCC在GitHub上的项目:https://github.com/to2mbn/JMCCC

本教程的适用人群:有一定经验的Java开发者

目录


点击下面的页码来跳到下一页。



1. 环境搭建
JMCCC由三个模块组成,分别为jmccc、jmccc-yggdrasil-authenticator、jmccc-mcdownloader,它们分别提供了启动Minecraft、正版登录、下载Minecraft的功能。您可以按需引用这三个模块。
假如您使用Maven或者Gradle,您只需要加入如下的依赖即可(目前JMCCC最新版本为2.4):
  1. org.to2mbn:jmccc:2.4
  2. org.to2mbn:jmccc-yggdrasil-authenticator:2.4
  3. org.to2mbn:jmccc-mcdownloader:2.4
复制代码

假如您不使用Maven/Gradle,您就需要手动导入jar包,上述jar可以在Maven中心仓库找到,并且还需要导入org.jsontukaani xz这两个依赖。

JMCCC要求的Java最低版本为Java7。



2. 启动游戏
首先,您需要通过LauncherBuilder类来创建一个Launcher对象:
  1. Launcher launcher = LauncherBuilder.buildDefault();
复制代码

buildDefault()方法会创建一个默认配置的Launcher。当然您也可以通过方法链来自定义Launcher的配置,比如:
  1. Launcher launcher = LauncherBuilder.create()
  2.     .setDebugPrintCommandline(true) // (1)
  3.     .setNativeFastCheck(true) // (2)
  4.     .build();
复制代码

方法意义
setDebugPrintCommandline(boolean)(即上面的(1))设置是否在启动时将启动参数输出到控制台以供调试,默认为false。
setNativeFastCheck(boolean)(即上面的(2))设置是否开启对Natives文件的快速检查,默认为false。
假如没有开启该选项,jmccc在启动时会对Natives文件进行全文比较来判断文件是否完整,如果发现Natives文件内容不一致,则将文件替换。
开启该选项后,jmccc仅会通过比较文件大小来判断文件是否完整,这样可以加快启动速度,但有可能造成某些问题。

接着,您需要创建一个LaunchOption来描述启动设置,比如:
  1. LaunchOption option = new LaunchOption("1.9", new OfflineAuthenticator("test_user"), new MinecraftDirectory(".minecraft")); // (3)
复制代码

在上面的例子中,第一个参数为为要启动的Minecraft版本,这里为1.9。第二个参数为验证方式,这里用的是离线验证(即所谓盗版),用户名是test_user。第三个参数是.minecraft目录的位置,这里使用的是当前目录下的.minecraft目录。
除此之外,您还可以对LaunchOption进行其它配置,下面列举了一些LaunchOption类的方法。
方法意义
setMaxMemory(int)设置最大内存(MB),默认为1024。如果为0则不会添加-Xmx参数(即让JVM自己决定)。
setMinMemory(int)设置最小内存(MB),默认为0。如果为0则不会添加-Xms参数(即让JVM自己决定)。
setServerInfo(ServerInfo)设置游戏启动后要自动进入的服务器,默认为null。
例如 new ServerInfo("localhost", 25565) 就描述了在localhost的25565端口上的服务器。
setWindowSize(WindowSize)设置游戏窗口大小,默认为null(不指定)。
例如 WindowSize.fullscreen() 方法返回一个代表全屏的WindowSize对象;
WindowSize.window(640, 480) 返回一个代表了窗口大小是640x480的WindowSize。
setExtraJvmArguments(List<String>)设置额外的JVM参数,默认为null。
相关的用法可以跳读到7、8节。
setExtraMinecraftArguments(List<String>)设置额外的Minecraft参数,默认为null。这些参数将被添加到默认Minecraft启动参数的末尾。
setCommandlineVariables(Map<String, String>)设置额外的命令行模板参数,通过该方法指定的参数可以覆盖默认的参数。version.json中的minecraftArguments是参数化的,其中${...}格式的字符串会被替换为对应变量的实际值。
例如,minecraftArguments出现了${a}这样的字符串,并且通过该方法指定了"a" -> "233",那么启动时${a}就会被替换为233。
再例如,minecraftArguments中出现了${version_name},则这段字符串在启动时将自动被Minecraft的版本号代替。但如果通过该方法指定了"version_name" -> "abc",则${version_name}会被abc代替,而不是Minecraft版本号,因为"version_name" -> "abc"覆盖了默认的参数。
为了帮助理解,下面给出一段Minecraft 1.8.9的minecraftArguments:
  1. --username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userProperties ${user_properties} --userType ${user_type}
复制代码

相关的使用可以跳读到第11节。
setRuntimeDirectory(MinecraftDirectory)设置Minecraft运行时使用的目录,默认和getMinecraftDirectory()一样(同上面(3)中构造方法的第三个参数)。这里指定的runtimeDirectory包含的是存档、资源包、截图等,而上面的minecraftDirectory包含的是游戏jar(versions)、库文件(libraries)、资源文件(assets)等。所以可以用这个方法来实现各版本独立。

然后您就可以通过调用launch(LaunchOption)方法启动游戏了:
  1. launcher.launch(option);
复制代码
假如启动失败则会抛出一个LaunchException,如果该LaunchException是一个MissingDependenciesException(MissingDependenciesException是LaunchException的子类),则代表有libraries缺失。

如果您想获取Minecraft进程的控制台日志,则可以使用launch(LaunchOption, GameProcessListener):
  1. launcher.launch(option, new GameProcessListener() {

  2.     @Override
  3.     public void onLog(String log) {
  4.         System.out.println(log); // (4)
  5.     }

  6.     @Override
  7.     public void onErrorLog(String log) {
  8.         System.err.println(log); // (5)
  9.     }

  10.     @Override
  11.     public void onExit(int code) {
  12.         System.err.println("游戏进程退出,状态码:" + code); // (6)
  13.     }
  14. });
复制代码

上面代码中(4)处的onLog(String)方法会在游戏进程的标准输出输出日志时调用,(5)处的onErrorLog(String)方法会在游戏进程的标准错误输出日志时调用,而(6)处的onExit(int)会在游戏进程结束时调用。上面这段代码把游戏进程的日志都输出到了自己的控制台,并且在游戏结束时还会输出 "游戏进程退出,状态码:xxx" 这样的字符串。

下面给出一段演示代码:
  1. package yushijinhun.jmccc.test;

  2. import org.to2mbn.jmccc.auth.OfflineAuthenticator;
  3. import org.to2mbn.jmccc.exec.GameProcessListener;
  4. import org.to2mbn.jmccc.launch.Launcher;
  5. import org.to2mbn.jmccc.launch.LauncherBuilder;
  6. import org.to2mbn.jmccc.option.LaunchOption;
  7. import org.to2mbn.jmccc.option.MinecraftDirectory;

  8. public class JmcccTest {

  9.     public static void main(String[] args) throws Exception {
  10.         // 创建一个Launcher对象
  11.         Launcher launcher = LauncherBuilder.create()
  12.                 .setDebugPrintCommandline(true) // 将启动命令打印到控制台以便调试
  13.                 .build();

  14.         // 启动配置
  15.         LaunchOption option = new LaunchOption(
  16.                 "1.9", // 游戏版本
  17.                 new OfflineAuthenticator("test_user"), // 使用离线验证,用户名test_user
  18.                 new MinecraftDirectory("/home/yushijinhun/.minecraft")); // .minecraft目录

  19.         // 最大内存2048M
  20.         option.setMaxMemory(2048);

  21.         // 启动游戏
  22.         launcher.launch(option, new GameProcessListener() {

  23.             @Override
  24.             public void onLog(String log) {
  25.                 System.out.println(log); // 输出日志到控制台
  26.             }

  27.             @Override
  28.             public void onErrorLog(String log) {
  29.                 System.err.println(log); // 输出日志到控制台(同上)
  30.             }

  31.             @Override
  32.             public void onExit(int code) {
  33.                 System.err.println("游戏进程退出,状态码:" + code); // 游戏结束时输出状态码
  34.             }
  35.         });
  36.     }

  37. }
复制代码

控制台输出:
  1. jmccc:
  2. /usr/lib/jvm/jdk1.8.0_66/jre/bin/java
  3. -Xmx2048M
  4. -Djava.library.path=/home/yushijinhun/.minecraft/versions/1.9/1.9-natives
  5. -cp
  6. /home/yushijinhun/.minecraft/libraries/com/paulscode/soundsystem/20120107/soundsystem-20120107.jar:/home/yushijinhun/.minecraft/libraries/net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar:/home/yushijinhun/.minecraft/libraries/com/google/guava/guava/17.0/guava-17.0.jar:/home/yushijinhun/.minecraft/libraries/commons-io/commons-io/2.4/commons-io-2.4.jar:/home/yushijinhun/.minecraft/libraries/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar:/home/yushijinhun/.minecraft/libraries/com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar:/home/yushijinhun/.minecraft/libraries/net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar:/home/yushijinhun/.minecraft/libraries/commons-codec/commons-codec/1.9/commons-codec-1.9.jar:/home/yushijinhun/.minecraft/libraries/com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar:/home/yushijinhun/.minecraft/libraries/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar:/home/yushijinhun/.minecraft/libraries/com/google/code/gson/gson/2.2.4/gson-2.2.4.jar:/home/yushijinhun/.minecraft/libraries/org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar:/home/yushijinhun/.minecraft/libraries/com/paulscode/codecwav/20101023/codecwav-20101023.jar:/home/yushijinhun/.minecraft/libraries/com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar:/home/yushijinhun/.minecraft/libraries/org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar:/home/yushijinhun/.minecraft/libraries/org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar:/home/yushijinhun/.minecraft/libraries/io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar:/home/yushijinhun/.minecraft/libraries/org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar:/home/yushijinhun/.minecraft/libraries/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar:/home/yushijinhun/.minecraft/libraries/com/mojang/realms/1.8.7/realms-1.8.7.jar:/home/yushijinhun/.minecraft/libraries/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar:/home/yushijinhun/.minecraft/libraries/oshi-project/oshi-core/1.1/oshi-core-1.1.jar:/home/yushijinhun/.minecraft/libraries/net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar:/home/yushijinhun/.minecraft/libraries/org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar:/home/yushijinhun/.minecraft/libraries/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar:/home/yushijinhun/.minecraft/libraries/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar:/home/yushijinhun/.minecraft/libraries/com/mojang/authlib/1.5.22/authlib-1.5.22.jar:/home/yushijinhun/.minecraft/libraries/com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar:/home/yushijinhun/.minecraft/versions/1.9/1.9.jar:
  7. net.minecraft.client.main.Main
  8. --username
  9. test_user
  10. --version
  11. 1.9
  12. --gameDir
  13. /home/yushijinhun/.minecraft
  14. --assetsDir
  15. /home/yushijinhun/.minecraft/assets
  16. --assetIndex
  17. 1.9
  18. --uuid
  19. 109fb39758f23a9a92c15a21ac5113c2
  20. --accessToken
  21. 5b54b31a68574518a71fdd7e9846f87d
  22. --userType
  23. mojang
  24. --versionType
  25. release

  26. [18:01:52] [Client thread/INFO]: Setting user: test_user
  27. [18:01:52] [Client thread/INFO]: (Session ID is token:5b54b31a68574518a71fdd7e9846f87d:109fb39758f23a9a92c15a21ac5113c2)
  28. [18:01:54] [Client thread/INFO]: LWJGL Version: 2.9.4
  29. [18:01:54] [Client thread/INFO]: Reloading ResourceManager: Default
  30. [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:block.note.pling
  31. [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.bat.loop
  32. [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.cat.hiss
  33. [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.ghast.scream
  34. [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.player.breath
  35. [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.small_slime.jump
  36. [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.snowman.ambient
  37. [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.wolf.howl
  38. [18:01:54] [Sound Library Loader/INFO]: Starting up SoundSystem...
  39. [18:01:55] [Thread-5/INFO]: Initializing LWJGL OpenAL
  40. [18:01:55] [Thread-5/INFO]: (The LWJGL binding of OpenAL.  For more information, see http://www.lwjgl.org)
  41. [18:01:55] [Thread-5/INFO]: OpenAL initialized.
  42. [18:01:55] [Sound Library Loader/INFO]: Sound engine started
  43. [18:01:56] [Client thread/INFO]: Created: 1024x512 textures-atlas
  44. [18:02:02] [Realms Notification Availability checker #1/INFO]: Could not authorize you against Realms server: Invalid session id
  45. [18:02:03] [Client thread/INFO]: Stopping!
  46. [18:02:03] [Client thread/INFO]: SoundSystem shutting down...
  47. [18:02:03] [Client thread/WARN]: Author: Paul Lamb, www.paulscode.com
  48. 游戏进程退出,状态码:0
复制代码



3. 正版登录
正版登录是jmccc的一个可选功能,您必须确保您已经导入了jmccc-yggdrasil-authenticator这个依赖。

(有必要解释一下yggdrasil的意思,yggdrasil就是mojang正版验证服务的代号)

正版登录的功能由YggdrasilAuthenticator类提供。

YggdrasilAuthenticator类存储了一个正版验证的session。当每次向正版验证服务器刷新session时,YggdrasilAuthenticator都会将新的session存储起来,以备下次使用。假如说因为某种原因session失效了(如长时间不使用),YggdrasilAuthenticator则会要求提供密码来重新登录。

当使用YggdrasilAuthenticator时(即要求返回一个有效的session),流程如下:



注:上面的逻辑就是auth()方法中的逻辑。

如何创建一个YggdrasilAuthenticator?

YggdrasilAuthenticator有两个工厂方法,分别是YggdrasilAuthenticator.password(String, String)和YggdrasilAuthenticator.token(String, String)。(这两个方法还有若干重载)
方法意义
password(String, String)创建一个YggdrasilAuthenticator,并用所给的密码初始化。
token(String, String)创建一个YggdrasilAuthenticator,并用所给的token初始化。

使用上面这两个方法创建的YggdrasilAuthenticator都已经存储着了一个有效的session,因此您可以直接将它们拿来使用:
(举第2节中的例子)
  1. LaunchOption option = new LaunchOption("1.9", YggdrasilAuthenticator.password("[email protected]", "password"), new MinecraftDirectory(".minecraft"));
复制代码


需要注意的是YggdrasilAuthenticator有一个无参的构造方法。不同于上面的两个工厂方法,用这个构造方法创建出来的YggdrasilAuthenticator是不带有有效的session的。也就是说,new YggdrasilAuthenticator()创建出来的YggdrasilAuthenticator要刷新一次之后才能使用。

如何刷新YggdrasilAuthenticator中的session?
YggdrasilAuthenticator中session的刷新分为被动刷新主动刷新

被动刷新就像本节一开始所说的,当要使用YggdrasilAuthenticator进行验证,但当前的session又无效时,就需要向用户询问密码来重新登录,用户此时是被动的。
YggdrasilAuthenticator默认情况下是不允许被动刷新的(因为YggdrasilAuthenticator不知道如何与用户交互),此时您需要为YggdrasilAuthenticator编写子类来实现被动刷新,例如:
  1. package yushijinhun.jmccc.test;

  2. import java.util.Scanner;
  3. import org.to2mbn.jmccc.auth.AuthenticationException;
  4. import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;

  5. public class MyYggdrasilAuthenticator extends YggdrasilAuthenticator {

  6.         // 下面两个构造方法并没有什么好看的
  7.         // 从超类生成过来的罢了
  8.         public MyYggdrasilAuthenticator() {
  9.                 super();
  10.         }

  11.         public MyYggdrasilAuthenticator(AuthenticationService sessionService) {
  12.                 super(sessionService);
  13.         }

  14.         @Override
  15.         protected PasswordProvider tryPasswordLogin() throws AuthenticationException {
  16.                 // 这个方法会在进行被动刷新时调用

  17.                 // 向用户询问邮箱与密码
  18.                 Scanner scanner = new Scanner(System.in);
  19.                 System.out.print("邮箱:");
  20.                 String email = scanner.nextLine();
  21.                 System.out.print("密码:");
  22.                 String password = scanner.nextLine();

  23.                 return YggdrasilAuthenticator.createPasswordProvider(email, password, null);
  24.         }

  25. }
复制代码


我们可以来测试一下:
  1. package yushijinhun.jmccc.test;

  2. import org.to2mbn.jmccc.auth.AuthenticationException;
  3. import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;

  4. public class AuthTest {

  5.         public static void main(String[] args) throws AuthenticationException {
  6.                 // 用无参构造函数创建的YggdrasilAuthenticator是不带有session的。
  7.                 // 所以在第一次使用YggdrasilAuthenticator时会触发一次被动刷新,
  8.                 // 要求用户输入邮箱和密码。
  9.                 YggdrasilAuthenticator authenticator = new MyYggdrasilAuthenticator();

  10.                 // 循环十次要求提供登录信息
  11.                 for (int i = 0; i < 10; i++)
  12.                         System.out.println(authenticator.auth());
  13.         }

  14. }
复制代码

控制台输出:
  1. 邮箱:********@qq.com
  2. 密码:********
  3. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
  4. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
  5. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
  6. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
  7. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
  8. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
  9. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
  10. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
  11. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
  12. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
复制代码

// 因为是个人敏感信息,所以我就无耻的打码了

可以看到虽然调用了使用了十次YggdrasilAuthenticator(调用auth()),但只向用户询问了一次密码。这说明YggdrasilAuthenticator记住了之前的登录状态。


主动刷新是指,程序主动要求YggdrasilAuthenticator刷新session。可以通过调用下面两个方法实现:
方法意义
refreshWithPassword(String, String)用邮箱和密码来刷新当前session
refreshWithToken(String, String)用token来刷新当前session

我们来测试一下:
  1. package yushijinhun.jmccc.test;

  2. import org.to2mbn.jmccc.auth.AuthenticationException;
  3. import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;

  4. public class AuthTest {

  5.         public static void main(String[] args) {
  6.                 // 这样创建的YggdrasilAuthenticator不带有有效session
  7.                 // 并且我们也没有实现被动刷新
  8.                 YggdrasilAuthenticator authenticator = new YggdrasilAuthenticator();

  9.                 // 第一次使用YggdrasilAuthenticator
  10.                 // 由于没有有效session,并且也无法进行被动刷新
  11.                 // 所以会出错
  12.                 try {
  13.                         System.out.println(authenticator.auth());
  14.                 } catch (AuthenticationException e) {
  15.                         e.printStackTrace();
  16.                 }

  17.                 // 此时我们主动去刷新它
  18.                 try {
  19.                         authenticator.refreshWithPassword("[email protected]", "password");
  20.                 } catch (AuthenticationException e) {
  21.                         e.printStackTrace();
  22.                 }

  23.                 // 第二次使用YggdrasilAuthenticator
  24.                 // 由于经过主动刷新,已经有有效的session了
  25.                 // 所以成功执行
  26.                 try {
  27.                         System.out.println(authenticator.auth());
  28.                 } catch (AuthenticationException e) {
  29.                         e.printStackTrace();
  30.                 }
  31.         }

  32. }
复制代码

控制台输出:
  1. org.to2mbn.jmccc.auth.AuthenticationException: no more authentication methods to try
  2.         at org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator.refresh(YggdrasilAuthenticator.java:293)
  3.         at org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator.session(YggdrasilAuthenticator.java:265)
  4.         at org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator.auth(YggdrasilAuthenticator.java:236)
  5.         at yushijinhun.jmccc.test.AuthTest.main(AuthTest.java:17)
  6. AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
复制代码


有一点要注意:在编写和用户交互的启动器时,使用被动刷新。尽量不用YggdrasilAuthenticator.password()和token()这些工厂方法,也尽量避免主动刷新。因为出现被动刷新只有在真的有必要要用密码登录时才会发生。


如何保存登录信息?
一般情况下我们的启动器都会有个“记住密码”的功能。但难道启动器真的保存了密码吗?这显然是不安全的。事实上启动器保存的是上次的session,到下一次再打开启动器时,便会加载上次的session。
那么如何在jmccc中实现这个功能呢?答案有两个。

第一种办法是直接序列化YggdrasilAuthenticator。此时YggdrasilAuthenticator中所含的session将一同被序列化。示例如下:
  1. package yushijinhun.jmccc.test;

  2. import java.io.File;
  3. import java.io.FileInputStream;
  4. import java.io.FileOutputStream;
  5. import java.io.IOException;
  6. import java.io.ObjectInputStream;
  7. import java.io.ObjectOutputStream;
  8. import org.to2mbn.jmccc.auth.AuthenticationException;
  9. import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;

  10. public class AuthTest {

  11.         public static void main(String[] args) throws Exception {
  12.                 // 找一个临时文件
  13.                 File file = File.createTempFile("jmccc-test", ".dat");

  14.                 saveAuth(file);
  15.                 loadAuth(file);
  16.         }

  17.         /**
  18.          * 创建一个YggdrasilAuthenticator并把它序列化到文件里。
  19.          *
  20.          * @param file 要保存到的文件
  21.          * @throws AuthenticationException 假如出现验证错误
  22.          * @throws IOException 假如出现I/O异常
  23.          */
  24.         static void saveAuth(File file) throws AuthenticationException, IOException {
  25.                 // 用密码创建一个包含有效session的YggdrasilAuthenticator
  26.                 YggdrasilAuthenticator authenticator = YggdrasilAuthenticator.password("[email protected]", "password");

  27.                 try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
  28.                         // 序列化YggdrasilAuthenticator
  29.                         out.writeObject(authenticator);
  30.                 }

  31.                 System.out.printf("YggdrasilAuthenticator已保存到%s%n", file);
  32.         }

  33.         /**
  34.          * 从文件里加载YggdrasilAuthenticator,并把它的session输出出来。
  35.          *
  36.          * @param file 要加载YggdrasilAuthenticator的文件
  37.          * @throws AuthenticationException 假如出现验证错误
  38.          * @throws IOException 假如出现I/O异常
  39.          * @throws ClassNotFoundException 假如类未找到(反序列化错误)
  40.          */
  41.         static void loadAuth(File file) throws AuthenticationException, IOException, ClassNotFoundException {
  42.                 YggdrasilAuthenticator authenticator;
  43.                 try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
  44.                         // 反序列化YggdrasilAuthenticator
  45.                         authenticator = (YggdrasilAuthenticator) in.readObject();
  46.                 }

  47.                 System.out.printf("从%s中加载了一个YggdrasilAuthenticator%n", file);
  48.                 System.out.printf("调用YggdrasilAuthenticator的auth():%s%n", authenticator.auth());
  49.         }

  50. }
复制代码

控制台输出:
  1. YggdrasilAuthenticator已保存到/tmp/jmccc-test3142550737446373738.dat
  2. 从/tmp/jmccc-test3142550737446373738.dat中加载了一个YggdrasilAuthenticator
  3. 调用YggdrasilAuthenticator的auth():AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
复制代码


另一种方式是,调用getCurrentSession()返回当前的session(可能为null),然后将这个对象序列化。下一次时使用无参的构造方法创建YggdrasilAuthenticator,然后调用setCurrentSession(Session)将session设置回去。在此便不多叙述。


如何实现角色的选择?
对于这一段的小标题读者可能不大理解,什么叫角色选择呢?其实Yggdrasil允许一个账户拥有多个角色,这里的角色就可以相当于minecraft中的玩家。当然,mojang目前似乎还没开放这个功能。但可以通过使用第三方的Yggdrasil服务提供商体验一下(比如authlib-agent)。(好像有点扯远了)
虽然mojang没有实现这个功能,但我们总得防范与未然吧,而且正版启动器以及HMCL等启动器都有实现这个功能。那么在jmccc中应该如何做到呢?
首先要为CharacterSelector接口编写一个实现类。这个接口中定义了一个select(GameProfile[])方法,即从所给的角色中选择一个。(GameProfile即角色)
下面给出一个例子:
  1. package yushijinhun.jmccc.test;

  2. import java.util.Scanner;
  3. import org.to2mbn.jmccc.auth.yggdrasil.CharacterSelector;
  4. import org.to2mbn.jmccc.auth.yggdrasil.core.GameProfile;

  5. public class MyCharacterSelector implements CharacterSelector {

  6.         @Override
  7.         public GameProfile select(GameProfile[] availableProfiles) {
  8.                 // 与用户交互
  9.                 for (int i = 0; i < availableProfiles.length; i++) {
  10.                         System.out.printf("[%d] %s%n", i, availableProfiles[i].getName());
  11.                 }
  12.                 System.out.printf("请从上面的角色中选择一个(输入序号):");
  13.                 Scanner scanner = new Scanner(System.in);
  14.                 int index = scanner.nextInt();

  15.                 // 返回要使用的角色
  16.                 return availableProfiles[index];
  17.         }

  18. }
复制代码


上面我们知道了可以使用YggdrasilAuthenticator.password(String, String)这个工厂方法创建一个YggdrasilAuthenticator,也知道了可以用YggdrasilAuthenticator.refreshWithPassword(String, String)刷新当前session。
如果说我们要进行角色选择,应该用什么方法来传递给YggdrasilAuthenticator一个CharacterSelector,让它知道在选择角色时通知我们呢?
这就要使用YggdrasilAuthenticator.password(String, String, CharacterSelector)和YggdrasilAuthenticator.refreshWithPassword(String, String, CharacterSelector)。(两个重载方法)

在进行登录时,假如需要对角色进行选择,则YggdrasilAuthenticator会调用CharacterSelector的select(GameProfile[])。
(注:只有当可以进行角色选择时才会调用select(GameProfile[]))

示例如下:
(注:因为mojang不能有多个角色,所以我使用了authlib-agent自己建了个yggdrasil服务端,下面的代码和上面的会略有出入。关于jmccc自定义yggdrasil服务提供商,请见第12节)
  1. package yushijinhun.jmccc.test;

  2. import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;
  3. import org.to2mbn.jmccc.auth.yggdrasil.core.AuthenticationService;
  4. import org.to2mbn.jmccc.auth.yggdrasil.core.yggdrasil.YggdrasilServiceBuilder;

  5. public class AuthTest {

  6.         public static void main(String[] args) throws Exception {
  7.                 AuthenticationService authenticationService = YggdrasilServiceBuilder.create()
  8.                                 .setAPIProvider(new yushijinhunYggdrasilAPIProvider())
  9.                                 .loadSessionPublicKey("/home/yushijinhun/yushijinhun_yggdrasil_pubkey.der")
  10.                                 .buildAuthenticationService();

  11.                 // 用特定的AuthenticationService创建一个AuthenticationService
  12.                 // 这样这个YggdrasilAuthenticator就会向我的Yggdrasil服务请求,而不是Mojang的
  13.                 YggdrasilAuthenticator authenticator = new YggdrasilAuthenticator(authenticationService);

  14.                 // 然后用密码登录
  15.                 authenticator.refreshWithPassword("[email protected]", "123456", new MyCharacterSelector());

  16.                 // 输出登录信息
  17.                 System.out.println(authenticator.auth());
  18.         }

  19. }
复制代码

控制台输出:
  1. [0] test_player
  2. [1] yushijinhun
  3. 请从上面的角色中选择一个(输入序号):1
  4. AuthInfo [username=yushijinhun, token=9395d1cdc7cf40a9a9fcf728aabdd7e7, uuid=8faf6e06d9e147fa8f63b9a6c19c5d5b, properties={}, userType=mojang]
复制代码

p.s. 因为这个yggdrasil服务端是我测试用的,在mojang服务器上并没有这个账号,所以不打码也无妨。

除了在主动刷新时进行角色选择,在被动刷新时也可以进行角色选择。只需对上面的MyYggdrasilAuthenticator做一下修改。
将tryPasswordLogin()中的return改为如下:
  1. return YggdrasilAuthenticator.createPasswordProvider(email, password, new MyCharacterSelector());
复制代码

可以看到第三个参数发生了变化。原来是null,代表使用默认的角色选择器。而现在是使用我们所指定的角色选择器。
再编写代码测试一下:
  1. package yushijinhun.jmccc.test;

  2. import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;
  3. import org.to2mbn.jmccc.auth.yggdrasil.core.AuthenticationService;
  4. import org.to2mbn.jmccc.auth.yggdrasil.core.yggdrasil.YggdrasilServiceBuilder;

  5. public class AuthTest {

  6.         public static void main(String[] args) throws Exception {
  7.                 AuthenticationService authenticationService = YggdrasilServiceBuilder.create()
  8.                                 .setAPIProvider(new yushijinhunYggdrasilAPIProvider())
  9.                                 .loadSessionPublicKey("/home/yushijinhun/yushijinhun_yggdrasil_pubkey.der")
  10.                                 .buildAuthenticationService();

  11.                 // 用特定的AuthenticationService创建一个AuthenticationService
  12.                 // 这样这个YggdrasilAuthenticator就会向我的Yggdrasil服务请求,而不是Mojang的
  13.                 YggdrasilAuthenticator authenticator = new MyYggdrasilAuthenticator(authenticationService);

  14.                 // 输出登录信息
  15.                 System.out.println(authenticator.auth());
  16.         }

  17. }
复制代码

可以看到只是将上面例子里的refreshWithPassword删掉了,并且换成了MyYggdrasilAuthenticator(因为我们要处理被动刷新)。变主动刷新为被动刷新,不指定密码,让YggdrasilAuthenticator来询问我们密码。
控制台输出:
  1. 邮箱:[email protected]
  2. 密码:123456
  3. [0] test_player
  4. [1] yushijinhun
  5. 请从上面的角色中选择一个(输入序号):1
  6. AuthInfo [username=yushijinhun, token=889af3a4d0f04277aaf3431f0a48a38e, uuid=8faf6e06d9e147fa8f63b9a6c19c5d5b, properties={}, userType=mojang]
复制代码




第3节到此便结束了,内容可能不太好理解,但只要各位多写代码运行运行就没问题。



4. 下载游戏
游戏下载是jmccc的一个可选功能,您必须确保您已经导入了jmccc-mcdownloader这个依赖。

先介绍一下jmccc-mcdownloader中几个比较重要的类:
MinecraftDownloader - jmccc-mcdownloader中最为重要的类,提供了下载的接口方法。所有下载任务都是提供这个类提交上去的。
MinecraftDownloaderBuilder - 用来配置及创建MinecraftDownloader的类。下载时的代理、超时时间、下载源,如何检查缺失文件,缓存策略等都是通过这个类配置的。
Callback - 异步处理的回调接口。包含了done(),failed(),cancelled()三个回调方法,分别在任务成功完成、任务失败、任务被取消时调用。
DownloadTask - 代表了一个下载任务。每一个下载任务都有一个明确的URL表明数据来源。
DownloadCallback - 下载的异步处理的回调接口,继承自Callback。还包含了updateProgress()和retry()用来汇报下载时的进度和重试情况。
CombinedDownloadTask - 由多个下载任务组合而成的组合任务。每一个CombinedDownloadTask都可以派生出若干个DownloadTask。
CombinedDownloadCallback - 组合任务的异步处理的回调接口。还包含了taskStart(),在该CombinedDownloadTask派生出一个DownloadTask时会调用该方法。

首先要搞清楚DownloadTask与CombinedDownloadTask的关系。DownloadTask是下载一个文件的任务,CombinedDownloadTask是由多个DownloadTask组合而成的任务。如下图:


要下载Minecraft,首先要创建一个MinecraftDownloader对象,可以通过以下方式:
  1. MinecraftDownloader downloader = MinecraftDownloaderBuilder.buildDefault();
复制代码


然后便可以调用它的downloadIncrementally(MinecraftDirectory, String, CombinedDownloadCallback<Version>)方法来下载Minecraft了。
其中第个一参数是.minecraft目录位置;第二个参数是版本名称,如"1.9";第三个是回调接口,返回的是一个Version,代表实际下载到的Minecraft版本。
downloadIncrementally方法会自动检查缺失的文件(如libraries、assets),并下载。
MinecraftDownloader中所有的downloadXXX方法都是异步的(当然也包括上面的downloadIncrementally)。调用之后会立即返回。任务完成后会通知回调。

如下面的这段代码下载了Minecraft 1.9:
(注:可以使用CallbackAdapter来避免接口内写重复的空方法,类似于swing的Adapter)
  1. package yushijinhun.jmccc.test;

  2. import org.to2mbn.jmccc.mcdownloader.MinecraftDownloader;
  3. import org.to2mbn.jmccc.mcdownloader.MinecraftDownloaderBuilder;
  4. import org.to2mbn.jmccc.mcdownloader.download.DownloadCallback;
  5. import org.to2mbn.jmccc.mcdownloader.download.DownloadTask;
  6. import org.to2mbn.jmccc.mcdownloader.download.concurrent.CallbackAdapter;
  7. import org.to2mbn.jmccc.option.MinecraftDirectory;
  8. import org.to2mbn.jmccc.version.Version;

  9. public class DownloadTest {

  10.         public static void main(String[] args) {
  11.                 // 下载位置(要下载到的.minecraft目录)
  12.                 MinecraftDirectory dir = new MinecraftDirectory("/home/yushijinhun/.minecraft");

  13.                 // 创建MinecraftDownloader
  14.                 MinecraftDownloader downloader = MinecraftDownloaderBuilder.create().build();

  15.                 // 下载Minecraft1.9
  16.                 downloader.downloadIncrementally(dir, "1.9", new CallbackAdapter<Version>() {

  17.                         @Override
  18.                         public void done(Version result) {
  19.                                 // 当完成时调用
  20.                                 // 参数代表实际下载到的Minecraft版本
  21.                                 System.out.printf("下载完成,下载到的Minecraft版本:%s%n", result);
  22.                         }

  23.                         @Override
  24.                         public void failed(Throwable e) {
  25.                                 // 当失败时调用
  26.                                 // 参数代表是由于哪个异常而失败的
  27.                                 System.out.printf("下载失败%n");
  28.                                 e.printStackTrace();
  29.                         }

  30.                         @Override
  31.                         public void cancelled() {
  32.                                 // 当被取消时调用
  33.                                 System.out.printf("下载取消%n");
  34.                         }

  35.                         @Override
  36.                         public <R> DownloadCallback<R> taskStart(DownloadTask<R> task) {
  37.                                 // 当有一个下载任务被派生出来时调用
  38.                                 // 在这里返回一个DownloadCallback就可以监听该下载任务的状态
  39.                                 System.out.printf("开始下载:%s%n", task.getURI());
  40.                                 return new CallbackAdapter<R>() {

  41.                                         @Override
  42.                                         public void done(R result) {
  43.                                                 // 当这个DownloadTask完成时调用
  44.                                                 System.out.printf("子任务完成:%s%n", task.getURI());
  45.                                         }

  46.                                         @Override
  47.                                         public void failed(Throwable e) {
  48.                                                 // 当这个DownloadTask失败时调用
  49.                                                 System.out.printf("子任务失败:%s。原因:%s%n", task.getURI(), e);
  50.                                         }

  51.                                         @Override
  52.                                         public void cancelled() {
  53.                                                 // 当这个DownloadTask被取消时调用
  54.                                                 System.out.printf("子任务取消:%s%n", task.getURI());
  55.                                         }

  56.                                         @Override
  57.                                         public void retry(Throwable e, int current, int max) {
  58.                                                 // 当这个DownloadTask因出错而重试时调用
  59.                                                 // 重试不代表着失败
  60.                                                 // 也就是说,一个DownloadTask可以重试若干次,
  61.                                                 // 每次决定要进行一次重试时就会调用这个方法
  62.                                                 // 当最后一次重试失败,这个任务也将失败了,failed()才会被调用
  63.                                                 // 所以调用顺序就是这样:
  64.                                                 // retry()->retry()->...->failed()
  65.                                                 System.out.printf("子任务重试[%d/%d]:%s。原因:%s%n", current, max, task.getURI(), e);
  66.                                         }
  67.                                 };
  68.                         }
  69.                 });
  70.         }

  71. }
复制代码

当下载成功时控制台输出:
  1. 开始下载:https://launchermeta.mojang.com/mc/game/version_manifest.json
  2. 子任务完成:https://launchermeta.mojang.com/mc/game/version_manifest.json
  3. 开始下载:https://launchermeta.mojang.com/mc/game/6768033e216468247bd031a0a2d9876d79818f8f/1.9.json
  4. 子任务完成:https://launchermeta.mojang.com/mc/game/6768033e216468247bd031a0a2d9876d79818f8f/1.9.json
  5. 开始下载:https://launchermeta.mojang.com/mc-staging/assets/1.9/092c59b361816c7fa7f000587caa977c515b179c/1.9.json
  6. 开始下载:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar
  7. 开始下载:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar
  8. 子任务完成:https://launchermeta.mojang.com/mc-staging/assets/1.9/092c59b361816c7fa7f000587caa977c515b179c/1.9.json
  9. 开始下载:http://resources.download.minecraft.net/4b/4b90ff3a9b1486642bc0f15da0045d83a91df82e
  10. 子任务完成:http://resources.download.minecraft.net/4b/4b90ff3a9b1486642bc0f15da0045d83a91df82e
  11. 子任务完成:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar
  12. 下载完成,下载到的Minecraft版本:1.9
  13. 子任务完成:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar
复制代码

当下载失败时(通过拔网线实现)控制台输出:
  1. 开始下载:https://launchermeta.mojang.com/mc/game/version_manifest.json
  2. 开始下载:https://launchermeta.mojang.com/mc/game/6768033e216468247bd031a0a2d9876d79818f8f/1.9.json
  3. 子任务完成:https://launchermeta.mojang.com/mc/game/version_manifest.json
  4. 子任务完成:https://launchermeta.mojang.com/mc/game/6768033e216468247bd031a0a2d9876d79818f8f/1.9.json
  5. 开始下载:https://launchermeta.mojang.com/mc-staging/assets/1.9/092c59b361816c7fa7f000587caa977c515b179c/1.9.json
  6. 开始下载:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar
  7. 开始下载:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar
  8. 子任务重试[1/3]:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar。原因:java.net.UnknownHostException: launcher.mojang.com: unknown error
  9. 子任务重试[2/3]:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar。原因:java.net.UnknownHostException: launcher.mojang.com
  10. 子任务失败:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar。原因:java.net.UnknownHostException: launcher.mojang.com
  11. 子任务重试[1/3]:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar。原因:java.net.UnknownHostException: libraries.minecraft.net: unknown error
  12. 子任务重试[2/3]:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar。原因:java.net.UnknownHostException: libraries.minecraft.net
  13. 子任务失败:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar。原因:java.net.UnknownHostException: libraries.minecraft.net
  14. 子任务取消:https://launchermeta.mojang.com/mc-staging/assets/1.9/092c59b361816c7fa7f000587caa977c515b179c/1.9.json
  15. 下载失败
  16. java.net.UnknownHostException: launcher.mojang.com
  17.         at java.net.InetAddress.getAllByName0(InetAddress.java:1280)
  18. ......// 此处省略
复制代码


如果说要下载Minecraft版本列表,则需要调用fetchRemoteVersionList(CombinedDownloadCallback<RemoteVersionList>)方法。如下:
  1. downloader.fetchRemoteVersionList(new CallbackAdapter<RemoteVersionList>() {

  2.         @Override
  3.         public void done(RemoteVersionList result) {
  4.                 System.out.printf("版本列表下载完成:%s%n", result);
  5.         }
  6.         
  7.         // ............省略其它方法
  8. });
复制代码

控制台输出如下:
  1. 版本列表下载完成:[latestSnapshot=1.RV-Pre1, latestRelease=1.9.2, versions={16w05b=RemoteVersion [version=16w05b, ............
复制代码

注:如果说要在下载完后启动Minecraft的话,可以直接将Version对象传进LaunchOption的构造函数中。另外实际下载到的Minecraft的version id可能会与downloadIncrementally指定的不同,一般出现在下载forge时(下一节会有介绍)。

当使用MinecraftDownloaderBuilder创建MinecraftDownloader时,可以通过方法链来自定义配置。下面是MinecraftDownloaderBuilder里的一些方法:
方法意义
setMaxConnections(int)设置下载时的最大链接数
setMaxConnectionsPerRouter(int)设置NIO下每个I/O Dispatcher线程的最大链接数
setConnectTimeout(int)设置连接超时的毫秒数
setSoTimeout(int)设置Socket超时的毫秒数
setBaseProvider(MinecraftDownloadProvider)设置下载源
appendProvider(MinecraftDownloadProvider)将一个拓展下载源添加到解析链中
setPoolMaxThreads(int)设置线程池的最大线程数
setPoolThreadLivingTime(long)设置线程池里线程在不使用后最大的存活时间(毫秒)
setDefaultTries(int)设置下载失败后最大的尝试次数(默认为3,不宜过大)
setUseVersionDownloadInfo(boolean)设置是否从json中指定的url下载(即1.9的新json格式,默认true)
setCheckAssetsHash(boolean)设置是否通过计算assets的hash来判断完整性(默认true)
setCheckLibrariesHash(boolean)设置是否通过计算libraries的hash来判断完整性(默认false,文件hash会与1.9新json中指定的hash值比较)


关于如何使用自定义的下载源,下面以BMCL API为例:
  1. package org.to2mbn.jmccc.mcdownloader.wiki.provider;

  2. import org.to2mbn.jmccc.mcdownloader.provider.DefaultLayoutProvider;

  3. public class BmclApiProvider extends DefaultLayoutProvider {

  4.         @Override
  5.         protected String getLibraryBaseURL() {
  6.                 return "http://bmclapi2.bangbang93.com/libraries/";
  7.         }

  8.         @Override
  9.         protected String getVersionBaseURL() {
  10.                 return "http://bmclapi2.bangbang93.com/versions/";
  11.         }

  12.         @Override
  13.         protected String getAssetIndexBaseURL() {
  14.                 return "http://bmclapi2.bangbang93.com/indexes/";
  15.         }

  16.         @Override
  17.         protected String getVersionListURL() {
  18.                 return "http://bmclapi2.bangbang93.com/mc/game/version_manifest.json";
  19.         }

  20.         @Override
  21.         protected String getAssetBaseURL() {
  22.                 return "http://bmclapi2.bangbang93.com/assets/";
  23.         }

  24. }
复制代码

然后只要在创建MinecraftDownloader时,调用setBaseProvider即可:
  1. MinecraftDownloader downloader = MinecraftDownloaderBuilder.create()
  2.         .setBaseProvider(new BmclApiProvider())
  3.         .build();
复制代码


最后,您必须手动关闭MinecraftDownloader,否则MinecraftDownloader占用的资源将不会释放(如缓存、线程等):
  1. downloader.shutdown();
复制代码

要注意的是,MinecraftDownloader是一个重量级对象,创建和销毁都会消耗大量的系统资源。所以在一般情况下推荐启动器使用一个MinecraftDownloader对象。



5. 下载并安装Forge及Liteloader

本节将介绍Forge、Liteloader版本列表的获取,以及Forge、Liteloader的下载安装。

我们知道,Forge和Liteloader的版本在1.6(新.minecraft目录格式)之后,在versions目录中都是单独算一个版本的,比如1.7.10-LiteLoader1.7.10、1.8.9-forge1.8.9-11.15.1.1757。在jmccc也是这样,1.7.10-LiteLoader1.7.10、1.8.9-forge1.8.9-11.15.1.1757它们都是一个Minecraft版本,与1.9、1.7.10这样的版本是同等地位的,所以这些Forge、Liteloader版本可以像正常的Minecraft版本一样下载、启动。
要支持Forge和Liteloader,首先要创建一个ForgeDownloadProvider和LiteloaderDownloadProvider。它们提供了对Forge、Liteloader的解析。然后通过MinecraftDownloaderBuilder的appendProvider方法将它们添加到MinecraftDownloader的解析链中:
  1. ForgeDownloadProvider forgeProvider = new ForgeDownloadProvider();
  2. LiteloaderDownloadProvider liteloaderProvider = new LiteloaderDownloadProvider();
  3. MinecraftDownloader downloader = MinecraftDownloaderBuilder.create()
  4.         .appendProvider(forgeProvider)
  5.         .appendProvider(liteloaderProvider)
  6.         .build();
复制代码

注意:appendProvider(liteloaderProvider)需要在appendProvider(forgeProvider)之后调用,否则将不能下载同时Forge、Liteloader并存的版本。
这样,这个MinecraftDownloader就具备了下载Forge、Liteloader的能力。

要下载Forge或Liteloader,首先要获取它们的版本列表,如下:
  1. downloader.download(forgeProvider.forgeVersionList(), new CallbackAdapter<ForgeVersionList>() {...});
  2. downloader.download(liteloaderProvider.liteloaderVersionList(), new CallbackAdapter<LiteloaderVersionList>() {...});
复制代码

p.s. 上面省略了Callback中的方法

对于ForgeVersionList,有以下方法:
方法意义
getVersions()获取所有的ForgeVersion。返回一个Map,key为build number。
getLatests()获取所有标记为latest的版本,即每个Minecraft版本所对应的最新的ForgeVersion。返回一个Map,key为minecraft版本,value是此minecraft版本对应的最新的ForgeVersion。
getRecommendeds()获取所有标记为recommended的版本,即每个Minecraft版本所对应的推荐的ForgeVersion。返回一个Map,key为minecraft版本,value是此minecraft版本对应的推荐的ForgeVersion。
getLatest()获取最最新的ForgeVersion。不考虑minecraft版本。
getLatest(String)获取给定的minecraft版本最新的ForgeVersion。
getRecommended()获取最新的推荐的ForgeVersion。不考虑minecraft版本。
getRecommended(String)获取给定的minecraft版本推荐的ForgeVersion。


对于LiteloaderVersionList,有以下方法:
方法意义
getLatests()获取每个minecraft版本对应的最新的LiteloaderVersion。返回的是Map,key为minecraft版本,value为该版本对应的最新的LiteloaderVersion。
getLatest(String)获取给定的minecraft版本最新的LiteloaderVersion。

注意:因为Liteloader开发者所提供的api过于变态,因此不支持snapshot版本,并且只能下载某个minecraft版本对应的最新的liteloader。

在获取到要下载的ForgeVersion或LiteloaderVersion后就可以正常下载了,比如:
  1. // 这里的forgeVersion即要下载的forge版本
  2. downloader.downloadIncrementally(dir, forgeVersion.getVersionName(), new CallbackAdapter<Version>() {......});
复制代码

可以看到和上文下载官方的minecraft版本并没有什么区别,只是使用了ForgeVersion的getVersionName()来作为要下载的minecraft的版本号罢了。要下载LiteloaderVersion如法炮制即可。

如果说要下载一个Forge和Liteloader并存的minecraft该怎么办呢?
假如我已经挑选好了ForgeVersion和LiteloaderVersion(forge和liteloader对应的minecraft版本要一致,不然肯定不能启动),那么只要将downloadIncrementally中的版本号替换为:
  1. liteloaderVersion.customize(forgeVersion.getVersionName()).getVersionName()
复制代码

至此,Forge、Liteloader相关内容就讲解完了。



6. 获取正版玩家的皮肤
从本节开始,我们就将开始介绍jmccc的高级用法。一般来说,听不懂是正常的。;(手动斜眼

第3节中,我们学到了YggdrasilAuthenticator。事实上,YggdrasilAuthenticator的底层是YggdrasilService,YggdrasilService提供了一组访问Yggdrasil服务的接口。而与YggdrasilService并行的就是ProfileService,提供了和游戏角色相关的接口。
我们可以通过下面的方法来创建一个ProfileService:
  1. ProfileService profileService = YggdrasilServiceBuilder.defaultProfileService();
复制代码

ProfileService中有三个方法,它们分别是:
方法意义
lookupUUIDByName(String)查询与玩家游戏中的名称对应的UUID。
getGameProfile(UUID)查询给定UUID的角色的信息。
getTextures(GameProfile)从给定的角色信息中获取皮肤(等)。

注意:上面这三个方法都是要访问网络的,并且会阻塞,所以千万别脑残在UI线程之类的里面调用。

下面给出一个例子:
  1. package yushijinhun.jmccc.test;

  2. import java.util.UUID;
  3. import org.to2mbn.jmccc.auth.AuthenticationException;
  4. import org.to2mbn.jmccc.auth.yggdrasil.core.GameProfile;
  5. import org.to2mbn.jmccc.auth.yggdrasil.core.PlayerTextures;
  6. import org.to2mbn.jmccc.auth.yggdrasil.core.ProfileService;
  7. import org.to2mbn.jmccc.auth.yggdrasil.core.yggdrasil.YggdrasilServiceBuilder;

  8. public class ProfileServiceTest {

  9.         public static void main(String[] args) throws AuthenticationException {
  10.                 ProfileService profileService = YggdrasilServiceBuilder.defaultProfileService();

  11.                 // 查询ztcjohn的uuid
  12.                 UUID uuid = profileService.lookupUUIDByName("ztcjohn");
  13.                 System.out.println(uuid);

  14.                 // 根据uuid获取GameProfile
  15.                 // 注意啦!假如lookupUUIDByName没找到对应玩家就会返回null。我这里因为是演示所以偷懒不做判断,大家写的时候记得一定要判断一下
  16.                 GameProfile profile = profileService.getGameProfile(uuid);

  17.                 // 获取GameProfile的皮肤
  18.                 // 同上,假如getGameProfile没找到对应玩家就会返回null。我这里是偷懒,大家写的时候千万不要学
  19.                 PlayerTextures textures = profileService.getTextures(profile);
  20.                 System.out.println(textures);
  21.         }

  22. }
复制代码

控制台输出:
  1. 469aa369-6304-40d4-8b99-7e63199677ac
  2. PlayerTextures [skin=Texture [url=http://textures.minecraft.net/texture/2b28e73ff96914596963cb468da14fdb5e36217a6e327e8353efb44ec71, metadata=null], cape=Texture [url=http://textures.minecraft.net/texture/efd61c3c4ac88f1a3468fbdeef45cec89e5afb87b97a1a845bfb3c64fd0b883, metadata=null], elytra=null]
复制代码

可以看到ztcjohn不但有一个皮肤还有一个披风。getTextures方法返回的PlayerTextures包含了皮肤、披风、elytra(1.9新加的滑翔翼)。要注意的是,并不是每个角色都有这三个东西。
(对不起咯~ztc喵)
PlayerTextures只包含皮肤的url,具体图像需要再去下载。

如果说要判断一个角色是Alex还是Steve,可以通过下面这个方法:
  1. public static boolean isAlex(PlayerTextures textures) {
  2.         Texture skin = textures.getSkin();
  3.         if (skin != null) {
  4.                 Map<String, String> metadata = skin.getMetadata();
  5.                 if (metadata != null) {
  6.                         return "slim".equals(metadata.get("model"));
  7.                 }
  8.         }
  9.         return false;
  10. }
复制代码




7. OS X下Dock相关配置
官方启动器在OSX下时,启动Minecraft的时候会添加一些和Dock相关的参数,用来设置Minecraft在Dock中的呈现。(貌似是这样)

在jmccc中,您需要将ExtraArgumentsTemplates.OSX_DOCK_NAME和ExtraArgumentsTemplates.OSX_DOCK_ICON(MinecraftDirectory, Version)的返回值加进JVM参数中。在此之前,务必要检查当前系统是否为OSX,这两个参数只有在OSX下的JVM里才是有效的。
比如:
  1. MinecraftDirectory dir = new MinecraftDirectory(".minecraft");
  2. LaunchOption option = new LaunchOption("1.9", new OfflineAuthenticator("test_player"), dir);

  3. // 一定要先判断是否为OSX
  4. if (Platform.CURRENT == Platform.OSX) {
  5.         option.setExtraJvmArguments(Arrays.asList(
  6.                         ExtraArgumentsTemplates.OSX_DOCK_NAME,
  7.                         ExtraArgumentsTemplates.OSX_DOCK_ICON(dir, option.getVersion())));
  8. }
复制代码




8. 忽略Forge的数字签名
有些Forge版本要添加-Dfml.ignoreInvalidMinecraftCertificates=true和-Dfml.ignorePatchDiscrepancies=true这两项JVM参数才能正常启动。出现这种情况一般是往jar里塞了一些奇怪的东西,然后FML发现jar的数字签名损坏了。解决方法是往JVM参数列表里加上这两个参数(同前一节)。
当然,jmccc是不会让您手动打这两项参数的。这两个参数都已经在ExtraArgumentsTemplates里面预定义好了,只需引用即可。
  1. option.setExtraJvmArguments(Arrays.asList(
  2.                 ExtraArgumentsTemplates.FML_IGNORE_INVALID_MINECRAFT_CERTIFICATES,
  3.                 ExtraArgumentsTemplates.FML_IGNORE_PATCH_DISCREPANCISE));
复制代码




9. 使用HttpAsyncClient来进行下载
下载Minecraft时一般会下载大量文件(上千个),如果逐一下载效率固然很低,所以jmccc是允许多个任务同时下载的。默认情况下,jmccc用的是jdk自带的BIO(阻塞式I/O),会给每一个链接打开一个线程。当链接数很大时,便会造成系统资源的严重浪费。解决方法是切换到NIO(非阻塞式I/O),这样仅用数个线程便可以处理上万个链接,将下载速度最大化。
如果说要在jmccc中使用NIO来下载,则需要添加Apache HttpAsyncClient这个依赖:
  1. org.apache.httpcomponents:httpasyncclient:4.1.1
复制代码

jmccc在初始化时,如果发现classpath中存在HttpAsyncClient,则会自动使用HttpAsyncClient来下载。
在将HttpAsyncClient添加到classpath中后,您便可以将最大链接数调到任意大了:
  1. MinecraftDownloader downloader = MinecraftDownloaderBuilder.create()
  2.         .setMaxConnections(4096)
  3.         .build();
复制代码

比如这样将MaxConnections调到了4096,那么最多可以同时下载4096个文件。

如果说您不想使用HttpAsyncClient,您可以调用MinecraftDownloaderBuilder的disableApacheHttpAsyncClient()来禁用对HttpAsyncClient的支持。这样jmccc就只会使用BIO来下载文件。



10. 使用Ehcache缓存下载的文件
jmccc有时候会多次下载同一个文件,这样就会造成不必要的网络I/O和等待时间。所以jmccc提供了对Ehcache的支持(Ehcache是最有名的一个轻量级Java缓存框架)。如果说要启用这个缓存功能的话首先要将Ehcache添加到依赖,需要注意的是jmccc使用的是Ehcache3,不是2。
  1. org.ehcache.modules:ehcache-impl:3.0.0.rc2
复制代码

注意:ehcache使用了slf4j作为logging框架,推荐您的项目中至少包含一个slf4j的实现。

将ehcache添加到classpath之后,您便可以对缓存进行配置了。
ehcache提供了三种类型的缓存:
堆上缓存(heap) - 存储在java堆上的缓存
离堆缓存(offheap) - 存储在本地内存中的缓存,在java堆之外,不受jvm托管
磁盘缓存(disk) - 存储在磁盘上的缓存

jmccc默认开启32MB的堆上缓存,不开启离堆缓存和磁盘缓存,缓存的有效时间是2小时。您可以使用下面的这些方法来设置缓存:
方法意义
setHeapCacheSize(long)设置Java堆上缓存的最大大小,0则不开启,单位:MB。
setOffheapCacheSize(long)设置离堆缓存(本地内存)的最大大小,0则不开启,单位:MB。
setDiskCacheSize(long)设置磁盘上缓存的最大大小,0则不开启,单位:MB。开启该功能后还需调用setDiskCacheDir(File)进行设置。
setDiskCacheDir(File)设置磁盘上用于存储缓存的目录。
setCacheLiveTime(long, TimeUnit)配置缓存的有效时间(TTL),超过该时间的缓存将被自动清除。


比如下面这段代码,只将最大为128M的缓存存储在磁盘上,有效时间1天:
  1. MinecraftDownloader downloader = MinecraftDownloaderBuilder.create()
  2.         .setHeapCacheSize(0) // 关闭堆上缓存
  3.         .setOffheapCacheSize(0) // 关闭离堆缓存
  4.         .setDiskCacheSize(128) // 开启最大为128MB的磁盘缓存
  5.         .setDiskCacheDir(new File("/tmp/jmccc-cache")) // 存储缓存的目录是/tmp/jmccc-cache
  6.         .setCacheLiveTime(1, TimeUnit.DAYS)// 缓冲有效时间1天
  7.         .build();
复制代码


如果说您不想使用缓存,那您可以调用MinecraftDownloaderBuilder的disableEhcache()方法来禁用对Ehcache的支持。



11. 修改Minecraft1.9中的version_type
大家用HMCL启动Minecraft1.9的时候可能会发现,Minecraft主界面下面有这样的东西:

这是怎么实现的呢?
可以打开1.9.json看看,发现其中有这样一段:
  1. "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} --versionType ${version_type}"
复制代码

不知大家有没有注意到${version_type}这一个字符串。官方启动器在启动时会自动用Minecraft版本的type来代替这段字符串,比如snapshot、release,这样在Minecraft底部就显示为Minecraft 1.9-pre2/snapshot,Minecraft 1.9/release。但HMCL则将它替换为了HMCL 2.4.1.41。
jmccc的行为默认和官方启动器一样,会将version_type指定为Version.getType()。但jmccc也提供了一个覆写该变量的方法,它就是第2节中介绍的setCommandlineVariables。

我们可以手动指定version_type的值,如下:
  1. package yushijinhun.jmccc.test;

  2. import java.util.HashMap;
  3. import java.util.Map;
  4. import org.to2mbn.jmccc.auth.OfflineAuthenticator;
  5. import org.to2mbn.jmccc.launch.Launcher;
  6. import org.to2mbn.jmccc.launch.LauncherBuilder;
  7. import org.to2mbn.jmccc.option.LaunchOption;
  8. import org.to2mbn.jmccc.option.MinecraftDirectory;

  9. public class JmcccTest {

  10.         public static void main(String[] args) throws Exception {
  11.                 Launcher launcher = LauncherBuilder.buildDefault();
  12.                 LaunchOption option = new LaunchOption("1.9", new OfflineAuthenticator("test_user"), new MinecraftDirectory("/home/yushijinhun/.minecraft"));

  13.                 // 重点:手动指定version_type
  14.                 Map<String, String> vars = new HashMap<>();
  15.                 vars.put("version_type", "JMCCC大法好!");
  16.                 option.setCommandlineVariables(vars);

  17.                 launcher.launch(option);
  18.         }
  19. }
复制代码

运行效果:




12. 使用自定义的Yggdrasil API提供商
p.s. 如果您没有深入了解过Yggdrasil服务,那么本节对您来说可能有些困难。
wiki.vg上有一篇介绍Yggdrasil的条目(英文):http://wiki.vg/Authentication

正版验证(即Yggdrasil)是Mojang提供的服务,但这不一定必须由Mojang提供。也就是说,您可以创建一套和Mojang并行的Yggdrasil服务,相当于Yggdrasil私服。事实上,yushijinhun(我)的authlib-agent就已经成功通过字节码操纵实现了对Minecraft内Yggdrasil API的重定向,并给出了一个Yggdrasil服务的开源实现。(本节的部分内容曾在第3节中出现,所以对下面的代码您可能会感到有些眼熟?)

在jmccc中,如果要手动指定Yggdrasil API提供商,就得先为YggdrasilAPIProvider编写一个子类,里面定义API的URL。
我在本地用authlib-agent架设了一个yggdrasil服务端,给YggdrasilAPIProvider编写的子类如下:
  1. package yushijinhun.jmccc.test;

  2. import java.util.UUID;
  3. import org.to2mbn.jmccc.auth.yggdrasil.core.yggdrasil.YggdrasilAPIProvider;
  4. import org.to2mbn.jmccc.util.UUIDUtils;

  5. public class yushijinhunYggdrasilAPIProvider implements YggdrasilAPIProvider {

  6.         @Override
  7.         public String authenticate() {
  8.                 return "http://localhost:8080/yggdrasil/authenticate";
  9.         }

  10.         @Override
  11.         public String refresh() {
  12.                 return "http://localhost:8080/yggdrasil/refresh";
  13.         }

  14.         @Override
  15.         public String validate() {
  16.                 return "http://localhost:8080/yggdrasil/validate";
  17.         }

  18.         @Override
  19.         public String invalidate() {
  20.                 return "http://localhost:8080/yggdrasil/invalidate";
  21.         }

  22.         @Override
  23.         public String signout() {
  24.                 return "http://localhost:8080/yggdrasil/signout";
  25.         }

  26.         @Override
  27.         public String profile(UUID profileUUID) {
  28.                 return "http://localhost:8080/yggdrasil/profiles/minecraft/" + UUIDUtils.unsign(profileUUID);
  29.         }

  30.         @Override
  31.         public String profileLookup() {
  32.                 return "http://localhost:8080/yggdrasil/profilerepo/minecraft";
  33.         }
  34. }
复制代码

然后您就可以用YggdrasilServiceBuilder来配置AuthenticationService和ProfileService了:
  1. YggdrasilServiceBuilder yggdrasilBuilder = YggdrasilServiceBuilder.create()
  2.         .setAPIProvider(new yushijinhunYggdrasilAPIProvider())
  3.         .loadSessionPublicKey("/home/yushijinhun/yushijinhun_yggdrasil_pubkey.der"); //(1)
  4. ProfileService profileService = yggdrasilBuilder.buildProfileService(); //(2)
  5. AuthenticationService authenticationService = yggdrasilBuilder.buildAuthenticationService(); //(3)
复制代码

上面代码中(1)代表从/home/yushijinhun/yushijinhun_yggdrasil_pubkey.der这个文件中加载Yggdrasil的数字签名公钥。该文件应是PKCS#8格式的RSA证书的SubjectPublicKeyInfo部分(与mojang authlib使用的密钥格式相同)。您也可以使用setSessionPublicKey(PublicKey)方法直接设置公钥。
(2)代表用上面的YggdrasilServiceBuilder创建一个ProfileService。这个ProfileService是绑定到上面指定的Yggdrasil API上的,上面第6节有介绍。
(3)代表用上面的YggdrasilServiceBuilder创建一个AuthenticationService。这个AuthenticationService是绑定到上面指定的Yggdrasil API上的。

您可以通过new YggdrasilAuthenticator(authenticationService)来创建一个使用指定的AuthenticationService的YggdrasilAuthenticator。
需要注意的是,如果您要序列化YggdrasilAuthenticator,请务必重写createAuthenticationServiceForDeserialization()方法。这个方法用来在反序列化过程中重新创建AuthenticationService。
  1. package yushijinhun.jmccc.test;

  2. import java.io.IOException;
  3. import java.security.NoSuchAlgorithmException;
  4. import java.security.spec.InvalidKeySpecException;
  5. import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;
  6. import org.to2mbn.jmccc.auth.yggdrasil.core.AuthenticationService;
  7. import org.to2mbn.jmccc.auth.yggdrasil.core.yggdrasil.YggdrasilServiceBuilder;

  8. public class MyYggdrasilAuthenticator extends YggdrasilAuthenticator {

  9.         /**
  10.          * 创建一个我自定义的AuthenticationService。
  11.          *
  12.          * @return 我自定义的AuthenticationService
  13.          */
  14.         private static AuthenticationService createMyAuthenticationService() {
  15.                 try {
  16.                         return YggdrasilServiceBuilder.create()
  17.                                         .setAPIProvider(new yushijinhunYggdrasilAPIProvider())
  18.                                         .loadSessionPublicKey("/home/yushijinhun/yushijinhun_yggdrasil_pubkey.der")
  19.                                         .buildAuthenticationService();
  20.                 } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
  21.                         throw new IllegalStateException("无法创建自定义的AuthenticationService!", e);
  22.                 }
  23.         }

  24.         public MyYggdrasilAuthenticator() {
  25.                 // 使用自定义的AuthenticationService
  26.                 super(createMyAuthenticationService());
  27.         }

  28.         @Override
  29.         protected AuthenticationService createAuthenticationServiceForDeserialization() {
  30.                 // 在反序列化过程中重新创建我自定义的AuthenticationService
  31.                 return createMyAuthenticationService();
  32.         }
  33. }
复制代码


题外话:由于某些原因(比如gfw),会导致国内有时候连不上Mojang的Yggdrasil服务,这时候可以用YggdrasilServiceBuilder的setProxy(Proxy)指定一个代理。



后记
jmccc从2.3到2.4,中间经过了近5个月的开发,加入了大量新的功能,也进行了大量重构。jmccc 2.4将先前就在开发的下载功能并入了主干,并对Yggdrasil部分进行了重写,因此也难免有些bug。
如果说您在使用过程中发现了jmccc的bug,欢迎将bug报告到GitHub Issues,我们将十分感谢。
如果您有什么好点子,也欢迎您在Gitter上和我们交流。
同时也欢迎您向jmccc PR代码。

jmccc提供了大量的API,因此本教程也难以面面俱到。如果说您发现教程中出现了错误,请将错误指出,我将万分感谢。
如果您对本教程有什么建议,也欢迎提出。

关于图床:本文的图片都是挂在to2mbn.github.io上的,考虑到国内部分地区封杀到github pages的https流量,所以都用了http协议。


最后,感谢所有支持jmccc开发的人!
作者: TanDan2016    时间: 2016-4-9 13:47
太好了,终于找到了JMCCC的教程了
作者: TanDan2016    时间: 2016-4-10 09:23
LZ为什么我启动不了MC,这是我的代码
  1. public class start_up
  2. {
  3.         ReadString readString=new ReadString();
  4.         public start_up()throws Exception
  5.         {
  6.                 String gameMemory = ReadString.readFileContent("","gameMemory.TD");
  7.                 String gameName = ReadString.readFileContent("", "gameName.TD");
  8.                 String gamePath = ReadString.readFileContent("", "gamePath.TD");
  9.                 String gameVersion = ReadString.readFileContent("", "gameVersion.TD");
  10.                  int a = Integer.parseInt(gameMemory);
  11.                  System.out.print("gameMemory  "+a+"  gameName  "+gameName+"  gamePath  "+gamePath+"  gameVersion"+gameVersion);
  12.                  Launcher launcher = LauncherBuilder.buildDefault();
  13.                  LaunchOption option = new LaunchOption(gameVersion, new OfflineAuthenticator(gameName), new MinecraftDirectory(gamePath));
  14.                  
  15.                  option.setMaxMemory(a);
  16.                  
  17.                  launcher.launch(option);
  18.         }
  19. }
复制代码

作者: Darkyoooooo    时间: 2016-4-10 10:40
TanDan2016 发表于 2016-4-10 09:23
LZ为什么我启动不了MC,这是我的代码

stderror有没有输出什么
作者: TanDan2016    时间: 2016-4-15 20:43
Darkyoooooo 发表于 2016-4-10 10:40
stderror有没有输出什么

空指针错误,不过我自己也经解决了
作者: 懒虫哥    时间: 2016-4-19 11:36
好复杂...这是自己做启动器吗...
作者: 18278869354    时间: 2016-4-23 21:31
空指针错误,不过我自己也经解决了

作者: lizhaohan001    时间: 2016-4-23 22:14
非常好用!!正在用。
作者: hhttll    时间: 2016-4-28 06:57
看到 Callback 就一定要吐槽了...
LZ 一定是 JS症患者2333

顺便感谢下这个库
作者: yuanzhihang1    时间: 2016-4-29 20:42
感谢分享!!!非常重要
作者: YDDonald    时间: 2016-5-1 20:53
好赞,我的阅读恐惧症又犯了
作者: _Ⅲ儿_    时间: 2016-5-2 22:03
教程一出,赌450,java启动器要炸
作者: qq1748997604    时间: 2016-5-4 08:04
提示: 作者被禁止或删除 内容自动屏蔽
作者: Ziang39    时间: 2016-5-4 11:08
赞赞赞,很棒的作品!!赞赞赞,很棒的作品!!赞赞赞,很棒的作品!!赞赞赞,很棒的作品!!赞赞赞,很棒的作品!!赞赞赞,很棒的作品!!
作者: GTA守护使者    时间: 2016-5-26 17:40
为什么报错,这是什么原因!
环境:
java version "1.7.0_80"
Java(TM) SE Runtime Environment (build 1.7.0_80-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)
Windows 7 64位 8G内存 i5CPU


调试信息:
Exception in thread "main" java.lang.NullPointerException
        at java.util.Objects.requireNonNull(Unknown Source)
        at org.to2mbn.jmccc.option.LaunchOption.<init>(LaunchOption.java:121)
        at org.to2mbn.jmccc.option.LaunchOption.<init>(LaunchOption.java:96)
        at mcgui.Mcgui.mc(Mcgui.java:34)
        at mcgui.Main.main(Main.java:12)



类一:
  1. package mcgui;

  2. public class Main {
  3.         public static void main(String[] args) throws Exception {
  4.                 Mcgui mc = new Mcgui();
  5.                 mc.logs = true; // 启动调试  boolean
  6.                 mc.charm = true; // 开启对Natives文件的快速检查  boolean
  7.                 mc.version = "1.8"; // 游戏版本  String
  8.                 mc.user = "test_user"; // 使用离线验证,用户名  String  
  9.                 mc.mineurl = "D:/Program Files (x86)/Minecraft 1.8-forge/.minecraft"; // .minecraft目录  String
  10.                 mc.mem = 512; // 最大内存512M  int
  11.                 mc.mc();
  12.                
  13.                 /*
  14.                  * mc.logs                        boolean
  15.                  * mc.charm                        boolean
  16.                  * mc.version                String
  17.                  * mc.user                        String
  18.                  * mc.mineurl                String
  19.                  * mc.mem                        int
  20.                  *
  21.                  */
  22.         }
  23. }
复制代码

类二:
  1. package mcgui;

  2. import org.json.*;
  3. import org.tukaani.xz.*;
  4. import org.to2mbn.jmccc.auth.OfflineAuthenticator;
  5. import org.to2mbn.jmccc.exec.GameProcessListener;
  6. import org.to2mbn.jmccc.launch.Launcher;
  7. import org.to2mbn.jmccc.launch.LauncherBuilder;
  8. import org.to2mbn.jmccc.option.LaunchOption;
  9. import org.to2mbn.jmccc.option.MinecraftDirectory;

  10. public class Mcgui {
  11.         boolean logs ; // 启动调试
  12.         boolean charm ; // 开启对Natives文件的快速检查
  13.         String version ; // 游戏版本
  14.         String user ; // 使用离线验证,用户名
  15.         String mineurl ; // .minecraft目录
  16.         int mem ; // 最大内存512M
  17.         
  18.         
  19.         public void mc() throws Exception {
  20.                
  21.         // 创建一个Launcher对象
  22.                 System.out.println("创建一个Launcher对象");
  23.                 Launcher launcher = LauncherBuilder.create()
  24.                             .setDebugPrintCommandline(logs) // 设置是否在启动时将启动参数输出到控制台以供调试,默认为false。
  25.                             .setNativeFastCheck(charm) // 设置是否开启对Natives文件的快速检查,默认为false。
  26.                             //开启该选项后,jmccc仅会通过比较文件大小来判断文件是否完整,这样可以加快启动速度,但有可能造成某些问题。
  27.                             .build();
  28.         System.out.println("创建对象成功");
  29.         
  30.         // 启动配置
  31.         System.out.println("启动配置");
  32.         LaunchOption option = new LaunchOption(
  33.                         version, // 游戏版本
  34.                 new OfflineAuthenticator(user), // 使用离线验证,用户名
  35.                 new MinecraftDirectory(mineurl) // .minecraft目录
  36.         );
  37.         System.out.println("启动成功!");
  38.         
  39.         // 最大内存512M
  40.         System.out.println(mem);
  41.         option.setMaxMemory(mem);
  42.         
  43.         // 启动游戏
  44.         System.out.println("启动游戏!");
  45.         
  46.         launcher.launch(option, new GameProcessListener() {

  47.             @Override
  48.             public void onLog(String log) {
  49.                 System.out.println(log); // 输出日志到控制台
  50.             }

  51.             @Override
  52.             public void onErrorLog(String log) {
  53.                 System.err.println(log); // 输出日志到控制台(同上)
  54.             }

  55.             @Override
  56.             public void onExit(int code) {
  57.                 System.err.println("游戏进程退出,状态码:" + code); // 游戏结束时输出状态码
  58.             }
  59.         });
  60.         System.out.println("游戏启动成功!");
  61.     }
  62. }
复制代码





作者: yushijinhun    时间: 2016-5-26 20:17
GTA守护使者 发表于 2016-5-26 17:40
为什么报错,这是什么原因!
环境:
java version "1.7.0_80"

这是因为指定的Minecraft版本不存在。
你可以用Versions.getVersions(MinecraftDirectory)来获取某个.minecraft目录下的所有版本。
这里抛出NullPointerException确实不是特别好,在2.5中(未发布)已经改成了IllegalArgumentException: Version not found: xxx了。
作者: tt36999    时间: 2016-5-27 00:07
本帖最后由 tt36999 于 2016-5-27 00:09 编辑

顶LZ,确实好用简单易行,不过我在使用你的JMCCC之后如果启动的时候mods文件夹内有mcheli(直升机MOD)时游戏就会提示
Forge Mod Loder has found a problem with your minecraft installation You have mod sources that are duplicate within your system Mod Id : File name
mcheli : minecraft
mcheli : mcheli不过同样的整合包和forge使用HMCL启动器不会出现类似的问题
请问你知道这是什么原因么?或者这个是由于MOD本身的BUG造成的不兼容?这个MOD是一个文件夹放在Mods目录内而不是jar包是否有影响?

作者: yushijinhun    时间: 2016-5-27 00:18
tt36999 发表于 2016-5-27 00:07
顶LZ,确实好用简单易行,不过我在使用你的JMCCC之后如果启动的时候mods文件夹内有mcheli(直升机MOD)时游 ...

具体我也不大清楚,也许你可以把debugPrintCommandline开出来然后对比一下启动命令行?
作者: 照烧鸡腿饭    时间: 2016-5-27 00:52
mark下,考完试慢慢研究{:10_492:}
作者: tt36999    时间: 2016-5-27 08:04
yushijinhun 发表于 2016-5-27 00:18
具体我也不大清楚,也许你可以把debugPrintCommandline开出来然后对比一下启动命令行? ...

好吧……

作者: jkaa13579    时间: 2016-5-30 10:33
好东西,楼主造
作者: 深海鲸鱼座    时间: 2016-6-9 17:42
本帖最后由 lj2000lj 于 2016-6-9 17:49 编辑

无法启动安装了mcheli的1.7.10客户端,不知道是不是吾辈操作有误
Jmccc调用代码与本帖中所示一致
Forge日志文件显示如下:
[时间] [Client thread/ERROR]: Found a duplicate mod mcheli at [路径\.minecraft, 路径\.minecraft\mods\mcheli]
之前有显示mcheli加载了两次,一次不能找到mcmod.info,第二次可以找到。
如果将mcheli放入mods下的1.7.10文件夹,forge会多识别出一个mcheli,并且依旧不能找到mcmod.info
其他启动器正常,MinecraftForum上有提到一款名为FTB Launcher的启动器存在同样的问题

才发现新版换人开发惹= =
作者: yushijinhun    时间: 2016-6-10 21:44
tt36999 发表于 2016-5-27 00:07
顶LZ,确实好用简单易行,不过我在使用你的JMCCC之后如果启动的时候mods文件夹内有mcheli(直升机MOD)时游 ...

你所述的这个bug已经在2.5-SNAPSHOT中修复了。具体见 https://github.com/to2mbn/JMCCC/issues/19 。
作者: officeyutong    时间: 2016-8-16 19:43
启动时 客户端一直卡在Finishing up 但是用其他启动器启动时没有问题
以下为启动命令行
C:\Program Files\Java\jdk1.7.0_51\jre\bin\java.exe
-Xmx2048M
-Djava.library.path=J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\versions\1.7.10\1.7.10-natives
-cp
J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\google\code\gson\gson\2.2.4\gson-2.2.4.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\google\guava\guava\17.0\guava-17.0.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\ibm\icu\icu4j-core-mojang\51.2\icu4j-core-mojang-51.2.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\mojang\authlib\1.5.21\authlib-1.5.21.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\mojang\realms\1.3.5\realms-1.3.5.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\mumfrey\liteloader\1.7.10\liteloader-1.7.10.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\paulscode\codecjorbis\20101023\codecjorbis-20101023.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\paulscode\codecwav\20101023\codecwav-20101023.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\paulscode\libraryjavasound\20101123\libraryjavasound-20101123.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\paulscode\librarylwjglopenal\20100824\librarylwjglopenal-20100824.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\paulscode\soundsystem\20120107\soundsystem-20120107.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\typesafe\akka\akka-actor_2.11\2.3.3\akka-actor_2.11-2.3.3.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\com\typesafe\config\1.2.1\config-1.2.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\commons-codec\commons-codec\1.9\commons-codec-1.9.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\commons-io\commons-io\2.4\commons-io-2.4.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\commons-logging\commons-logging\1.1.3\commons-logging-1.1.3.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\io\netty\netty-all\4.0.10.Final\netty-all-4.0.10.Final.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\java3d\vecmath\1.3.1\vecmath-1.3.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\lzma\lzma\0.0.1\lzma-0.0.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\net\java\jinput\jinput\2.0.5\jinput-2.0.5.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\net\java\jutils\jutils\1.0.0\jutils-1.0.0.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\net\minecraft\launchwrapper\1.12\launchwrapper-1.12.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\net\minecraftforge\forge\1.7.10-10.13.4.1614-1.7.10\forge-1.7.10-10.13.4.1614-1.7.10.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\net\sf\jopt-simple\jopt-simple\4.5\jopt-simple-4.5.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\net\sf\trove4j\trove4j\3.0.3\trove4j-3.0.3.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\apache\commons\commons-compress\1.8.1\commons-compress-1.8.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\apache\commons\commons-lang3\3.3.2\commons-lang3-3.3.2.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\apache\httpcomponents\httpclient\4.3.3\httpclient-4.3.3.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\apache\httpcomponents\httpcore\4.3.2\httpcore-4.3.2.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\apache\logging\log4j\log4j-api\2.0-beta9\log4j-api-2.0-beta9.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\apache\logging\log4j\log4j-core\2.0-beta9\log4j-core-2.0-beta9.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\lwjgl\lwjgl\lwjgl\2.9.1\lwjgl-2.9.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\lwjgl\lwjgl\lwjgl_util\2.9.1\lwjgl_util-2.9.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\ow2\asm\asm-all\5.0.3\asm-all-5.0.3.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\scala-lang\plugins\scala-continuations-library_2.11\1.0.2\scala-continuations-library_2.11-1.0.2.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\scala-lang\plugins\scala-continuations-plugin_2.11.1\1.0.2\scala-continuations-plugin_2.11.1-1.0.2.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\scala-lang\scala-actors-migration_2.11\1.1.0\scala-actors-migration_2.11-1.1.0.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\scala-lang\scala-compiler\2.11.1\scala-compiler-2.11.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\scala-lang\scala-library\2.11.1\scala-library-2.11.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\scala-lang\scala-parser-combinators_2.11\1.0.1\scala-parser-combinators_2.11-1.0.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\scala-lang\scala-reflect\2.11.1\scala-reflect-2.11.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\scala-lang\scala-swing_2.11\1.0.1\scala-swing_2.11-1.0.1.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\org\scala-lang\scala-xml_2.11\1.0.2\scala-xml_2.11-1.0.2.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\libraries\tv\twitch\twitch\5.16\twitch-5.16.jar;J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\versions\1.7.10\1.7.10.jar
net.minecraft.launchwrapper.Launch
--username
Server_crashed
--version
1.7.10-Forge10.13.4.1614-1.7.10
--gameDir
J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft
--assetsDir
J:\mc\LifeCraftX6V1.0\LCX6V2.5\.minecraft\assets
--assetIndex
1.7.10
--uuid
375b37e9a31b3d34be69788c60644134
--accessToken
fb6aa3588e6744bcbf831f2aa360d48a
--userProperties
{}
--userType
mojang
--tweakClass
cpw.mods.fml.common.launcher.FMLTweaker

作者: yushijinhun    时间: 2016-8-16 21:05
officeyutong 发表于 2016-8-16 19:43
启动时 客户端一直卡在Finishing up 但是用其他启动器启动时没有问题
以下为启动命令行
C:\Program Files\J ...

感谢您的反馈。您可不可以将其它启动器的启动参数发上来供我们对比?或者您也可以将.minecraft发到 [email protected] 来帮助我们重现bug。
作者: officeyutong    时间: 2016-8-16 22:32
yushijinhun 发表于 2016-8-16 21:05
感谢您的反馈。您可不可以将其它启动器的启动参数发上来供我们对比?或者您也可以将.minecraft发到  来帮 ...

已经解决了 用的java7 同时因为PermSize太小导致内存溢出 换了java8好了
作者: officeyutong    时间: 2016-8-16 22:33
yushijinhun 发表于 2016-8-16 21:05
感谢您的反馈。您可不可以将其它启动器的启动参数发上来供我们对比?或者您也可以将.minecraft发到  来帮 ...

以及另一个问题 JMCCC不能指定PermSize
作者: yushijinhun    时间: 2016-8-17 00:10
officeyutong 发表于 2016-8-16 22:33
以及另一个问题 JMCCC不能指定PermSize

可以通过LaunchOption.extraJVMArguments()指定
作者: jinyigeng    时间: 2016-8-20 19:53
楼主你好,提几个问题,谢谢1.为什么下载Minecraft总是被取消?
下面是源代码
Main.java


UseDownload.java


Download.java



2.麻烦告诉一下在什么情况下下载取消

如neng

作者: yushijinhun    时间: 2016-8-20 21:18
jinyigeng 发表于 2016-8-20 19:53
楼主你好,提几个问题,谢谢1.为什么下载Minecraft总是被取消?
下面是源代码
Main.java

downloadIncrementally是异步操作,调用后立即返回。你调用downloadIncrementally()之后就去shutdown(),会导致任务还没下载完就把downloader关了。

正确做法是写callback里
作者: 1241892914    时间: 2016-9-22 19:41
启动原版可以启动 但是启动forge版本的话就一点反应也没有   
作者: 1241892914    时间: 2016-9-22 20:12
输入这个的时候
                                option.setExtraJvmArguments(Arrays.asList(
                                ExtraArgumentsTemplates.FML_IGNORE_INVALID_MINECRAFT_CERTIFICATES,
                                ExtraArgumentsTemplates.FML_IGNORE_PATCH_DISCREPANCISE));


setExtraJvmArguments会报错   提示是The method setExtraJvmArguments(Arrays.asList(ExtraArgumentsTemplates.FML_IGNORE_INVALID_MINECRAFT_CERTIFICATES, ExtraArgumentsTemplates.FML_IGNORE_PATCH_DISCREPANCISE)) is undefined for the type LaunchOption


怎么解决
作者: qlaall    时间: 2017-1-10 16:36
mark一下  不记得之前又没有找到过了

作者: zc2202    时间: 2017-1-12 13:59
我第二步就懵逼了完全不懂,能教一下吗?

作者: 122474363    时间: 2017-5-7 19:50
现在用JMCCC启动1.11.2没有声音
作者: 122474363    时间: 2017-5-7 20:43
发现了几个问题,
第一 是1.11.2启动后有时候有声音 有时候没有声音的问题。-无法解决
第二 是无法启动目录中带有中文的问题。-我已解决
作者: yushijinhun    时间: 2017-5-9 23:09
122474363 发表于 2017-5-7 20:43
发现了几个问题,
第一 是1.11.2启动后有时候有声音 有时候没有声音的问题。-无法解决
第二 是无法启动目录 ...

你确定启动minecraft的时候,assets都下载全了吗?我这边没出现过这样的问题
作者: 122474363    时间: 2017-5-10 20:43
yushijinhun 发表于 2017-5-9 23:09
你确定启动minecraft的时候,assets都下载全了吗?我这边没出现过这样的问题 ...

当然是下载完了的 亲哥。
作者: ーのものー    时间: 2017-8-10 14:10
如果可以的话,能否在『12. 使用自定义的 Yggdrasil API 提供商』章节中加上我这篇文章的链接呢?

『又是一种 Minecraft 外置登录解决方案:自行实现 Yggdrasil API』

https://blessing.studio/minecraf ... rty-implementation/
作者: yy1426489531    时间: 2017-8-11 18:09
6666顶顶顶
作者: Godpower    时间: 2017-8-13 11:21
提示: 作者被禁止或删除 内容自动屏蔽
作者: Viosin    时间: 2018-5-24 12:10
无法启动mc1.12.2......
作者: wrf_PC    时间: 2018-6-14 21:18
Viosin 发表于 2018-5-24 12:10
无法启动mc1.12.2......

同。。。
作者: 南柯郡守    时间: 2018-7-27 12:51
本帖最后由 969756790 于 2018-7-29 20:02 编辑

您好,我在编写过程中出现了如下问题

游戏启动后 会出现游戏窗口
但启动到一半就闪退

控制台输出如下
  1. [12:49:14] [Client thread/INFO]: Setting user: username
  2. [12:49:15] [Client thread/WARN]: Skipping bad option: lastServer:
  3. [12:49:15] [Client thread/INFO]: LWJGL Version: 2.9.4
  4. [12:49:18] [Client thread/INFO]: Reloading ResourceManager: Default
  5. [12:49:19] [Sound Library Loader/INFO]: Starting up SoundSystem...
  6. [12:49:19] [Thread-3/INFO]: Initializing LWJGL OpenAL
  7. [12:49:19] [Thread-3/INFO]: (The LWJGL binding of OpenAL.  For more information, see http://www.lwjgl.org)
  8. [12:49:19] [Thread-3/INFO]: OpenAL initialized.
  9. [12:49:19] [Sound Library Loader/INFO]: Sound engine started
  10. [12:49:25] [Client thread/INFO]: Created: 1024x512 textures-atlas
  11. ---- Minecraft Crash Report ----
  12. // Daisy, daisy...

  13. Time: 7/27/18 12:49 PM
  14. Description: Initializing game

  15. java.lang.NoClassDefFoundError: com/mojang/text2speech/Narrator
  16.         at biv.<init>(SourceFile:15)
  17.         at biv.<clinit>(SourceFile:14)
  18.         at biq.<init>(SourceFile:127)
  19.         at bib.aq(SourceFile:557)
  20.         at bib.a(SourceFile:404)
  21.         at net.minecraft.client.main.Main.main(SourceFile:123)
  22. Caused by: java.lang.ClassNotFoundException: com.mojang.text2speech.Narrator
  23.         at java.net.URLClassLoader.findClass(Unknown Source)
  24.         at java.lang.ClassLoader.loadClass(Unknown Source)
  25.         at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
  26.         at java.lang.ClassLoader.loadClass(Unknown Source)
  27.         ... 6 more


  28. A detailed walkthrough of the error, its code path and all known details is as follows:
  29. ---------------------------------------------------------------------------------------

  30. -- Head --
  31. Thread: Client thread
  32. Stacktrace:
  33.         at biv.<init>(SourceFile:15)
  34.         at biv.<clinit>(SourceFile:14)
  35.         at biq.<init>(SourceFile:127)
  36.         at bib.aq(SourceFile:557)

  37. -- Initialization --
  38. Details:
  39. Stacktrace:
  40.         at bib.a(SourceFile:404)
  41.         at net.minecraft.client.main.Main.main(SourceFile:123)

  42. -- System Details --
  43. Details:
  44.         Minecraft Version: 1.12.2
  45.         Operating System: Windows 10 (amd64) version 10.0
  46.         Java Version: 1.8.0_181, Oracle Corporation
  47.         Java VM Version: Java HotSpot(TM) 64-Bit Server VM (mixed mode), Oracle Corporation
  48.         Memory: 110166544 bytes (105 MB) / 395313152 bytes (377 MB) up to 954728448 bytes (910 MB)
  49.         JVM Flags: 1 total; -Xmx1024M
  50.         IntCache: cache: 0, tcache: 0, allocated: 0, tallocated: 0
  51.         Launched Version: 1.12.2
  52.         LWJGL: 2.9.4
  53.         OpenGL: GeForce 940MX/PCIe/SSE2 GL version 4.5.0 NVIDIA 382.05, NVIDIA Corporation
  54.         GL Caps: Using GL 1.3 multitexturing.
  55. Using GL 1.3 texture combiners.
  56. Using framebuffer objects because OpenGL 3.0 is supported and separate blending is supported.
  57. Shaders are available because OpenGL 2.1 is supported.
  58. VBOs are available because OpenGL 1.5 is supported.

  59.         Using VBOs: Yes
  60.         Is Modded: Probably not. Jar signature remains and client brand is untouched.
  61.         Type: Client (map_client.txt)
  62.         Resource Packs:
  63.         Current Language: 简体中文 (中国)
  64.         Profiler Position: N/A (disabled)
  65.         CPU: 4x Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
  66. #@!@# Game crashed! Crash report saved to: #@!@# C:\Users\<span style='display: inline !important; float: none; background-color: transparent; color: rgb(68, 68, 68); font-family: Tahoma,"Microsoft Yahei","Simsun"; font-size: 14px; font-style: normal; font-variant: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: left; text-decoration: none; text-indent: 0px; text-transform: none; -webkit-text-stroke-width: 0px; white-space: normal; word-spacing: 0px; word-wrap: break-word;'>{中文内容}</span>\Desktop\3D\.minecraft\crash-reports\crash-2018-07-27_12.49.27-client.txt
  67. AL lib: (EE) alc_cleanup: 1 device not closed
  68. 游戏进程退出,状态码:-1
复制代码
启动类源码如下
  1. <div>public class GameLauncher {

  2.     public static void main(String[] args) throws Exception {
  3.             MinecraftDirectory dir = new MinecraftDirectory("C:\\Users\\{中文内容}\\Desktop\\3D\\.minecraft");</div><div>            LaunchOption option = new LaunchOption("1.12.2", new OfflineAuthenticator("username"), dir); // (3)
  4.             Launcher launcher = LauncherBuilder.buildDefault();
  5.             option.setExtraJvmArguments(Arrays.asList(ExtraArgumentsTemplates.FML_IGNORE_INVALID_MINECRAFT_CERTIFICATES,ExtraArgumentsTemplates.FML_IGNORE_PATCH_DISCREPANCISE));
  6.             launcher.launch(option, new GameProcessListener() {

  7.             @Override
  8.             public void onLog(String log) {
  9.                 System.out.println(log); // 输出日志到控制台
  10.             }

  11.             @Override
  12.             public void onErrorLog(String log) {
  13.                 System.err.println(log); // 输出日志到控制台(同上)
  14.             }

  15.             @Override
  16.             public void onExit(int code) {
  17.                 System.err.println("游戏进程退出,状态码:" + code); // 游戏结束时输出状态码
  18.             }
  19.         });
  20. }</div>
复制代码
希望能给予解决方案,谢谢!




作者: @TGL    时间: 2019-2-21 11:54
本帖最后由 @TGL 于 2019-2-21 12:52 编辑

怎么启动forge,原版的可以启动成功
Exception in thread "main" java.lang.ClassCastException: java.base/jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to java.base/java.net.URLClassLoader
        at net.minecraft.launchwrapper.Launch.<init>(Launch.java:34)
        at net.minecraft.launchwrapper.Launch.main(Launch.java:28)
游戏进程退出,状态码:1


作者: ReinEchoes    时间: 2019-3-25 20:33
你好.我1.8.8版本能正常启动,1.12.2版本无法启动,也没有报错,请问这个情况怎么处理
作者: MinecraftJasmi    时间: 2019-4-6 13:31
MCBBS有你更精彩~
作者: tianxiaochengzi    时间: 2019-4-20 12:28
发生的fgsft
作者: tianxiaochengzi    时间: 2019-4-20 12:28
呃呃呃具体已于噢谷uykiuhkhkjhllklk
作者: ReinEchoes    时间: 2019-7-19 02:42
你好,JMCCC2.5beta版本添加自定义Yggdrasil API出现了问题,有一些类发生了改变,能不请教一下2.5版本如何添加Yggdrasil API
作者: sulinly    时间: 2019-8-2 01:49
非常有用 谢谢
作者: sulinly    时间: 2019-8-8 17:50
1.9能运行 1.13.2不能运行 咋办呢?
作者: gooding300    时间: 2019-8-9 01:17
sulinly 发表于 2019-8-8 17:50
1.9能运行 1.13.2不能运行 咋办呢?

https://github.com/to2mbn/JMCCC/issues/30
作者: sulinly    时间: 2019-8-9 11:29
gooding300 发表于 2019-8-9 01:17
https://github.com/to2mbn/JMCCC/issues/30

谢谢!这个好像没法直接gradle依赖?只有下源码?
作者: gooding300    时间: 2019-8-9 15:14
sulinly 发表于 2019-8-9 11:29
谢谢!这个好像没法直接gradle依赖?只有下源码?

那个url是maven化的,加一个repo就行
作者: 还房贷好    时间: 2019-8-9 21:06
我太懒啦!
作者: 翔梦FD    时间: 2019-8-30 06:48
请问在哪能得到tukaani xz,百度木有啊
作者: 翔梦FD    时间: 2019-8-30 10:47
这个大佬又是什么神仙,这个实在是太厉害了!
作者: sulinly    时间: 2020-1-3 14:02
没找到appendProvider方法呢?

**图片_20200103135732.png (45.72 KB, 下载次数: 1)

**图片_20200103135732.png

**图片_20200103140208.png (12.76 KB, 下载次数: 1)

**图片_20200103140208.png

作者: sulinly    时间: 2020-1-3 14:08
报错的,截图不对,重新截了个:

qqq.png (13.01 KB, 下载次数: 0)

qqq.png

作者: 翔梦FD    时间: 2020-1-11 10:18
能启动游戏,但是为什么启动完之后过一会游戏就自动退出了啊???
作者: 史丹安    时间: 2020-1-11 22:51
看起来很不错
作者: Mo_xiaoxi    时间: 2020-2-15 21:20
666感谢大佬分享
作者: Eerobrine_tan    时间: 2020-3-21 11:25
无法使用jmccc-yggdrasil-authenticator-2.5beta1与jmccc -2.5 -SNAPS配套,哪位大佬发一下配套的jmccc-yggdrasil-authenticator
作者: 忘了我是谁    时间: 2020-4-12 18:50
水回复***************
作者: 成品油    时间: 2020-5-3 19:48
不服不行!
作者: liangcha2009    时间: 2020-6-14 09:42
懒癌患者表示不愿意写代码
作者: 德芙DakFu    时间: 2020-8-13 20:03
学习ing,谢谢指教噢
作者: 德芙DakFu    时间: 2020-8-13 20:04
感觉真的会很难啊