JMCCC是由 @yushijinhun 和 @Darkyoooooo 开发的一个Java启动器核心,支持:
- 启动Minecraft
- 正版验证(以及Yggdrasil的其它API)
- 下载Minecraft
- 下载并安装Forge/Liteloader
并且JMCCC是开源的(MIT许可证),JMCCC在GitHub上的项目:https://github.com/to2mbn/JMCCC
本教程的适用人群:有一定经验的Java开发者
目录
- 环境搭建
- 启动游戏
- 正版登录
- 下载游戏
- 下载并安装Forge及Liteloader
- 获取正版玩家的皮肤
- OS X下Dock相关配置
- 忽略Forge的数字签名
- 使用HttpAsyncClient来进行下载
- 使用Ehcache缓存下载的文件
- 修改Minecraft1.9中的version_type
- 使用自定义的Yggdrasil API提供商
点击下面的页码来跳到下一页。
1. 环境搭建
JMCCC由三个模块组成,分别为jmccc、jmccc-yggdrasil-authenticator、jmccc-mcdownloader,它们分别提供了启动Minecraft、正版登录、下载Minecraft的功能。您可以按需引用这三个模块。
假如您使用Maven或者Gradle,您只需要加入如下的依赖即可(目前JMCCC最新版本为2.4):
- org.to2mbn:jmccc:2.4
- org.to2mbn:jmccc-yggdrasil-authenticator:2.4
- org.to2mbn:jmccc-mcdownloader:2.4
假如您不使用Maven/Gradle,您就需要手动导入jar包,上述jar可以在Maven中心仓库找到,并且还需要导入org.json和tukaani xz这两个依赖。
JMCCC要求的Java最低版本为Java7。
2. 启动游戏
首先,您需要通过LauncherBuilder类来创建一个Launcher对象:
- Launcher launcher = LauncherBuilder.buildDefault();
buildDefault()方法会创建一个默认配置的Launcher。当然您也可以通过方法链来自定义Launcher的配置,比如:
- Launcher launcher = LauncherBuilder.create()
- .setDebugPrintCommandline(true) // (1)
- .setNativeFastCheck(true) // (2)
- .build();
方法 | 意义 |
setDebugPrintCommandline(boolean) | (即上面的(1))设置是否在启动时将启动参数输出到控制台以供调试,默认为false。 |
setNativeFastCheck(boolean) | (即上面的(2))设置是否开启对Natives文件的快速检查,默认为false。 假如没有开启该选项,jmccc在启动时会对Natives文件进行全文比较来判断文件是否完整,如果发现Natives文件内容不一致,则将文件替换。 开启该选项后,jmccc仅会通过比较文件大小来判断文件是否完整,这样可以加快启动速度,但有可能造成某些问题。 |
接着,您需要创建一个LaunchOption来描述启动设置,比如:
- 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:
相关的使用可以跳读到第11节。 |
setRuntimeDirectory(MinecraftDirectory) | 设置Minecraft运行时使用的目录,默认和getMinecraftDirectory()一样(同上面(3)中构造方法的第三个参数)。这里指定的runtimeDirectory包含的是存档、资源包、截图等,而上面的minecraftDirectory包含的是游戏jar(versions)、库文件(libraries)、资源文件(assets)等。所以可以用这个方法来实现各版本独立。 |
然后您就可以通过调用launch(LaunchOption)方法启动游戏了:
- launcher.launch(option);
如果您想获取Minecraft进程的控制台日志,则可以使用launch(LaunchOption, GameProcessListener):
- launcher.launch(option, new GameProcessListener() {
- @Override
- public void onLog(String log) {
- System.out.println(log); // (4)
- }
- @Override
- public void onErrorLog(String log) {
- System.err.println(log); // (5)
- }
- @Override
- public void onExit(int code) {
- System.err.println("游戏进程退出,状态码:" + code); // (6)
- }
- });
上面代码中(4)处的onLog(String)方法会在游戏进程的标准输出输出日志时调用,(5)处的onErrorLog(String)方法会在游戏进程的标准错误输出日志时调用,而(6)处的onExit(int)会在游戏进程结束时调用。上面这段代码把游戏进程的日志都输出到了自己的控制台,并且在游戏结束时还会输出 "游戏进程退出,状态码:xxx" 这样的字符串。
下面给出一段演示代码:
- package yushijinhun.jmccc.test;
- import org.to2mbn.jmccc.auth.OfflineAuthenticator;
- import org.to2mbn.jmccc.exec.GameProcessListener;
- import org.to2mbn.jmccc.launch.Launcher;
- import org.to2mbn.jmccc.launch.LauncherBuilder;
- import org.to2mbn.jmccc.option.LaunchOption;
- import org.to2mbn.jmccc.option.MinecraftDirectory;
- public class JmcccTest {
- public static void main(String[] args) throws Exception {
- // 创建一个Launcher对象
- Launcher launcher = LauncherBuilder.create()
- .setDebugPrintCommandline(true) // 将启动命令打印到控制台以便调试
- .build();
- // 启动配置
- LaunchOption option = new LaunchOption(
- "1.9", // 游戏版本
- new OfflineAuthenticator("test_user"), // 使用离线验证,用户名test_user
- new MinecraftDirectory("/home/yushijinhun/.minecraft")); // .minecraft目录
- // 最大内存2048M
- option.setMaxMemory(2048);
- // 启动游戏
- launcher.launch(option, new GameProcessListener() {
- @Override
- public void onLog(String log) {
- System.out.println(log); // 输出日志到控制台
- }
- @Override
- public void onErrorLog(String log) {
- System.err.println(log); // 输出日志到控制台(同上)
- }
- @Override
- public void onExit(int code) {
- System.err.println("游戏进程退出,状态码:" + code); // 游戏结束时输出状态码
- }
- });
- }
- }
控制台输出:
- jmccc:
- /usr/lib/jvm/jdk1.8.0_66/jre/bin/java
- -Xmx2048M
- -Djava.library.path=/home/yushijinhun/.minecraft/versions/1.9/1.9-natives
- -cp
- /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:
- net.minecraft.client.main.Main
- --username
- test_user
- --version
- 1.9
- --gameDir
- /home/yushijinhun/.minecraft
- --assetsDir
- /home/yushijinhun/.minecraft/assets
- --assetIndex
- 1.9
- --uuid
- 109fb39758f23a9a92c15a21ac5113c2
- --accessToken
- 5b54b31a68574518a71fdd7e9846f87d
- --userType
- mojang
- --versionType
- release
- [18:01:52] [Client thread/INFO]: Setting user: test_user
- [18:01:52] [Client thread/INFO]: (Session ID is token:5b54b31a68574518a71fdd7e9846f87d:109fb39758f23a9a92c15a21ac5113c2)
- [18:01:54] [Client thread/INFO]: LWJGL Version: 2.9.4
- [18:01:54] [Client thread/INFO]: Reloading ResourceManager: Default
- [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:block.note.pling
- [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.bat.loop
- [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.cat.hiss
- [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.ghast.scream
- [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.player.breath
- [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.small_slime.jump
- [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.snowman.ambient
- [18:01:54] [Client thread/WARN]: Missing sound for event: minecraft:entity.wolf.howl
- [18:01:54] [Sound Library Loader/INFO]: Starting up SoundSystem...
- [18:01:55] [Thread-5/INFO]: Initializing LWJGL OpenAL
- [18:01:55] [Thread-5/INFO]: (The LWJGL binding of OpenAL. For more information, see http://www.lwjgl.org)
- [18:01:55] [Thread-5/INFO]: OpenAL initialized.
- [18:01:55] [Sound Library Loader/INFO]: Sound engine started
- [18:01:56] [Client thread/INFO]: Created: 1024x512 textures-atlas
- [18:02:02] [Realms Notification Availability checker #1/INFO]: Could not authorize you against Realms server: Invalid session id
- [18:02:03] [Client thread/INFO]: Stopping!
- [18:02:03] [Client thread/INFO]: SoundSystem shutting down...
- [18:02:03] [Client thread/WARN]: Author: Paul Lamb, www.paulscode.com
- 游戏进程退出,状态码: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节中的例子)
- LaunchOption option = new LaunchOption("1.9", YggdrasilAuthenticator.password("email@xxx.com", "password"), new MinecraftDirectory(".minecraft"));
需要注意的是YggdrasilAuthenticator有一个无参的构造方法。不同于上面的两个工厂方法,用这个构造方法创建出来的YggdrasilAuthenticator是不带有有效的session的。也就是说,new YggdrasilAuthenticator()创建出来的YggdrasilAuthenticator要刷新一次之后才能使用。
如何刷新YggdrasilAuthenticator中的session?
YggdrasilAuthenticator中session的刷新分为被动刷新和主动刷新。
被动刷新就像本节一开始所说的,当要使用YggdrasilAuthenticator进行验证,但当前的session又无效时,就需要向用户询问密码来重新登录,用户此时是被动的。
YggdrasilAuthenticator默认情况下是不允许被动刷新的(因为YggdrasilAuthenticator不知道如何与用户交互),此时您需要为YggdrasilAuthenticator编写子类来实现被动刷新,例如:
- package yushijinhun.jmccc.test;
- import java.util.Scanner;
- import org.to2mbn.jmccc.auth.AuthenticationException;
- import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;
- public class MyYggdrasilAuthenticator extends YggdrasilAuthenticator {
- // 下面两个构造方法并没有什么好看的
- // 从超类生成过来的罢了
- public MyYggdrasilAuthenticator() {
- super();
- }
- public MyYggdrasilAuthenticator(AuthenticationService sessionService) {
- super(sessionService);
- }
- @Override
- protected PasswordProvider tryPasswordLogin() throws AuthenticationException {
- // 这个方法会在进行被动刷新时调用
- // 向用户询问邮箱与密码
- Scanner scanner = new Scanner(System.in);
- System.out.print("邮箱:");
- String email = scanner.nextLine();
- System.out.print("密码:");
- String password = scanner.nextLine();
- return YggdrasilAuthenticator.createPasswordProvider(email, password, null);
- }
- }
我们可以来测试一下:
- package yushijinhun.jmccc.test;
- import org.to2mbn.jmccc.auth.AuthenticationException;
- import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;
- public class AuthTest {
- public static void main(String[] args) throws AuthenticationException {
- // 用无参构造函数创建的YggdrasilAuthenticator是不带有session的。
- // 所以在第一次使用YggdrasilAuthenticator时会触发一次被动刷新,
- // 要求用户输入邮箱和密码。
- YggdrasilAuthenticator authenticator = new MyYggdrasilAuthenticator();
- // 循环十次要求提供登录信息
- for (int i = 0; i < 10; i++)
- System.out.println(authenticator.auth());
- }
- }
控制台输出:
- 邮箱:********@qq.com
- 密码:********
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
// 因为是个人敏感信息,所以我就无耻的打码了
可以看到虽然调用了使用了十次YggdrasilAuthenticator(调用auth()),但只向用户询问了一次密码。这说明YggdrasilAuthenticator记住了之前的登录状态。
主动刷新是指,程序主动要求YggdrasilAuthenticator刷新session。可以通过调用下面两个方法实现:
方法 | 意义 |
refreshWithPassword(String, String) | 用邮箱和密码来刷新当前session |
refreshWithToken(String, String) | 用token来刷新当前session |
我们来测试一下:
- package yushijinhun.jmccc.test;
- import org.to2mbn.jmccc.auth.AuthenticationException;
- import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;
- public class AuthTest {
- public static void main(String[] args) {
- // 这样创建的YggdrasilAuthenticator不带有有效session
- // 并且我们也没有实现被动刷新
- YggdrasilAuthenticator authenticator = new YggdrasilAuthenticator();
- // 第一次使用YggdrasilAuthenticator
- // 由于没有有效session,并且也无法进行被动刷新
- // 所以会出错
- try {
- System.out.println(authenticator.auth());
- } catch (AuthenticationException e) {
- e.printStackTrace();
- }
- // 此时我们主动去刷新它
- try {
- authenticator.refreshWithPassword("email@xxx.com", "password");
- } catch (AuthenticationException e) {
- e.printStackTrace();
- }
- // 第二次使用YggdrasilAuthenticator
- // 由于经过主动刷新,已经有有效的session了
- // 所以成功执行
- try {
- System.out.println(authenticator.auth());
- } catch (AuthenticationException e) {
- e.printStackTrace();
- }
- }
- }
控制台输出:
- org.to2mbn.jmccc.auth.AuthenticationException: no more authentication methods to try
- at org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator.refresh(YggdrasilAuthenticator.java:293)
- at org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator.session(YggdrasilAuthenticator.java:265)
- at org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator.auth(YggdrasilAuthenticator.java:236)
- at yushijinhun.jmccc.test.AuthTest.main(AuthTest.java:17)
- AuthInfo [username=********, token=********, uuid=********, properties={}, userType=mojang]
有一点要注意:在编写和用户交互的启动器时,使用被动刷新。尽量不用YggdrasilAuthenticator.password()和token()这些工厂方法,也尽量避免主动刷新。因为出现被动刷新只有在真的有必要要用密码登录时才会发生。
如何保存登录信息?
一般情况下我们的启动器都会有个“记住密码”的功能。但难道启动器真的保存了密码吗?这显然是不安全的。事实上启动器保存的是上次的session,到下一次再打开启动器时,便会加载上次的session。
那么如何在jmccc中实现这个功能呢?答案有两个。
第一种办法是直接序列化YggdrasilAuthenticator。此时YggdrasilAuthenticator中所含的session将一同被序列化。示例如下:
- package yushijinhun.jmccc.test;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.ObjectInputStream;
- import java.io.ObjectOutputStream;
- import org.to2mbn.jmccc.auth.AuthenticationException;
- import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;
- public class AuthTest {
- public static void main(String[] args) throws Exception {
- // 找一个临时文件
- File file = File.createTempFile("jmccc-test", ".dat");
- saveAuth(file);
- loadAuth(file);
- }
- /**
- * 创建一个YggdrasilAuthenticator并把它序列化到文件里。
- *
- * @param file 要保存到的文件
- * @throws AuthenticationException 假如出现验证错误
- * @throws IOException 假如出现I/O异常
- */
- static void saveAuth(File file) throws AuthenticationException, IOException {
- // 用密码创建一个包含有效session的YggdrasilAuthenticator
- YggdrasilAuthenticator authenticator = YggdrasilAuthenticator.password("email@xxx.com", "password");
- try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file))) {
- // 序列化YggdrasilAuthenticator
- out.writeObject(authenticator);
- }
- System.out.printf("YggdrasilAuthenticator已保存到%s%n", file);
- }
- /**
- * 从文件里加载YggdrasilAuthenticator,并把它的session输出出来。
- *
- * @param file 要加载YggdrasilAuthenticator的文件
- * @throws AuthenticationException 假如出现验证错误
- * @throws IOException 假如出现I/O异常
- * @throws ClassNotFoundException 假如类未找到(反序列化错误)
- */
- static void loadAuth(File file) throws AuthenticationException, IOException, ClassNotFoundException {
- YggdrasilAuthenticator authenticator;
- try (ObjectInputStream in = new ObjectInputStream(new FileInputStream(file))) {
- // 反序列化YggdrasilAuthenticator
- authenticator = (YggdrasilAuthenticator) in.readObject();
- }
- System.out.printf("从%s中加载了一个YggdrasilAuthenticator%n", file);
- System.out.printf("调用YggdrasilAuthenticator的auth():%s%n", authenticator.auth());
- }
- }
控制台输出:
- YggdrasilAuthenticator已保存到/tmp/jmccc-test3142550737446373738.dat
- 从/tmp/jmccc-test3142550737446373738.dat中加载了一个YggdrasilAuthenticator
- 调用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即角色)
下面给出一个例子:
- package yushijinhun.jmccc.test;
- import java.util.Scanner;
- import org.to2mbn.jmccc.auth.yggdrasil.CharacterSelector;
- import org.to2mbn.jmccc.auth.yggdrasil.core.GameProfile;
- public class MyCharacterSelector implements CharacterSelector {
- @Override
- public GameProfile select(GameProfile[] availableProfiles) {
- // 与用户交互
- for (int i = 0; i < availableProfiles.length; i++) {
- System.out.printf("[%d] %s%n", i, availableProfiles[i].getName());
- }
- System.out.printf("请从上面的角色中选择一个(输入序号):");
- Scanner scanner = new Scanner(System.in);
- int index = scanner.nextInt();
- // 返回要使用的角色
- return availableProfiles[index];
- }
- }
上面我们知道了可以使用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节)
- package yushijinhun.jmccc.test;
- import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;
- import org.to2mbn.jmccc.auth.yggdrasil.core.AuthenticationService;
- import org.to2mbn.jmccc.auth.yggdrasil.core.yggdrasil.YggdrasilServiceBuilder;
- public class AuthTest {
- public static void main(String[] args) throws Exception {
- AuthenticationService authenticationService = YggdrasilServiceBuilder.create()
- .setAPIProvider(new yushijinhunYggdrasilAPIProvider())
- .loadSessionPublicKey("/home/yushijinhun/yushijinhun_yggdrasil_pubkey.der")
- .buildAuthenticationService();
- // 用特定的AuthenticationService创建一个AuthenticationService
- // 这样这个YggdrasilAuthenticator就会向我的Yggdrasil服务请求,而不是Mojang的
- YggdrasilAuthenticator authenticator = new YggdrasilAuthenticator(authenticationService);
- // 然后用密码登录
- authenticator.refreshWithPassword("yushijinhun@gmail.com", "123456", new MyCharacterSelector());
- // 输出登录信息
- System.out.println(authenticator.auth());
- }
- }
控制台输出:
- [0] test_player
- [1] yushijinhun
- 请从上面的角色中选择一个(输入序号):1
- AuthInfo [username=yushijinhun, token=9395d1cdc7cf40a9a9fcf728aabdd7e7, uuid=8faf6e06d9e147fa8f63b9a6c19c5d5b, properties={}, userType=mojang]
p.s. 因为这个yggdrasil服务端是我测试用的,在mojang服务器上并没有这个账号,所以不打码也无妨。
除了在主动刷新时进行角色选择,在被动刷新时也可以进行角色选择。只需对上面的MyYggdrasilAuthenticator做一下修改。
将tryPasswordLogin()中的return改为如下:
- return YggdrasilAuthenticator.createPasswordProvider(email, password, new MyCharacterSelector());
可以看到第三个参数发生了变化。原来是null,代表使用默认的角色选择器。而现在是使用我们所指定的角色选择器。
再编写代码测试一下:
- package yushijinhun.jmccc.test;
- import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;
- import org.to2mbn.jmccc.auth.yggdrasil.core.AuthenticationService;
- import org.to2mbn.jmccc.auth.yggdrasil.core.yggdrasil.YggdrasilServiceBuilder;
- public class AuthTest {
- public static void main(String[] args) throws Exception {
- AuthenticationService authenticationService = YggdrasilServiceBuilder.create()
- .setAPIProvider(new yushijinhunYggdrasilAPIProvider())
- .loadSessionPublicKey("/home/yushijinhun/yushijinhun_yggdrasil_pubkey.der")
- .buildAuthenticationService();
- // 用特定的AuthenticationService创建一个AuthenticationService
- // 这样这个YggdrasilAuthenticator就会向我的Yggdrasil服务请求,而不是Mojang的
- YggdrasilAuthenticator authenticator = new MyYggdrasilAuthenticator(authenticationService);
- // 输出登录信息
- System.out.println(authenticator.auth());
- }
- }
可以看到只是将上面例子里的refreshWithPassword删掉了,并且换成了MyYggdrasilAuthenticator(因为我们要处理被动刷新)。变主动刷新为被动刷新,不指定密码,让YggdrasilAuthenticator来询问我们密码。
控制台输出:
- 邮箱:yushijinhun@gmail.com
- 密码:123456
- [0] test_player
- [1] yushijinhun
- 请从上面的角色中选择一个(输入序号):1
- 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对象,可以通过以下方式:
- 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)
- package yushijinhun.jmccc.test;
- import org.to2mbn.jmccc.mcdownloader.MinecraftDownloader;
- import org.to2mbn.jmccc.mcdownloader.MinecraftDownloaderBuilder;
- import org.to2mbn.jmccc.mcdownloader.download.DownloadCallback;
- import org.to2mbn.jmccc.mcdownloader.download.DownloadTask;
- import org.to2mbn.jmccc.mcdownloader.download.concurrent.CallbackAdapter;
- import org.to2mbn.jmccc.option.MinecraftDirectory;
- import org.to2mbn.jmccc.version.Version;
- public class DownloadTest {
- public static void main(String[] args) {
- // 下载位置(要下载到的.minecraft目录)
- MinecraftDirectory dir = new MinecraftDirectory("/home/yushijinhun/.minecraft");
- // 创建MinecraftDownloader
- MinecraftDownloader downloader = MinecraftDownloaderBuilder.create().build();
- // 下载Minecraft1.9
- downloader.downloadIncrementally(dir, "1.9", new CallbackAdapter<Version>() {
- @Override
- public void done(Version result) {
- // 当完成时调用
- // 参数代表实际下载到的Minecraft版本
- System.out.printf("下载完成,下载到的Minecraft版本:%s%n", result);
- }
- @Override
- public void failed(Throwable e) {
- // 当失败时调用
- // 参数代表是由于哪个异常而失败的
- System.out.printf("下载失败%n");
- e.printStackTrace();
- }
- @Override
- public void cancelled() {
- // 当被取消时调用
- System.out.printf("下载取消%n");
- }
- @Override
- public <R> DownloadCallback<R> taskStart(DownloadTask<R> task) {
- // 当有一个下载任务被派生出来时调用
- // 在这里返回一个DownloadCallback就可以监听该下载任务的状态
- System.out.printf("开始下载:%s%n", task.getURI());
- return new CallbackAdapter<R>() {
- @Override
- public void done(R result) {
- // 当这个DownloadTask完成时调用
- System.out.printf("子任务完成:%s%n", task.getURI());
- }
- @Override
- public void failed(Throwable e) {
- // 当这个DownloadTask失败时调用
- System.out.printf("子任务失败:%s。原因:%s%n", task.getURI(), e);
- }
- @Override
- public void cancelled() {
- // 当这个DownloadTask被取消时调用
- System.out.printf("子任务取消:%s%n", task.getURI());
- }
- @Override
- public void retry(Throwable e, int current, int max) {
- // 当这个DownloadTask因出错而重试时调用
- // 重试不代表着失败
- // 也就是说,一个DownloadTask可以重试若干次,
- // 每次决定要进行一次重试时就会调用这个方法
- // 当最后一次重试失败,这个任务也将失败了,failed()才会被调用
- // 所以调用顺序就是这样:
- // retry()->retry()->...->failed()
- System.out.printf("子任务重试[%d/%d]:%s。原因:%s%n", current, max, task.getURI(), e);
- }
- };
- }
- });
- }
- }
当下载成功时控制台输出:
- 开始下载:https://launchermeta.mojang.com/mc/game/version_manifest.json
- 子任务完成:https://launchermeta.mojang.com/mc/game/version_manifest.json
- 开始下载:https://launchermeta.mojang.com/mc/game/6768033e216468247bd031a0a2d9876d79818f8f/1.9.json
- 子任务完成:https://launchermeta.mojang.com/mc/game/6768033e216468247bd031a0a2d9876d79818f8f/1.9.json
- 开始下载:https://launchermeta.mojang.com/mc-staging/assets/1.9/092c59b361816c7fa7f000587caa977c515b179c/1.9.json
- 开始下载:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar
- 开始下载:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar
- 子任务完成:https://launchermeta.mojang.com/mc-staging/assets/1.9/092c59b361816c7fa7f000587caa977c515b179c/1.9.json
- 开始下载:http://resources.download.minecraft.net/4b/4b90ff3a9b1486642bc0f15da0045d83a91df82e
- 子任务完成:http://resources.download.minecraft.net/4b/4b90ff3a9b1486642bc0f15da0045d83a91df82e
- 子任务完成:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar
- 下载完成,下载到的Minecraft版本:1.9
- 子任务完成:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar
当下载失败时(通过拔网线实现)控制台输出:
- 开始下载:https://launchermeta.mojang.com/mc/game/version_manifest.json
- 开始下载:https://launchermeta.mojang.com/mc/game/6768033e216468247bd031a0a2d9876d79818f8f/1.9.json
- 子任务完成:https://launchermeta.mojang.com/mc/game/version_manifest.json
- 子任务完成:https://launchermeta.mojang.com/mc/game/6768033e216468247bd031a0a2d9876d79818f8f/1.9.json
- 开始下载:https://launchermeta.mojang.com/mc-staging/assets/1.9/092c59b361816c7fa7f000587caa977c515b179c/1.9.json
- 开始下载:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar
- 开始下载:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar
- 子任务重试[1/3]:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar。原因:java.net.UnknownHostException: launcher.mojang.com: unknown error
- 子任务重试[2/3]:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar。原因:java.net.UnknownHostException: launcher.mojang.com
- 子任务失败:https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar。原因:java.net.UnknownHostException: launcher.mojang.com
- 子任务重试[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
- 子任务重试[2/3]:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar。原因:java.net.UnknownHostException: libraries.minecraft.net
- 子任务失败:https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar。原因:java.net.UnknownHostException: libraries.minecraft.net
- 子任务取消:https://launchermeta.mojang.com/mc-staging/assets/1.9/092c59b361816c7fa7f000587caa977c515b179c/1.9.json
- 下载失败
- java.net.UnknownHostException: launcher.mojang.com
- at java.net.InetAddress.getAllByName0(InetAddress.java:1280)
- ......// 此处省略
如果说要下载Minecraft版本列表,则需要调用fetchRemoteVersionList(CombinedDownloadCallback<RemoteVersionList>)方法。如下:
- downloader.fetchRemoteVersionList(new CallbackAdapter<RemoteVersionList>() {
- @Override
- public void done(RemoteVersionList result) {
- System.out.printf("版本列表下载完成:%s%n", result);
- }
-
- // ............省略其它方法
- });
控制台输出如下:
- 版本列表下载完成:[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为例:
- package org.to2mbn.jmccc.mcdownloader.wiki.provider;
- import org.to2mbn.jmccc.mcdownloader.provider.DefaultLayoutProvider;
- public class BmclApiProvider extends DefaultLayoutProvider {
- @Override
- protected String getLibraryBaseURL() {
- return "http://bmclapi2.bangbang93.com/libraries/";
- }
- @Override
- protected String getVersionBaseURL() {
- return "http://bmclapi2.bangbang93.com/versions/";
- }
- @Override
- protected String getAssetIndexBaseURL() {
- return "http://bmclapi2.bangbang93.com/indexes/";
- }
- @Override
- protected String getVersionListURL() {
- return "http://bmclapi2.bangbang93.com/mc/game/version_manifest.json";
- }
- @Override
- protected String getAssetBaseURL() {
- return "http://bmclapi2.bangbang93.com/assets/";
- }
- }
然后只要在创建MinecraftDownloader时,调用setBaseProvider即可:
- MinecraftDownloader downloader = MinecraftDownloaderBuilder.create()
- .setBaseProvider(new BmclApiProvider())
- .build();
最后,您必须手动关闭MinecraftDownloader,否则MinecraftDownloader占用的资源将不会释放(如缓存、线程等):
- 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的解析链中:
- ForgeDownloadProvider forgeProvider = new ForgeDownloadProvider();
- LiteloaderDownloadProvider liteloaderProvider = new LiteloaderDownloadProvider();
- MinecraftDownloader downloader = MinecraftDownloaderBuilder.create()
- .appendProvider(forgeProvider)
- .appendProvider(liteloaderProvider)
- .build();
注意:appendProvider(liteloaderProvider)需要在appendProvider(forgeProvider)之后调用,否则将不能下载同时Forge、Liteloader并存的版本。
这样,这个MinecraftDownloader就具备了下载Forge、Liteloader的能力。
要下载Forge或Liteloader,首先要获取它们的版本列表,如下:
- downloader.download(forgeProvider.forgeVersionList(), new CallbackAdapter<ForgeVersionList>() {...});
- 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后就可以正常下载了,比如:
- // 这里的forgeVersion即要下载的forge版本
- downloader.downloadIncrementally(dir, forgeVersion.getVersionName(), new CallbackAdapter<Version>() {......});
可以看到和上文下载官方的minecraft版本并没有什么区别,只是使用了ForgeVersion的getVersionName()来作为要下载的minecraft的版本号罢了。要下载LiteloaderVersion如法炮制即可。
如果说要下载一个Forge和Liteloader并存的minecraft该怎么办呢?
假如我已经挑选好了ForgeVersion和LiteloaderVersion(forge和liteloader对应的minecraft版本要一致,不然肯定不能启动),那么只要将downloadIncrementally中的版本号替换为:
- liteloaderVersion.customize(forgeVersion.getVersionName()).getVersionName()
至此,Forge、Liteloader相关内容就讲解完了。
6. 获取正版玩家的皮肤
从本节开始,我们就将开始介绍jmccc的高级用法。一般来说,听不懂是正常的。;(手动斜眼
第3节中,我们学到了YggdrasilAuthenticator。事实上,YggdrasilAuthenticator的底层是YggdrasilService,YggdrasilService提供了一组访问Yggdrasil服务的接口。而与YggdrasilService并行的就是ProfileService,提供了和游戏角色相关的接口。
我们可以通过下面的方法来创建一个ProfileService:
- ProfileService profileService = YggdrasilServiceBuilder.defaultProfileService();
ProfileService中有三个方法,它们分别是:
方法 | 意义 |
lookupUUIDByName(String) | 查询与玩家游戏中的名称对应的UUID。 |
getGameProfile(UUID) | 查询给定UUID的角色的信息。 |
getTextures(GameProfile) | 从给定的角色信息中获取皮肤(等)。 |
注意:上面这三个方法都是要访问网络的,并且会阻塞,所以千万别脑残在UI线程之类的里面调用。
下面给出一个例子:
- package yushijinhun.jmccc.test;
- import java.util.UUID;
- import org.to2mbn.jmccc.auth.AuthenticationException;
- import org.to2mbn.jmccc.auth.yggdrasil.core.GameProfile;
- import org.to2mbn.jmccc.auth.yggdrasil.core.PlayerTextures;
- import org.to2mbn.jmccc.auth.yggdrasil.core.ProfileService;
- import org.to2mbn.jmccc.auth.yggdrasil.core.yggdrasil.YggdrasilServiceBuilder;
- public class ProfileServiceTest {
- public static void main(String[] args) throws AuthenticationException {
- ProfileService profileService = YggdrasilServiceBuilder.defaultProfileService();
- // 查询ztcjohn的uuid
- UUID uuid = profileService.lookupUUIDByName("ztcjohn");
- System.out.println(uuid);
- // 根据uuid获取GameProfile
- // 注意啦!假如lookupUUIDByName没找到对应玩家就会返回null。我这里因为是演示所以偷懒不做判断,大家写的时候记得一定要判断一下
- GameProfile profile = profileService.getGameProfile(uuid);
- // 获取GameProfile的皮肤
- // 同上,假如getGameProfile没找到对应玩家就会返回null。我这里是偷懒,大家写的时候千万不要学
- PlayerTextures textures = profileService.getTextures(profile);
- System.out.println(textures);
- }
- }
控制台输出:
- 469aa369-6304-40d4-8b99-7e63199677ac
- 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,可以通过下面这个方法:
- public static boolean isAlex(PlayerTextures textures) {
- Texture skin = textures.getSkin();
- if (skin != null) {
- Map<String, String> metadata = skin.getMetadata();
- if (metadata != null) {
- return "slim".equals(metadata.get("model"));
- }
- }
- return false;
- }
7. OS X下Dock相关配置
官方启动器在OSX下时,启动Minecraft的时候会添加一些和Dock相关的参数,用来设置Minecraft在Dock中的呈现。(貌似是这样)
在jmccc中,您需要将ExtraArgumentsTemplates.OSX_DOCK_NAME和ExtraArgumentsTemplates.OSX_DOCK_ICON(MinecraftDirectory, Version)的返回值加进JVM参数中。在此之前,务必要检查当前系统是否为OSX,这两个参数只有在OSX下的JVM里才是有效的。
比如:
- MinecraftDirectory dir = new MinecraftDirectory(".minecraft");
- LaunchOption option = new LaunchOption("1.9", new OfflineAuthenticator("test_player"), dir);
- // 一定要先判断是否为OSX
- if (Platform.CURRENT == Platform.OSX) {
- option.setExtraJvmArguments(Arrays.asList(
- ExtraArgumentsTemplates.OSX_DOCK_NAME,
- ExtraArgumentsTemplates.OSX_DOCK_ICON(dir, option.getVersion())));
- }
8. 忽略Forge的数字签名
有些Forge版本要添加-Dfml.ignoreInvalidMinecraftCertificates=true和-Dfml.ignorePatchDiscrepancies=true这两项JVM参数才能正常启动。出现这种情况一般是往jar里塞了一些奇怪的东西,然后FML发现jar的数字签名损坏了。解决方法是往JVM参数列表里加上这两个参数(同前一节)。
当然,jmccc是不会让您手动打这两项参数的。这两个参数都已经在ExtraArgumentsTemplates里面预定义好了,只需引用即可。
- option.setExtraJvmArguments(Arrays.asList(
- ExtraArgumentsTemplates.FML_IGNORE_INVALID_MINECRAFT_CERTIFICATES,
- ExtraArgumentsTemplates.FML_IGNORE_PATCH_DISCREPANCISE));
9. 使用HttpAsyncClient来进行下载
下载Minecraft时一般会下载大量文件(上千个),如果逐一下载效率固然很低,所以jmccc是允许多个任务同时下载的。默认情况下,jmccc用的是jdk自带的BIO(阻塞式I/O),会给每一个链接打开一个线程。当链接数很大时,便会造成系统资源的严重浪费。解决方法是切换到NIO(非阻塞式I/O),这样仅用数个线程便可以处理上万个链接,将下载速度最大化。
如果说要在jmccc中使用NIO来下载,则需要添加Apache HttpAsyncClient这个依赖:
- org.apache.httpcomponents:httpasyncclient:4.1.1
jmccc在初始化时,如果发现classpath中存在HttpAsyncClient,则会自动使用HttpAsyncClient来下载。
在将HttpAsyncClient添加到classpath中后,您便可以将最大链接数调到任意大了:
- MinecraftDownloader downloader = MinecraftDownloaderBuilder.create()
- .setMaxConnections(4096)
- .build();
比如这样将MaxConnections调到了4096,那么最多可以同时下载4096个文件。
如果说您不想使用HttpAsyncClient,您可以调用MinecraftDownloaderBuilder的disableApacheHttpAsyncClient()来禁用对HttpAsyncClient的支持。这样jmccc就只会使用BIO来下载文件。
10. 使用Ehcache缓存下载的文件
jmccc有时候会多次下载同一个文件,这样就会造成不必要的网络I/O和等待时间。所以jmccc提供了对Ehcache的支持(Ehcache是最有名的一个轻量级Java缓存框架)。如果说要启用这个缓存功能的话首先要将Ehcache添加到依赖,需要注意的是jmccc使用的是Ehcache3,不是2。
- 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天:
- MinecraftDownloader downloader = MinecraftDownloaderBuilder.create()
- .setHeapCacheSize(0) // 关闭堆上缓存
- .setOffheapCacheSize(0) // 关闭离堆缓存
- .setDiskCacheSize(128) // 开启最大为128MB的磁盘缓存
- .setDiskCacheDir(new File("/tmp/jmccc-cache")) // 存储缓存的目录是/tmp/jmccc-cache
- .setCacheLiveTime(1, TimeUnit.DAYS)// 缓冲有效时间1天
- .build();
如果说您不想使用缓存,那您可以调用MinecraftDownloaderBuilder的disableEhcache()方法来禁用对Ehcache的支持。
11. 修改Minecraft1.9中的version_type
大家用HMCL启动Minecraft1.9的时候可能会发现,Minecraft主界面下面有这样的东西:
这是怎么实现的呢?
可以打开1.9.json看看,发现其中有这样一段:
- "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的值,如下:
- package yushijinhun.jmccc.test;
- import java.util.HashMap;
- import java.util.Map;
- import org.to2mbn.jmccc.auth.OfflineAuthenticator;
- import org.to2mbn.jmccc.launch.Launcher;
- import org.to2mbn.jmccc.launch.LauncherBuilder;
- import org.to2mbn.jmccc.option.LaunchOption;
- import org.to2mbn.jmccc.option.MinecraftDirectory;
- public class JmcccTest {
- public static void main(String[] args) throws Exception {
- Launcher launcher = LauncherBuilder.buildDefault();
- LaunchOption option = new LaunchOption("1.9", new OfflineAuthenticator("test_user"), new MinecraftDirectory("/home/yushijinhun/.minecraft"));
- // 重点:手动指定version_type
- Map<String, String> vars = new HashMap<>();
- vars.put("version_type", "JMCCC大法好!");
- option.setCommandlineVariables(vars);
- launcher.launch(option);
- }
- }
运行效果:
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编写的子类如下:
- package yushijinhun.jmccc.test;
- import java.util.UUID;
- import org.to2mbn.jmccc.auth.yggdrasil.core.yggdrasil.YggdrasilAPIProvider;
- import org.to2mbn.jmccc.util.UUIDUtils;
- public class yushijinhunYggdrasilAPIProvider implements YggdrasilAPIProvider {
- @Override
- public String authenticate() {
- return "http://localhost:8080/yggdrasil/authenticate";
- }
- @Override
- public String refresh() {
- return "http://localhost:8080/yggdrasil/refresh";
- }
- @Override
- public String validate() {
- return "http://localhost:8080/yggdrasil/validate";
- }
- @Override
- public String invalidate() {
- return "http://localhost:8080/yggdrasil/invalidate";
- }
- @Override
- public String signout() {
- return "http://localhost:8080/yggdrasil/signout";
- }
- @Override
- public String profile(UUID profileUUID) {
- return "http://localhost:8080/yggdrasil/profiles/minecraft/" + UUIDUtils.unsign(profileUUID);
- }
- @Override
- public String profileLookup() {
- return "http://localhost:8080/yggdrasil/profilerepo/minecraft";
- }
- }
然后您就可以用YggdrasilServiceBuilder来配置AuthenticationService和ProfileService了:
- YggdrasilServiceBuilder yggdrasilBuilder = YggdrasilServiceBuilder.create()
- .setAPIProvider(new yushijinhunYggdrasilAPIProvider())
- .loadSessionPublicKey("/home/yushijinhun/yushijinhun_yggdrasil_pubkey.der"); //(1)
- ProfileService profileService = yggdrasilBuilder.buildProfileService(); //(2)
- 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。
- package yushijinhun.jmccc.test;
- import java.io.IOException;
- import java.security.NoSuchAlgorithmException;
- import java.security.spec.InvalidKeySpecException;
- import org.to2mbn.jmccc.auth.yggdrasil.YggdrasilAuthenticator;
- import org.to2mbn.jmccc.auth.yggdrasil.core.AuthenticationService;
- import org.to2mbn.jmccc.auth.yggdrasil.core.yggdrasil.YggdrasilServiceBuilder;
- public class MyYggdrasilAuthenticator extends YggdrasilAuthenticator {
- /**
- * 创建一个我自定义的AuthenticationService。
- *
- * @return 我自定义的AuthenticationService
- */
- private static AuthenticationService createMyAuthenticationService() {
- try {
- return YggdrasilServiceBuilder.create()
- .setAPIProvider(new yushijinhunYggdrasilAPIProvider())
- .loadSessionPublicKey("/home/yushijinhun/yushijinhun_yggdrasil_pubkey.der")
- .buildAuthenticationService();
- } catch (NoSuchAlgorithmException | InvalidKeySpecException | IOException e) {
- throw new IllegalStateException("无法创建自定义的AuthenticationService!", e);
- }
- }
- public MyYggdrasilAuthenticator() {
- // 使用自定义的AuthenticationService
- super(createMyAuthenticationService());
- }
- @Override
- protected AuthenticationService createAuthenticationServiceForDeserialization() {
- // 在反序列化过程中重新创建我自定义的AuthenticationService
- return createMyAuthenticationService();
- }
- }
题外话:由于某些原因(比如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开发的人!