Make background transparent?
This commit is contained in:
144
HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java
Normal file
144
HMCL/src/main/java/org/jackhuang/hmcl/game/AccountHelper.java
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Scheduler;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.FXUtilsKt;
|
||||
import org.jackhuang.hmcl.util.NetworkUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.Proxy;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public final class AccountHelper {
|
||||
public static final AccountHelper INSTANCE = new AccountHelper();
|
||||
private AccountHelper() {}
|
||||
|
||||
public static final File SKIN_DIR = new File(Main.APPDATA, "skins");
|
||||
|
||||
public static void loadSkins() {
|
||||
loadSkins(Proxy.NO_PROXY);
|
||||
}
|
||||
|
||||
public static void loadSkins(Proxy proxy) {
|
||||
for (Account account : Settings.INSTANCE.getAccounts().values()) {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
new SkinLoadTask((YggdrasilAccount) account, proxy, false).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
public static Task loadSkinAsync(YggdrasilAccount account) {
|
||||
return loadSkinAsync(account, Settings.INSTANCE.getProxy());
|
||||
}
|
||||
|
||||
public static Task loadSkinAsync(YggdrasilAccount account, Proxy proxy) {
|
||||
return new SkinLoadTask(account, proxy, false);
|
||||
}
|
||||
|
||||
public static Task refreshSkinAsync(YggdrasilAccount account) {
|
||||
return refreshSkinAsync(account, Settings.INSTANCE.getProxy());
|
||||
}
|
||||
|
||||
public static Task refreshSkinAsync(YggdrasilAccount account, Proxy proxy) {
|
||||
return new SkinLoadTask(account, proxy, true);
|
||||
}
|
||||
|
||||
private static File getSkinFile(String name) {
|
||||
return new File(SKIN_DIR, name + ".png");
|
||||
}
|
||||
|
||||
public static Image getSkin(YggdrasilAccount account) {
|
||||
return getSkin(account, 1);
|
||||
}
|
||||
|
||||
public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
|
||||
if (account.getSelectedProfile() == null) return FXUtilsKt.DEFAULT_ICON;
|
||||
String name = account.getSelectedProfile().getName();
|
||||
if (name == null) return FXUtilsKt.DEFAULT_ICON;
|
||||
File file = getSkinFile(name);
|
||||
if (file.exists()) {
|
||||
Image original = new Image("file:" + file.getAbsolutePath());
|
||||
return new Image("file:" + file.getAbsolutePath(),
|
||||
original.getWidth() * scaleRatio,
|
||||
original.getHeight() * scaleRatio,
|
||||
false, false);
|
||||
}
|
||||
return FXUtilsKt.DEFAULT_ICON;
|
||||
}
|
||||
|
||||
public static Rectangle2D getViewport(double scaleRatio) {
|
||||
double size = 8.0 * scaleRatio;
|
||||
return new Rectangle2D(size, size, size, size);
|
||||
}
|
||||
|
||||
private static class SkinLoadTask extends Task {
|
||||
private final YggdrasilAccount account;
|
||||
private final Proxy proxy;
|
||||
private final boolean refresh;
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
public SkinLoadTask(YggdrasilAccount account, Proxy proxy) {
|
||||
this(account, proxy, false);
|
||||
}
|
||||
|
||||
public SkinLoadTask(YggdrasilAccount account, Proxy proxy, boolean refresh) {
|
||||
this.account = account;
|
||||
this.proxy = proxy;
|
||||
this.refresh = refresh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scheduler getScheduler() {
|
||||
return Schedulers.io();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (account.canLogIn() && (account.getSelectedProfile() == null || refresh))
|
||||
DialogController.INSTANCE.logIn(account);
|
||||
|
||||
GameProfile profile = account.getSelectedProfile();
|
||||
if (profile == null) return;
|
||||
String name = profile.getName();
|
||||
if (name == null) return;
|
||||
String url = "http://skins.minecraft.net/MinecraftSkins/" + name + ".png";
|
||||
File file = getSkinFile(name);
|
||||
if (!refresh && file.exists())
|
||||
return;
|
||||
dependencies.add(new FileDownloadTask(NetworkUtils.toURL(url), file, proxy));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options) {
|
||||
this(repository, versionId, authInfo, options, null);
|
||||
}
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) {
|
||||
this(repository, versionId, authInfo, options, listener, true);
|
||||
}
|
||||
|
||||
public HMCLGameLauncher(GameRepository repository, String versionId, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) {
|
||||
super(repository, versionId, authInfo, options, listener, daemon);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void appendJvmArgs(List<String> result) {
|
||||
super.appendJvmArgs(result);
|
||||
|
||||
result.add("-Dminecraft.launcher.version=" + Main.VERSION);
|
||||
result.add("-Dminecraft.launcher.brand=" + Main.NAME);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class HMCLGameRepository extends DefaultGameRepository {
|
||||
private final Profile profile;
|
||||
private final Map<String, VersionSetting> versionSettings = new HashMap<>();
|
||||
private final Set<String> beingModpackVersions = new HashSet<>();
|
||||
|
||||
public boolean checkedModpack = false, checkingModpack = false;
|
||||
|
||||
public HMCLGameRepository(Profile profile, File baseDirectory) {
|
||||
super(baseDirectory);
|
||||
this.profile = profile;
|
||||
}
|
||||
|
||||
private boolean useSelf(String version, String assetId) {
|
||||
VersionSetting vs = profile.getVersionSetting(version);
|
||||
return new File(getBaseDirectory(), "assets/indexes/" + assetId + ".json").exists() || vs.isNoCommon();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getAssetDirectory(String version, String assetId) {
|
||||
if (useSelf(version, assetId))
|
||||
return super.getAssetDirectory(version, assetId);
|
||||
else
|
||||
return new File(Settings.INSTANCE.getCommonPath(), "assets");
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getRunDirectory(String id) {
|
||||
if (beingModpackVersions.contains(id))
|
||||
return getVersionRoot(id);
|
||||
else {
|
||||
VersionSetting vs = profile.getVersionSetting(id);
|
||||
switch (vs.getGameDirType()) {
|
||||
case VERSION_FOLDER: return getVersionRoot(id);
|
||||
case ROOT_FOLDER: return super.getRunDirectory(id);
|
||||
case CUSTOM: return new File(vs.getGameDir());
|
||||
default: throw new Error();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getLibraryFile(Version version, Library lib) {
|
||||
VersionSetting vs = profile.getVersionSetting(version.getId());
|
||||
File self = super.getLibraryFile(version, lib);
|
||||
if (self.exists() || vs.isNoCommon())
|
||||
return self;
|
||||
else
|
||||
return new File(Settings.INSTANCE.getCommonPath(), "libraries/" + lib.getPath());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void refreshVersionsImpl() {
|
||||
versionSettings.clear();
|
||||
super.refreshVersionsImpl();
|
||||
versions.keySet().forEach(this::loadVersionSetting);
|
||||
|
||||
checkModpack();
|
||||
|
||||
try {
|
||||
File file = new File(getBaseDirectory(), "launcher_profiles.json");
|
||||
if (!file.exists() && !versions.isEmpty())
|
||||
FileUtils.writeText(file, PROFILE);
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshVersions() {
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this));
|
||||
Schedulers.newThread().schedule(() -> {
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
});
|
||||
}
|
||||
|
||||
public void changeDirectory(File newDirectory) {
|
||||
setBaseDirectory(newDirectory);
|
||||
refreshVersions();
|
||||
}
|
||||
|
||||
private void checkModpack() {
|
||||
|
||||
if (!checkingModpack) {
|
||||
checkingModpack = true;
|
||||
if (getVersionCount() == 0) {
|
||||
File modpack = new File("modpack.zip").getAbsoluteFile();
|
||||
if (modpack.exists()) {
|
||||
// TODO
|
||||
}
|
||||
/*
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (TaskWindow.factory().execute(ModpackManager.install(MainFrame.INSTANCE, modpack, this, null)))
|
||||
refreshVersions();
|
||||
checkedModpack = true;
|
||||
});
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File getVersionSettingFile(String id) {
|
||||
return new File(getVersionRoot(id), "hmclversion.cfg");
|
||||
}
|
||||
|
||||
private void loadVersionSetting(String id) {
|
||||
File file = getVersionSettingFile(id);
|
||||
if (file.exists())
|
||||
try {
|
||||
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
|
||||
initVersionSetting(id, versionSetting);
|
||||
} catch (Exception ex) {
|
||||
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
|
||||
}
|
||||
}
|
||||
|
||||
public VersionSetting createVersionSetting(String id) {
|
||||
if (!hasVersion(id))
|
||||
return null;
|
||||
if (versionSettings.containsKey(id))
|
||||
return versionSettings.get(id);
|
||||
else
|
||||
return initVersionSetting(id, new VersionSetting());
|
||||
}
|
||||
|
||||
private VersionSetting initVersionSetting(String id, VersionSetting vs) {
|
||||
vs.addPropertyChangedListener(a -> saveVersionSetting(id));
|
||||
versionSettings.put(id, vs);
|
||||
return vs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version setting for version id.
|
||||
*
|
||||
* @param id version id
|
||||
*
|
||||
* @return may return null if the id not exists
|
||||
*/
|
||||
public VersionSetting getVersionSetting(String id) {
|
||||
if (!versionSettings.containsKey(id))
|
||||
loadVersionSetting(id);
|
||||
return versionSettings.get(id);
|
||||
}
|
||||
|
||||
public File getVersionIcon(String id) {
|
||||
return new File(getVersionRoot(id), "icon.png");
|
||||
}
|
||||
|
||||
public void saveVersionSetting(String id) {
|
||||
if (!versionSettings.containsKey(id))
|
||||
return;
|
||||
Lang.invoke(() -> FileUtils.writeText(getVersionSettingFile(id), GSON.toJson(versionSettings.get(id))));
|
||||
}
|
||||
|
||||
public void markVersionAsModpack(String id) {
|
||||
beingModpackVersions.add(id);
|
||||
}
|
||||
|
||||
public void undoMark(String id) {
|
||||
beingModpackVersions.remove(id);
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting()
|
||||
.registerTypeAdapter(VersionSetting.class, VersionSetting.Serializer.INSTANCE)
|
||||
.create();
|
||||
|
||||
private static final String PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}";
|
||||
}
|
||||
253
HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java
Normal file
253
HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java
Normal file
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import javafx.application.Platform;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.mod.CurseCompletionTask;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.LaunchingStepsPane;
|
||||
import org.jackhuang.hmcl.ui.LogWindow;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public final class LauncherHelper {
|
||||
public static final LauncherHelper INSTANCE = new LauncherHelper();
|
||||
private LauncherHelper(){}
|
||||
|
||||
private final LaunchingStepsPane launchingStepsPane = new LaunchingStepsPane();
|
||||
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public void launch(String selectedVersion) {
|
||||
Profile profile = Settings.INSTANCE.getSelectedProfile();
|
||||
GameRepository repository = profile.getRepository();
|
||||
DefaultDependencyManager dependencyManager = profile.getDependency();
|
||||
Account account = Settings.INSTANCE.getSelectedAccount();
|
||||
if (account == null)
|
||||
throw new IllegalStateException("No account");
|
||||
|
||||
Version version = repository.getVersion(selectedVersion);
|
||||
VersionSetting setting = profile.getVersionSetting(selectedVersion);
|
||||
|
||||
Controllers.INSTANCE.dialog(launchingStepsPane);
|
||||
TaskExecutor executor = Task.of(v -> emitStatus(LoadingState.DEPENDENCIES), Schedulers.javafx())
|
||||
.then(dependencyManager.checkGameCompletionAsync(version))
|
||||
.then(Task.of(v -> emitStatus(LoadingState.MODS), Schedulers.javafx()))
|
||||
.then(new CurseCompletionTask(dependencyManager, selectedVersion))
|
||||
.then(Task.of(v -> emitStatus(LoadingState.LOGIN), Schedulers.javafx()))
|
||||
.then(Task.of(v -> {
|
||||
try {
|
||||
v.set("account", account.logIn(HMCLMultiCharacterSelector.INSTANCE, Settings.INSTANCE.getProxy()));
|
||||
} catch (AuthenticationException e) {
|
||||
v.set("account", DialogController.INSTANCE.logIn(account));
|
||||
JFXUtilities.runInFX(() -> Controllers.INSTANCE.dialog(launchingStepsPane));
|
||||
}
|
||||
}))
|
||||
.then(Task.of(v -> emitStatus(LoadingState.LAUNCHING), Schedulers.javafx()))
|
||||
.then(Task.of(v -> {
|
||||
v.set("launcher", new HMCLGameLauncher(
|
||||
repository, selectedVersion, v.get("account"), setting.toLaunchOptions(profile.getGameDir()), new HMCLProcessListener(v.get("account"), setting)
|
||||
));
|
||||
}))
|
||||
.then(v -> v.<DefaultLauncher>get("launcher").launchAsync())
|
||||
.then(Task.of(v -> {
|
||||
PROCESSES.add(v.get(DefaultLauncher.LAUNCH_ASYNC_ID));
|
||||
if (setting.getLauncherVisibility() == LauncherVisibility.CLOSE)
|
||||
Main.Companion.stop();
|
||||
}))
|
||||
.executor();
|
||||
|
||||
executor.setTaskListener(new TaskListener() {
|
||||
AtomicInteger finished = new AtomicInteger(0);
|
||||
@Override
|
||||
public void onFinished(Task task) {
|
||||
finished.incrementAndGet();
|
||||
Platform.runLater(() -> {
|
||||
launchingStepsPane.getPgsTasks().setProgress(1.0 * finished.get() / executor.getRunningTasks());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminate() {
|
||||
Platform.runLater(Controllers.INSTANCE::closeDialog);
|
||||
}
|
||||
});
|
||||
|
||||
executor.start();
|
||||
}
|
||||
|
||||
public static void stopManagedProcesses() {
|
||||
synchronized (PROCESSES) {
|
||||
while (!PROCESSES.isEmpty())
|
||||
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
|
||||
}
|
||||
}
|
||||
|
||||
public void emitStatus(LoadingState state) {
|
||||
if (state == LoadingState.DONE)
|
||||
Controllers.INSTANCE.closeDialog();
|
||||
|
||||
launchingStepsPane.getLblCurrentState().setText(state.toString());
|
||||
launchingStepsPane.getLblSteps().setText((state.ordinal() + 1) + " / " + LoadingState.values().length);
|
||||
}
|
||||
|
||||
private void checkExit(LauncherVisibility v) {
|
||||
switch (v) {
|
||||
case HIDE_AND_REOPEN:
|
||||
Platform.runLater(Controllers.INSTANCE.getStage()::show);
|
||||
break;
|
||||
case KEEP:
|
||||
// No operations here
|
||||
break;
|
||||
case CLOSE:
|
||||
throw new Error("Never get to here");
|
||||
case HIDE:
|
||||
Platform.runLater(() -> {
|
||||
// Shut down the platform when user closed log window.
|
||||
Platform.setImplicitExit(true);
|
||||
// If we use Main.stop(), log window will be halt immediately.
|
||||
Main.Companion.stopWithoutJavaFXPlatform();
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The managed process listener.
|
||||
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
|
||||
* Because every time we launched a game, we generates a new [HMCLProcessListener]
|
||||
*/
|
||||
class HMCLProcessListener implements ProcessListener {
|
||||
|
||||
private final VersionSetting setting;
|
||||
private final Map<String, String> forbiddenTokens;
|
||||
private final LauncherVisibility visibility;
|
||||
private ManagedProcess process;
|
||||
private boolean lwjgl;
|
||||
private LogWindow logWindow;
|
||||
private final LinkedList<Pair<String, Log4jLevel>> logs;
|
||||
|
||||
public HMCLProcessListener(AuthInfo authInfo, VersionSetting setting) {
|
||||
this.setting = setting;
|
||||
|
||||
if (authInfo == null)
|
||||
forbiddenTokens = Collections.emptyMap();
|
||||
else
|
||||
forbiddenTokens = Lang.mapOf(
|
||||
new Pair<>(authInfo.getAuthToken(), "<access token>"),
|
||||
new Pair<>(authInfo.getUserId(), "<uuid>"),
|
||||
new Pair<>(authInfo.getUsername(), "<player>")
|
||||
);
|
||||
|
||||
visibility = setting.getLauncherVisibility();
|
||||
logs = new LinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProcess(ManagedProcess process) {
|
||||
this.process = process;
|
||||
|
||||
if (setting.isShowLogs())
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
logWindow.show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLog(String log, Log4jLevel level) {
|
||||
String newLog = log;
|
||||
for (Map.Entry<String, String> entry : forbiddenTokens.entrySet())
|
||||
newLog = newLog.replace(entry.getKey(), entry.getValue());
|
||||
|
||||
if (level.lessOrEqual(Log4jLevel.ERROR))
|
||||
System.err.print(log);
|
||||
else
|
||||
System.out.print(log);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
logs.add(new Pair<>(log, level));
|
||||
if (logs.size() > Settings.INSTANCE.getLogLines())
|
||||
logs.removeFirst();
|
||||
if (logWindow != null)
|
||||
logWindow.logLine(log, level);
|
||||
});
|
||||
|
||||
if (!lwjgl && log.contains("LWJGL Version: ")) {
|
||||
lwjgl = true;
|
||||
switch (visibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
Platform.runLater(() -> {
|
||||
Controllers.INSTANCE.getStage().hide();
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
break;
|
||||
case CLOSE:
|
||||
throw new Error("Never come to here");
|
||||
case KEEP:
|
||||
// No operations here
|
||||
break;
|
||||
case HIDE:
|
||||
Platform.runLater(() -> {
|
||||
Controllers.INSTANCE.getStage().close();
|
||||
emitStatus(LoadingState.DONE);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExit(int exitCode, ExitType exitType) {
|
||||
if (exitType != ExitType.NORMAL && logWindow == null)
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
logWindow.show();
|
||||
logWindow.onDone.register(() -> {
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
});
|
||||
});
|
||||
|
||||
checkExit(visibility);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
26
HMCL/src/main/java/org/jackhuang/hmcl/game/LoadingState.java
Normal file
26
HMCL/src/main/java/org/jackhuang/hmcl/game/LoadingState.java
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
public enum LoadingState {
|
||||
DEPENDENCIES,
|
||||
MODS,
|
||||
LOGIN,
|
||||
LAUNCHING,
|
||||
DONE
|
||||
}
|
||||
184
HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java
Normal file
184
HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java
Normal file
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2013 huangyuhui
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonNull;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Optional;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.ImmediateObjectProperty;
|
||||
import org.jackhuang.hmcl.util.ImmediateStringProperty;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class Profile {
|
||||
|
||||
private final HMCLGameRepository repository;
|
||||
private final ModManager modManager;
|
||||
|
||||
private final ImmediateObjectProperty<File> gameDirProperty;
|
||||
|
||||
public ImmediateObjectProperty<File> getGameDirProperty() {
|
||||
return gameDirProperty;
|
||||
}
|
||||
|
||||
public File getGameDir() {
|
||||
return gameDirProperty.get();
|
||||
}
|
||||
|
||||
public void setGameDir(File gameDir) {
|
||||
gameDirProperty.set(gameDir);
|
||||
}
|
||||
|
||||
private final ImmediateObjectProperty<VersionSetting> globalProperty = new ImmediateObjectProperty<>(this, "global", new VersionSetting());
|
||||
|
||||
public ImmediateObjectProperty<VersionSetting> globalProperty() {
|
||||
return globalProperty;
|
||||
}
|
||||
|
||||
public VersionSetting getGlobal() {
|
||||
return globalProperty.get();
|
||||
}
|
||||
|
||||
public void setGlobal(VersionSetting global) {
|
||||
globalProperty.set(global);
|
||||
}
|
||||
|
||||
private final ImmediateStringProperty nameProperty;
|
||||
|
||||
public ImmediateStringProperty getNameProperty() {
|
||||
return nameProperty;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return nameProperty.get();
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
nameProperty.set(name);
|
||||
}
|
||||
|
||||
public Profile() {
|
||||
this("Default");
|
||||
}
|
||||
|
||||
public Profile(String name) {
|
||||
this(name, new File(".minecraft"));
|
||||
}
|
||||
|
||||
public Profile(String name, File initialGameDir) {
|
||||
nameProperty = new ImmediateStringProperty(this, "name", name);
|
||||
gameDirProperty = new ImmediateObjectProperty<>(this, "gameDir", initialGameDir);
|
||||
repository = new HMCLGameRepository(this, initialGameDir);
|
||||
modManager = new ModManager(repository);
|
||||
|
||||
gameDirProperty.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
|
||||
}
|
||||
|
||||
public HMCLGameRepository getRepository() {
|
||||
return repository;
|
||||
}
|
||||
|
||||
public ModManager getModManager() {
|
||||
return modManager;
|
||||
}
|
||||
|
||||
public DefaultDependencyManager getDependency() {
|
||||
return new DefaultDependencyManager(repository, Settings.INSTANCE.getDownloadProvider(), Settings.INSTANCE.getProxy());
|
||||
}
|
||||
|
||||
public VersionSetting getVersionSetting(String id) {
|
||||
VersionSetting vs = repository.getVersionSetting(id);
|
||||
if (vs == null || vs.isUsesGlobal()) {
|
||||
getGlobal().setGlobal(true); // always keep global.isGlobal = true
|
||||
return getGlobal();
|
||||
} else
|
||||
return vs;
|
||||
}
|
||||
|
||||
public boolean isVersionGlobal(String id) {
|
||||
VersionSetting vs = repository.getVersionSetting(id);
|
||||
return vs == null || vs.isUsesGlobal();
|
||||
}
|
||||
|
||||
public VersionSetting specializeVersionSetting(String id) {
|
||||
VersionSetting vs = repository.getVersionSetting(id);
|
||||
if (vs == null)
|
||||
vs = repository.createVersionSetting(id);
|
||||
if (vs == null)
|
||||
return null;
|
||||
vs.setUsesGlobal(false);
|
||||
return vs;
|
||||
}
|
||||
|
||||
public void globalizeVersionSetting(String id) {
|
||||
VersionSetting vs = repository.getVersionSetting(id);
|
||||
if (vs != null)
|
||||
vs.setUsesGlobal(true);
|
||||
}
|
||||
|
||||
public void addPropertyChangedListener(InvalidationListener listener) {
|
||||
nameProperty.addListener(listener);
|
||||
globalProperty.addListener(listener);
|
||||
gameDirProperty.addListener(listener);
|
||||
}
|
||||
|
||||
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (src == null)
|
||||
return JsonNull.INSTANCE;
|
||||
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("global", context.serialize(src.getGlobal()));
|
||||
jsonObject.addProperty("gameDir", src.getGameDir().getPath());
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == null || json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null;
|
||||
JsonObject obj = (JsonObject) json;
|
||||
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
|
||||
|
||||
Profile profile = new Profile("Default", new File(gameDir));
|
||||
profile.setGlobal(context.deserialize(obj.get("global"), VersionSetting.class));
|
||||
return profile;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2013 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Optional;
|
||||
|
||||
import com.google.gson.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import org.jackhuang.hmcl.Main;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class VersionSetting {
|
||||
|
||||
public transient String id;
|
||||
|
||||
private boolean global = false;
|
||||
|
||||
public boolean isGlobal() {
|
||||
return global;
|
||||
}
|
||||
|
||||
public void setGlobal(boolean global) {
|
||||
this.global = global;
|
||||
}
|
||||
|
||||
/**
|
||||
* HMCL Version Settings have been divided into 2 parts.
|
||||
* 1. Global settings.
|
||||
* 2. Version settings.
|
||||
* If a version claims that it uses global settings, its version setting will be disabled.
|
||||
*
|
||||
* Defaults false because if one version uses global first, custom version file will not be generated.
|
||||
*/
|
||||
private final ImmediateBooleanProperty usesGlobalProperty = new ImmediateBooleanProperty(this, "usesGlobal", false);
|
||||
|
||||
public ImmediateBooleanProperty usesGlobalProperty() {
|
||||
return usesGlobalProperty;
|
||||
}
|
||||
|
||||
public boolean isUsesGlobal() {
|
||||
return usesGlobalProperty.get();
|
||||
}
|
||||
|
||||
public void setUsesGlobal(boolean usesGlobal) {
|
||||
usesGlobalProperty.set(usesGlobal);
|
||||
}
|
||||
|
||||
// java
|
||||
|
||||
/**
|
||||
* Java version or null if user customizes java directory.
|
||||
*/
|
||||
private final ImmediateStringProperty javaProperty = new ImmediateStringProperty(this, "java", "");
|
||||
|
||||
public ImmediateStringProperty javaProperty() {
|
||||
return javaProperty;
|
||||
}
|
||||
|
||||
public String getJava() {
|
||||
return javaProperty.get();
|
||||
}
|
||||
|
||||
public void setJava(String java) {
|
||||
javaProperty.set(java);
|
||||
}
|
||||
|
||||
/**
|
||||
* User customized java directory or null if user uses system Java.
|
||||
*/
|
||||
private final ImmediateStringProperty javaDirProperty = new ImmediateStringProperty(this, "javaDir", "");
|
||||
|
||||
public ImmediateStringProperty javaDirProperty() {
|
||||
return javaDirProperty;
|
||||
}
|
||||
|
||||
public String getJavaDir() {
|
||||
return javaDirProperty.get();
|
||||
}
|
||||
|
||||
public void setJavaDir(String javaDir) {
|
||||
javaDirProperty.set(javaDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* The command to launch java, i.e. optirun.
|
||||
*/
|
||||
private final ImmediateStringProperty wrapperProperty = new ImmediateStringProperty(this, "wrapper", "");
|
||||
|
||||
public ImmediateStringProperty wrapperProperty() {
|
||||
return wrapperProperty;
|
||||
}
|
||||
|
||||
public String getWrapper() {
|
||||
return wrapperProperty.get();
|
||||
}
|
||||
|
||||
public void setWrapper(String wrapper) {
|
||||
wrapperProperty.set(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* The permanent generation size of JVM garbage collection.
|
||||
*/
|
||||
private final ImmediateStringProperty permSizeProperty = new ImmediateStringProperty(this, "permSize", "");
|
||||
|
||||
public ImmediateStringProperty permSizeProperty() {
|
||||
return permSizeProperty;
|
||||
}
|
||||
|
||||
public String getPermSize() {
|
||||
return permSizeProperty.get();
|
||||
}
|
||||
|
||||
public void setPermSize(String permSize) {
|
||||
permSizeProperty.set(permSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* The maximum memory that JVM can allocate for heap.
|
||||
*/
|
||||
private final ImmediateIntegerProperty maxMemoryProperty = new ImmediateIntegerProperty(this, "maxMemory", (int) OperatingSystem.SUGGESTED_MEMORY);
|
||||
|
||||
public ImmediateIntegerProperty maxMemoryProperty() {
|
||||
return maxMemoryProperty;
|
||||
}
|
||||
|
||||
public int getMaxMemory() {
|
||||
return maxMemoryProperty.get();
|
||||
}
|
||||
|
||||
public void setMaxMemory(int maxMemory) {
|
||||
maxMemoryProperty.set(maxMemory);
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimum memory that JVM can allocate for heap.
|
||||
*/
|
||||
private final ImmediateObjectProperty<Integer> minMemoryProperty = new ImmediateObjectProperty<>(this, "minMemory", null);
|
||||
|
||||
public ImmediateObjectProperty<Integer> minMemoryProperty() {
|
||||
return minMemoryProperty;
|
||||
}
|
||||
|
||||
public Integer getMinMemory() {
|
||||
return minMemoryProperty.get();
|
||||
}
|
||||
|
||||
public void setMinMemory(Integer minMemory) {
|
||||
minMemoryProperty.set(minMemory);
|
||||
}
|
||||
|
||||
/**
|
||||
* The command that will be executed before launching the Minecraft.
|
||||
* Operating system relevant.
|
||||
*/
|
||||
private final ImmediateStringProperty preLaunchCommandProperty = new ImmediateStringProperty(this, "precalledCommand", "");
|
||||
|
||||
public ImmediateStringProperty preLaunchCommandProperty() {
|
||||
return preLaunchCommandProperty;
|
||||
}
|
||||
|
||||
public String getPreLaunchCommand() {
|
||||
return preLaunchCommandProperty.get();
|
||||
}
|
||||
|
||||
public void setPreLaunchCommand(String preLaunchCommand) {
|
||||
preLaunchCommandProperty.set(preLaunchCommand);
|
||||
}
|
||||
|
||||
// options
|
||||
|
||||
/**
|
||||
* The user customized arguments passed to JVM.
|
||||
*/
|
||||
private final ImmediateStringProperty javaArgsProperty = new ImmediateStringProperty(this, "javaArgs", "");
|
||||
|
||||
public ImmediateStringProperty javaArgsProperty() {
|
||||
return javaArgsProperty;
|
||||
}
|
||||
|
||||
public String getJavaArgs() {
|
||||
return javaArgsProperty.get();
|
||||
}
|
||||
|
||||
public void setJavaArgs(String javaArgs) {
|
||||
javaArgsProperty.set(javaArgs);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The user customized arguments passed to Minecraft.
|
||||
*/
|
||||
private final ImmediateStringProperty minecraftArgsProperty = new ImmediateStringProperty(this, "minecraftArgs", "");
|
||||
|
||||
public ImmediateStringProperty minecraftArgsProperty() {
|
||||
return minecraftArgsProperty;
|
||||
}
|
||||
|
||||
public String getMinecraftArgs() {
|
||||
return minecraftArgsProperty.get();
|
||||
}
|
||||
|
||||
public void setMinecraftArgs(String minecraftArgs) {
|
||||
minecraftArgsProperty.set(minecraftArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if disallow HMCL use default JVM arguments.
|
||||
*/
|
||||
private final ImmediateBooleanProperty noJVMArgsProperty = new ImmediateBooleanProperty(this, "noJVMArgs", false);
|
||||
|
||||
public ImmediateBooleanProperty noJVMArgsProperty() {
|
||||
return noJVMArgsProperty;
|
||||
}
|
||||
|
||||
public boolean isNoJVMArgs() {
|
||||
return noJVMArgsProperty.get();
|
||||
}
|
||||
|
||||
public void setNoJVMArgs(boolean noJVMArgs) {
|
||||
noJVMArgsProperty.set(noJVMArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if HMCL does not check game's completeness.
|
||||
*/
|
||||
private final ImmediateBooleanProperty notCheckGameProperty = new ImmediateBooleanProperty(this, "notCheckGame", false);
|
||||
|
||||
public ImmediateBooleanProperty notCheckGameProperty() {
|
||||
return notCheckGameProperty;
|
||||
}
|
||||
|
||||
public boolean isNotCheckGame() {
|
||||
return notCheckGameProperty.get();
|
||||
}
|
||||
|
||||
public void setNotCheckGame(boolean notCheckGame) {
|
||||
notCheckGameProperty.set(notCheckGame);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* True if HMCL does not find/download libraries in/to common path.
|
||||
*/
|
||||
private final ImmediateBooleanProperty noCommonProperty = new ImmediateBooleanProperty(this, "noCommon", false);
|
||||
|
||||
public ImmediateBooleanProperty noCommonProperty() {
|
||||
return noCommonProperty;
|
||||
}
|
||||
|
||||
public boolean isNoCommon() {
|
||||
return noCommonProperty.get();
|
||||
}
|
||||
|
||||
public void setNoCommon(boolean noCommon) {
|
||||
noCommonProperty.set(noCommon);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if show the logs after game launched.
|
||||
*/
|
||||
private final ImmediateBooleanProperty showLogsProperty = new ImmediateBooleanProperty(this, "showLogs", false);
|
||||
|
||||
public ImmediateBooleanProperty showLogsProperty() {
|
||||
return showLogsProperty;
|
||||
}
|
||||
|
||||
public boolean isShowLogs() {
|
||||
return showLogsProperty.get();
|
||||
}
|
||||
|
||||
public void setShowLogs(boolean showLogs) {
|
||||
showLogsProperty.set(showLogs);
|
||||
}
|
||||
|
||||
// Minecraft settings.
|
||||
|
||||
/**
|
||||
* The server ip that will be entered after Minecraft successfully loaded immediately.
|
||||
*
|
||||
* Format: ip:port or without port.
|
||||
*/
|
||||
private final ImmediateStringProperty serverIpProperty = new ImmediateStringProperty(this, "serverIp", "");
|
||||
|
||||
public ImmediateStringProperty serverIpProperty() {
|
||||
return serverIpProperty;
|
||||
}
|
||||
|
||||
public String getServerIp() {
|
||||
return serverIpProperty.get();
|
||||
}
|
||||
|
||||
public void setServerIp(String serverIp) {
|
||||
serverIpProperty.set(serverIp);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* True if Minecraft started in fullscreen mode.
|
||||
*/
|
||||
private final ImmediateBooleanProperty fullscreenProperty = new ImmediateBooleanProperty(this, "fullscreen", false);
|
||||
|
||||
public ImmediateBooleanProperty fullscreenProperty() {
|
||||
return fullscreenProperty;
|
||||
}
|
||||
|
||||
public boolean isFullscreen() {
|
||||
return fullscreenProperty.get();
|
||||
}
|
||||
|
||||
public void setFullscreen(boolean fullscreen) {
|
||||
fullscreenProperty.set(fullscreen);
|
||||
}
|
||||
|
||||
/**
|
||||
* The width of Minecraft window, defaults 800.
|
||||
*
|
||||
* The field saves int value.
|
||||
* String type prevents unexpected value from causing JsonSyntaxException.
|
||||
* We can only reset this field instead of recreating the whole setting file.
|
||||
*/
|
||||
private final ImmediateIntegerProperty widthProperty = new ImmediateIntegerProperty(this, "width", 854);
|
||||
|
||||
public ImmediateIntegerProperty widthProperty() {
|
||||
return widthProperty;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return widthProperty.get();
|
||||
}
|
||||
|
||||
public void setWidth(int width) {
|
||||
widthProperty.set(width);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The height of Minecraft window, defaults 480.
|
||||
*
|
||||
* The field saves int value.
|
||||
* String type prevents unexpected value from causing JsonSyntaxException.
|
||||
* We can only reset this field instead of recreating the whole setting file.
|
||||
*/
|
||||
private final ImmediateIntegerProperty heightProperty = new ImmediateIntegerProperty(this, "height", 480);
|
||||
|
||||
public ImmediateIntegerProperty heightProperty() {
|
||||
return heightProperty;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return heightProperty.get();
|
||||
}
|
||||
|
||||
public void setHeight(int height) {
|
||||
heightProperty.set(height);
|
||||
}
|
||||
|
||||
/**
|
||||
* 0 - .minecraft<br/>
|
||||
* 1 - .minecraft/versions/<version>/<br/>
|
||||
*/
|
||||
private final ImmediateObjectProperty<EnumGameDirectory> gameDirTypeProperty = new ImmediateObjectProperty<>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER);
|
||||
|
||||
public ImmediateObjectProperty<EnumGameDirectory> gameDirTypeProperty() {
|
||||
return gameDirTypeProperty;
|
||||
}
|
||||
|
||||
public EnumGameDirectory getGameDirType() {
|
||||
return gameDirTypeProperty.get();
|
||||
}
|
||||
|
||||
public void setGameDirType(EnumGameDirectory gameDirType) {
|
||||
gameDirTypeProperty.set(gameDirType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Your custom gameDir
|
||||
*/
|
||||
private final ImmediateStringProperty gameDirProperty = new ImmediateStringProperty(this, "gameDir", "");
|
||||
|
||||
public ImmediateStringProperty gameDirProperty() {
|
||||
return gameDirProperty;
|
||||
}
|
||||
|
||||
public String getGameDir() {
|
||||
return gameDirProperty.get();
|
||||
}
|
||||
|
||||
public void setGameDir(String gameDir) {
|
||||
gameDirProperty.set(gameDir);
|
||||
}
|
||||
|
||||
// launcher settings
|
||||
|
||||
/**
|
||||
* 0 - Close the launcher when the game starts.<br/>
|
||||
* 1 - Hide the launcher when the game starts.<br/>
|
||||
* 2 - Keep the launcher open.<br/>
|
||||
*/
|
||||
private final ImmediateObjectProperty<LauncherVisibility> launcherVisibilityProperty = new ImmediateObjectProperty<>(this, "launcherVisibility", LauncherVisibility.HIDE);
|
||||
|
||||
public ImmediateObjectProperty<LauncherVisibility> launcherVisibilityProperty() {
|
||||
return launcherVisibilityProperty;
|
||||
}
|
||||
|
||||
public LauncherVisibility getLauncherVisibility() {
|
||||
return launcherVisibilityProperty.get();
|
||||
}
|
||||
|
||||
public void setLauncherVisibility(LauncherVisibility launcherVisibility) {
|
||||
launcherVisibilityProperty.set(launcherVisibility);
|
||||
}
|
||||
|
||||
public JavaVersion getJavaVersion() throws InterruptedException {
|
||||
// TODO: lazy initialization may result in UI suspension.
|
||||
if (StringUtils.isBlank(getJava()))
|
||||
setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom");
|
||||
if ("Default".equals(getJava())) return JavaVersion.fromCurrentEnvironment();
|
||||
else if ("Custom".equals(getJava())) {
|
||||
try {
|
||||
return JavaVersion.fromExecutable(new File(getJavaDir()));
|
||||
} catch (IOException e) {
|
||||
return null; // Custom Java Directory not found,
|
||||
}
|
||||
} else if (StringUtils.isNotBlank(getJava())) {
|
||||
JavaVersion c = JavaVersion.getJREs().get(getJava());
|
||||
if (c == null) {
|
||||
setJava("Default");
|
||||
return JavaVersion.fromCurrentEnvironment();
|
||||
} else
|
||||
return c;
|
||||
} else throw new Error();
|
||||
}
|
||||
|
||||
public void addPropertyChangedListener(InvalidationListener listener) {
|
||||
usesGlobalProperty.addListener(listener);
|
||||
javaProperty.addListener(listener);
|
||||
javaDirProperty.addListener(listener);
|
||||
wrapperProperty.addListener(listener);
|
||||
permSizeProperty.addListener(listener);
|
||||
maxMemoryProperty.addListener(listener);
|
||||
minMemoryProperty.addListener(listener);
|
||||
preLaunchCommandProperty.addListener(listener);
|
||||
javaArgsProperty.addListener(listener);
|
||||
minecraftArgsProperty.addListener(listener);
|
||||
noJVMArgsProperty.addListener(listener);
|
||||
notCheckGameProperty.addListener(listener);
|
||||
noCommonProperty.addListener(listener);
|
||||
showLogsProperty.addListener(listener);
|
||||
serverIpProperty.addListener(listener);
|
||||
fullscreenProperty.addListener(listener);
|
||||
widthProperty.addListener(listener);
|
||||
heightProperty.addListener(listener);
|
||||
gameDirTypeProperty.addListener(listener);
|
||||
gameDirProperty.addListener(listener);
|
||||
launcherVisibilityProperty.addListener(listener);
|
||||
}
|
||||
|
||||
public LaunchOptions toLaunchOptions(File gameDir) throws InterruptedException {
|
||||
JavaVersion javaVersion = Optional.ofNullable(getJavaVersion()).orElse(JavaVersion.fromCurrentEnvironment());
|
||||
return new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion)
|
||||
.setVersionName(Main.TITLE)
|
||||
.setProfileName(Main.TITLE)
|
||||
.setMinecraftArgs(getMinecraftArgs())
|
||||
.setJavaArgs(getJavaArgs())
|
||||
.setMaxMemory(getMaxMemory())
|
||||
.setMinMemory(getMinMemory())
|
||||
.setMetaspace(StringUtils.parseInt(getPermSize()))
|
||||
.setWidth(getWidth())
|
||||
.setHeight(getHeight())
|
||||
.setFullscreen(isFullscreen())
|
||||
.setServerIp(getServerIp())
|
||||
.setWrapper(getWrapper())
|
||||
.setProxyHost(Settings.INSTANCE.getProxyHost())
|
||||
.setProxyPort(Settings.INSTANCE.getProxyPort())
|
||||
.setProxyUser(Settings.INSTANCE.getProxyUser())
|
||||
.setProxyPass(Settings.INSTANCE.getProxyPass())
|
||||
.setPrecalledCommand(getPreLaunchCommand())
|
||||
.setNoGeneratedJVMArgs(isNoJVMArgs())
|
||||
.create();
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (src == null) return JsonNull.INSTANCE;
|
||||
JsonObject obj = new JsonObject();
|
||||
|
||||
obj.addProperty("usesGlobal", src.isUsesGlobal());
|
||||
obj.addProperty("javaArgs", src.getJavaArgs());
|
||||
obj.addProperty("minecraftArgs", src.getMinecraftArgs());
|
||||
obj.addProperty("maxMemory", src.getMaxMemory() <= 0 ? OperatingSystem.SUGGESTED_MEMORY : src.getMaxMemory());
|
||||
obj.addProperty("minMemory", src.getMinMemory());
|
||||
obj.addProperty("permSize", src.getPermSize());
|
||||
obj.addProperty("width", src.getWidth());
|
||||
obj.addProperty("height", src.getHeight());
|
||||
obj.addProperty("javaDir", src.getJavaDir());
|
||||
obj.addProperty("precalledCommand", src.getPreLaunchCommand());
|
||||
obj.addProperty("serverIp", src.getServerIp());
|
||||
obj.addProperty("java", src.getJava());
|
||||
obj.addProperty("wrapper", src.getWrapper());
|
||||
obj.addProperty("fullscreen", src.isFullscreen());
|
||||
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
|
||||
obj.addProperty("notCheckGame", src.isNotCheckGame());
|
||||
obj.addProperty("noCommon", src.isNoCommon());
|
||||
obj.addProperty("showLogs", src.isShowLogs());
|
||||
obj.addProperty("gameDir", src.getGameDir());
|
||||
obj.addProperty("launcherVisibility", src.getLauncherVisibility().ordinal());
|
||||
obj.addProperty("gameDirType", src.getGameDirType().ordinal());
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == null || json == JsonNull.INSTANCE || !(json instanceof JsonObject))
|
||||
return null;
|
||||
JsonObject obj = (JsonObject) json;
|
||||
|
||||
int maxMemoryN = parseJsonPrimitive(Optional.ofNullable(obj.get("maxMemory")).map(JsonElement::getAsJsonPrimitive).orElse(null), OperatingSystem.SUGGESTED_MEMORY);
|
||||
if (maxMemoryN <= 0) maxMemoryN = OperatingSystem.SUGGESTED_MEMORY;
|
||||
|
||||
VersionSetting vs = new VersionSetting();
|
||||
|
||||
vs.setUsesGlobal(Optional.ofNullable(obj.get("usesGlobal")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setJavaArgs(Optional.ofNullable(obj.get("javaArgs")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setMinecraftArgs(Optional.ofNullable(obj.get("minecraftArgs")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setMaxMemory(maxMemoryN);
|
||||
vs.setMinMemory(Optional.ofNullable(obj.get("minMemory")).map(JsonElement::getAsInt).orElse(null));
|
||||
vs.setPermSize(Optional.ofNullable(obj.get("permSize")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setWidth(Optional.ofNullable(obj.get("width")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
|
||||
vs.setHeight(Optional.ofNullable(obj.get("height")).map(JsonElement::getAsJsonPrimitive).map(this::parseJsonPrimitive).orElse(0));
|
||||
vs.setJavaDir(Optional.ofNullable(obj.get("javaDir")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setPreLaunchCommand(Optional.ofNullable(obj.get("precalledCommand")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setServerIp(Optional.ofNullable(obj.get("serverIp")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setJava(Optional.ofNullable(obj.get("java")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setWrapper(Optional.ofNullable(obj.get("wrapper")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setGameDir(Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNoCommon(Optional.ofNullable(obj.get("noCommon")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(1)]);
|
||||
vs.setGameDirType(EnumGameDirectory.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(0)]);
|
||||
|
||||
return vs;
|
||||
}
|
||||
|
||||
private int parseJsonPrimitive(JsonPrimitive primitive) {
|
||||
return parseJsonPrimitive(primitive, 0);
|
||||
}
|
||||
|
||||
private int parseJsonPrimitive(JsonPrimitive primitive, int defaultValue) {
|
||||
if (primitive.isNumber())
|
||||
return primitive.getAsInt();
|
||||
else
|
||||
return Lang.parseInt(primitive.getAsString(), defaultValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
235
HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java
Normal file
235
HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java
Normal file
@@ -0,0 +1,235 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2013 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerWrapper;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebView;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.MainKt;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Settings;
|
||||
import org.jackhuang.hmcl.util.IOUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class LogWindow extends Stage {
|
||||
|
||||
private final ReadOnlyIntegerWrapper fatalProperty = new ReadOnlyIntegerWrapper(0);
|
||||
private final ReadOnlyIntegerWrapper errorProperty = new ReadOnlyIntegerWrapper(0);
|
||||
private final ReadOnlyIntegerWrapper warnProperty = new ReadOnlyIntegerWrapper(0);
|
||||
private final ReadOnlyIntegerWrapper infoProperty = new ReadOnlyIntegerWrapper(0);
|
||||
private final ReadOnlyIntegerWrapper debugProperty = new ReadOnlyIntegerWrapper(0);
|
||||
private final LogWindowImpl impl = new LogWindowImpl();
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
public final EventManager<Event> onDone = new EventManager<>();
|
||||
|
||||
public LogWindow() {
|
||||
setScene(new Scene(impl, 800, 480));
|
||||
getScene().getStylesheets().addAll(FXUtilsKt.getStylesheets());
|
||||
setTitle(MainKt.i18n("logwindow.title"));
|
||||
getIcons().add(new Image("/assets/img/icon.png"));
|
||||
}
|
||||
|
||||
public LogWindow(String text) {
|
||||
this();
|
||||
|
||||
onDone.register(() -> {
|
||||
logLine(text, Log4jLevel.INFO);
|
||||
});
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty fatalProperty() {
|
||||
return fatalProperty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public int getFatal() {
|
||||
return fatalProperty.get();
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty errorProperty() {
|
||||
return errorProperty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public int getError() {
|
||||
return errorProperty.get();
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty warnProperty() {
|
||||
return warnProperty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public int getWarn() {
|
||||
return warnProperty.get();
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty infoProperty() {
|
||||
return infoProperty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public int getInfo() {
|
||||
return infoProperty.get();
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty debugProperty() {
|
||||
return debugProperty.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public int getDebug() {
|
||||
return debugProperty.get();
|
||||
}
|
||||
|
||||
public void logLine(String line, Log4jLevel level) {
|
||||
Element div = impl.engine.getDocument().createElement("div");
|
||||
// a <pre> element to prevent multiple spaces and tabs being removed.
|
||||
Element pre = impl.engine.getDocument().createElement("pre");
|
||||
pre.setTextContent(line);
|
||||
div.appendChild(pre);
|
||||
impl.body.appendChild(div);
|
||||
impl.engine.executeScript("checkNewLog(\"" + level.name().toLowerCase() + "\");scrollToBottom();");
|
||||
|
||||
switch (level) {
|
||||
case FATAL:
|
||||
fatalProperty.set(fatalProperty.get() + 1);
|
||||
break;
|
||||
case ERROR:
|
||||
errorProperty.set(errorProperty.get() + 1);
|
||||
break;
|
||||
case WARN:
|
||||
warnProperty.set(warnProperty.get() + 1);
|
||||
break;
|
||||
case INFO:
|
||||
infoProperty.set(infoProperty.get() + 1);
|
||||
break;
|
||||
case DEBUG:
|
||||
debugProperty.set(debugProperty.get() + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public class LogWindowImpl extends StackPane {
|
||||
|
||||
@FXML
|
||||
public WebView webView;
|
||||
@FXML
|
||||
public ToggleButton btnFatals;
|
||||
@FXML
|
||||
public ToggleButton btnErrors;
|
||||
@FXML
|
||||
public ToggleButton btnWarns;
|
||||
@FXML
|
||||
public ToggleButton btnInfos;
|
||||
@FXML
|
||||
public ToggleButton btnDebugs;
|
||||
@FXML
|
||||
public ComboBox<String> cboLines;
|
||||
|
||||
WebEngine engine;
|
||||
Node body;
|
||||
Document document;
|
||||
|
||||
public LogWindowImpl() {
|
||||
FXUtilsKt.loadFXML(this, "/assets/fxml/log.fxml");
|
||||
|
||||
engine = webView.getEngine();
|
||||
engine.loadContent(Lang.ignoringException(() -> IOUtils.readFullyAsString(getClass().getResourceAsStream("/assets/log-window-content.html")))
|
||||
.replace("${FONT}", Settings.INSTANCE.getFont().getSize() + "px \"" + Settings.INSTANCE.getFont().getFamily() + "\""));
|
||||
engine.getLoadWorker().stateProperty().addListener((a, b, newValue) -> {
|
||||
if (newValue == Worker.State.SUCCEEDED) {
|
||||
document = engine.getDocument();
|
||||
body = document.getElementsByTagName("body").item(0);
|
||||
engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
|
||||
latch.countDown();
|
||||
onDone.fireEvent(new Event(LogWindow.this));
|
||||
}
|
||||
});
|
||||
|
||||
boolean flag = false;
|
||||
for (String i : cboLines.getItems())
|
||||
if (Integer.toString(Settings.INSTANCE.getLogLines()).equals(i)) {
|
||||
cboLines.getSelectionModel().select(i);
|
||||
flag = true;
|
||||
}
|
||||
|
||||
cboLines.getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> {
|
||||
Settings.INSTANCE.setLogLines(newValue == null ? 100 : Integer.parseInt(newValue));
|
||||
engine.executeScript("limitedLogs=" + Settings.INSTANCE.getLogLines());
|
||||
});
|
||||
|
||||
if (!flag)
|
||||
cboLines.getSelectionModel().select(0);
|
||||
|
||||
btnFatals.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(fatalProperty.get()) + " fatals", fatalProperty));
|
||||
btnErrors.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(errorProperty.get()) + " errors", errorProperty));
|
||||
btnWarns.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(warnProperty.get()) + " warns", warnProperty));
|
||||
btnInfos.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(infoProperty.get()) + " infos", infoProperty));
|
||||
btnDebugs.textProperty().bind(Bindings.createStringBinding(() -> Integer.toString(debugProperty.get()) + " debugs", debugProperty));
|
||||
|
||||
btnFatals.selectedProperty().addListener(o -> specificChanged());
|
||||
btnErrors.selectedProperty().addListener(o -> specificChanged());
|
||||
btnWarns.selectedProperty().addListener(o -> specificChanged());
|
||||
btnInfos.selectedProperty().addListener(o -> specificChanged());
|
||||
btnDebugs.selectedProperty().addListener(o -> specificChanged());
|
||||
}
|
||||
|
||||
private void specificChanged() {
|
||||
String res = "";
|
||||
if (btnFatals.isSelected())
|
||||
res += "\"fatal\", ";
|
||||
if (btnErrors.isSelected())
|
||||
res += "\"error\", ";
|
||||
if (btnWarns.isSelected())
|
||||
res += "\"warn\", ";
|
||||
if (btnInfos.isSelected())
|
||||
res += "\"info\", ";
|
||||
if (btnDebugs.isSelected())
|
||||
res += "\"debug\", ";
|
||||
if (StringUtils.isNotBlank(res))
|
||||
res = StringUtils.substringBeforeLast(res, ", ");
|
||||
engine.executeScript("specific([" + res + "])");
|
||||
}
|
||||
|
||||
public void onTerminateGame() {
|
||||
LauncherHelper.stopManagedProcesses();
|
||||
}
|
||||
|
||||
public void onClear() {
|
||||
engine.executeScript("clear()");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl
|
||||
|
||||
import org.jackhuang.hmcl.event.Event
|
||||
import org.jackhuang.hmcl.setting.Profile
|
||||
import java.util.*
|
||||
|
||||
@@ -30,7 +31,7 @@ import java.util.*
|
||||
* *
|
||||
* @author huangyuhui
|
||||
*/
|
||||
class ProfileChangedEvent(source: Any, val value: Profile) : EventObject(source)
|
||||
class ProfileChangedEvent(source: Any, val value: Profile) : Event(source)
|
||||
|
||||
/**
|
||||
* This event gets fired when loading profiles.
|
||||
@@ -40,4 +41,4 @@ class ProfileChangedEvent(source: Any, val value: Profile) : EventObject(source)
|
||||
* *
|
||||
* @author huangyuhui
|
||||
*/
|
||||
class ProfileLoadingEvent(source: Any) : EventObject(source)
|
||||
class ProfileLoadingEvent(source: Any) : Event(source)
|
||||
|
||||
@@ -57,10 +57,10 @@ class Main : Application() {
|
||||
|
||||
companion object {
|
||||
|
||||
val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
|
||||
val NAME = "HMCL"
|
||||
val TITLE = "$NAME $VERSION"
|
||||
val APPDATA = getWorkingDirectory("hmcl")
|
||||
@JvmField val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
|
||||
@JvmField val NAME = "HMCL"
|
||||
@JvmField val TITLE = "$NAME $VERSION"
|
||||
@JvmField val APPDATA = getWorkingDirectory("hmcl")
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game
|
||||
|
||||
import javafx.geometry.Rectangle2D
|
||||
import javafx.scene.image.Image
|
||||
import org.jackhuang.hmcl.Main
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
|
||||
import org.jackhuang.hmcl.setting.Settings
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask
|
||||
import org.jackhuang.hmcl.task.Scheduler
|
||||
import org.jackhuang.hmcl.task.Schedulers
|
||||
import org.jackhuang.hmcl.task.Task
|
||||
import org.jackhuang.hmcl.ui.DEFAULT_ICON
|
||||
import org.jackhuang.hmcl.ui.DialogController
|
||||
import org.jackhuang.hmcl.util.toURL
|
||||
import java.net.Proxy
|
||||
|
||||
object AccountHelper {
|
||||
val SKIN_DIR = Main.APPDATA.resolve("skins")
|
||||
|
||||
fun loadSkins(proxy: Proxy = Settings.proxy) {
|
||||
for (account in Settings.getAccounts().values) {
|
||||
if (account is YggdrasilAccount) {
|
||||
SkinLoadTask(account, proxy, false).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSkinAsync(account: YggdrasilAccount, proxy: Proxy = Settings.proxy): Task =
|
||||
SkinLoadTask(account, proxy, false)
|
||||
|
||||
fun refreshSkinAsync(account: YggdrasilAccount, proxy: Proxy = Settings.proxy): Task =
|
||||
SkinLoadTask(account, proxy, true)
|
||||
|
||||
private class SkinLoadTask(val account: YggdrasilAccount, val proxy: Proxy, val refresh: Boolean = false): Task() {
|
||||
|
||||
override fun getScheduler() = Schedulers.io()
|
||||
private val dependencies = mutableListOf<Task>()
|
||||
override fun getDependencies() = dependencies
|
||||
|
||||
override fun execute() {
|
||||
if (account.canLogIn() && (account.selectedProfile == null || refresh))
|
||||
DialogController.logIn(account)
|
||||
val profile = account.selectedProfile ?: return
|
||||
val name = profile.name ?: return
|
||||
val url = "http://skins.minecraft.net/MinecraftSkins/$name.png"
|
||||
val file = getSkinFile(name)
|
||||
if (!refresh && file.exists())
|
||||
return
|
||||
dependencies += FileDownloadTask(url.toURL(), file, proxy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSkinFile(name: String) = SKIN_DIR.resolve("$name.png")
|
||||
|
||||
fun getSkin(account: YggdrasilAccount, scaleRatio: Double = 1.0): Image {
|
||||
if (account.selectedProfile == null) return DEFAULT_ICON
|
||||
val name = account.selectedProfile?.name ?: return DEFAULT_ICON
|
||||
val file = getSkinFile(name)
|
||||
if (file.exists()) {
|
||||
val original = Image("file:" + file.absolutePath)
|
||||
return Image("file:" + file.absolutePath, original.width * scaleRatio, original.height * scaleRatio, false, false)
|
||||
}
|
||||
else return DEFAULT_ICON
|
||||
}
|
||||
|
||||
fun getViewport(scaleRatio: Double): Rectangle2D {
|
||||
val size = 8.0 * scaleRatio
|
||||
return Rectangle2D(size, size, size, size)
|
||||
}
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game
|
||||
|
||||
import com.google.gson.GsonBuilder
|
||||
import javafx.beans.InvalidationListener
|
||||
import org.jackhuang.hmcl.setting.EnumGameDirectory
|
||||
import org.jackhuang.hmcl.setting.Profile
|
||||
import org.jackhuang.hmcl.setting.Settings
|
||||
import org.jackhuang.hmcl.setting.VersionSetting
|
||||
import org.jackhuang.hmcl.task.Schedulers
|
||||
import org.jackhuang.hmcl.util.Logging.LOG
|
||||
import org.jackhuang.hmcl.util.fromJson
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.logging.Level
|
||||
|
||||
class HMCLGameRepository(val profile: Profile, baseDirectory: File)
|
||||
: DefaultGameRepository(baseDirectory) {
|
||||
|
||||
private val versionSettings = mutableMapOf<String, VersionSetting>()
|
||||
private val beingModpackVersions = mutableSetOf<String>()
|
||||
|
||||
private fun useSelf(version: String, assetId: String): Boolean {
|
||||
val vs = profile.getVersionSetting(version)
|
||||
return File(baseDirectory, "assets/indexes/$assetId.json").exists() || vs.noCommon
|
||||
}
|
||||
|
||||
override fun getAssetDirectory(version: String, assetId: String): File {
|
||||
if (useSelf(version, assetId))
|
||||
return super.getAssetDirectory(version, assetId)
|
||||
else
|
||||
return File(Settings.commonPath).resolve("assets")
|
||||
}
|
||||
|
||||
override fun getRunDirectory(id: String): File {
|
||||
if (beingModpackVersions.contains(id))
|
||||
return getVersionRoot(id)
|
||||
else {
|
||||
val vs = profile.getVersionSetting(id)
|
||||
return when (vs.gameDirType) {
|
||||
EnumGameDirectory.VERSION_FOLDER -> getVersionRoot(id)
|
||||
EnumGameDirectory.ROOT_FOLDER -> super.getRunDirectory(id)
|
||||
EnumGameDirectory.CUSTOM -> File(vs.gameDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLibraryFile(version: Version, lib: Library): File {
|
||||
val vs = profile.getVersionSetting(version.id)
|
||||
val self = super.getLibraryFile(version, lib);
|
||||
if (self.exists() || vs.noCommon)
|
||||
return self;
|
||||
else
|
||||
return File(Settings.commonPath).resolve("libraries/${lib.path}")
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun refreshVersionsImpl() {
|
||||
Schedulers.newThread().schedule {
|
||||
versionSettings.clear()
|
||||
|
||||
super.refreshVersionsImpl()
|
||||
|
||||
versions.keys.forEach(this::loadVersionSetting)
|
||||
|
||||
checkModpack()
|
||||
|
||||
try {
|
||||
val file = baseDirectory.resolve("launcher_profiles.json")
|
||||
if (!file.exists() && versions.isNotEmpty())
|
||||
file.writeText(PROFILE)
|
||||
} catch (ex: IOException) {
|
||||
LOG.log(Level.WARNING, "Unable to create launcher_profiles.json, Forge/LiteLoader installer will not work.", ex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun changeDirectory(newDir: File) {
|
||||
baseDirectory = newDir
|
||||
refreshVersions()
|
||||
}
|
||||
|
||||
private fun checkModpack() {}
|
||||
|
||||
private fun getVersionSettingFile(id: String) = getVersionRoot(id).resolve("hmclversion.cfg")
|
||||
|
||||
private fun loadVersionSetting(id: String) {
|
||||
val file = getVersionSettingFile(id)
|
||||
if (file.exists()) {
|
||||
try {
|
||||
val versionSetting = GSON.fromJson<VersionSetting>(file.readText())!!
|
||||
initVersionSetting(id, versionSetting)
|
||||
} catch (ignore: Exception) {
|
||||
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveVersionSetting(id: String) {
|
||||
if (!versionSettings.containsKey(id))
|
||||
return
|
||||
|
||||
getVersionSettingFile(id).writeText(GSON.toJson(versionSettings[id]))
|
||||
}
|
||||
|
||||
private fun initVersionSetting(id: String, vs: VersionSetting): VersionSetting {
|
||||
vs.addPropertyChangedListener(InvalidationListener { saveVersionSetting(id) })
|
||||
versionSettings[id] = vs
|
||||
return vs
|
||||
}
|
||||
|
||||
internal fun createVersionSetting(id: String): VersionSetting? {
|
||||
if (!hasVersion(id)) return null
|
||||
return versionSettings[id] ?: initVersionSetting(id, VersionSetting())
|
||||
}
|
||||
|
||||
fun getVersionSetting(id: String): VersionSetting? {
|
||||
if (!versionSettings.containsKey(id))
|
||||
loadVersionSetting(id)
|
||||
return versionSettings[id]
|
||||
}
|
||||
|
||||
fun getVersionIcon(id: String): File =
|
||||
getVersionRoot(id).resolve("icon.png")
|
||||
|
||||
fun markVersionAsModpack(id: String) {
|
||||
beingModpackVersions += id
|
||||
}
|
||||
|
||||
fun undoMark(id: String) {
|
||||
beingModpackVersions -= id
|
||||
}
|
||||
|
||||
companion object {
|
||||
val PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}"
|
||||
val GSON = GsonBuilder().registerTypeAdapter(VersionSetting::class.java, VersionSetting).setPrettyPrinting().create()
|
||||
}
|
||||
}
|
||||
@@ -17,18 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game
|
||||
|
||||
import org.jackhuang.hmcl.Main
|
||||
import org.jackhuang.hmcl.auth.AuthInfo
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher
|
||||
import org.jackhuang.hmcl.launch.ProcessListener
|
||||
import org.jackhuang.hmcl.auth.Account
|
||||
import org.jackhuang.hmcl.auth.MultiCharacterSelector
|
||||
import org.jackhuang.hmcl.auth.NoSelectedCharacterException
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile
|
||||
|
||||
class HMCLGameLauncher(repository: GameRepository, versionId: String, account: AuthInfo, options: LaunchOptions, listener: ProcessListener? = null, isDaemon: Boolean = true)
|
||||
: DefaultLauncher(repository, versionId, account, options, listener, isDaemon) {
|
||||
|
||||
override fun appendJvmArgs(res: MutableList<String>) {
|
||||
super.appendJvmArgs(res)
|
||||
|
||||
res.add("-Dminecraft.launcher.version=" + Main.VERSION);
|
||||
res.add("-Dminecraft.launcher.brand=" + Main.NAME);
|
||||
object HMCLMultiCharacterSelector : MultiCharacterSelector {
|
||||
override fun select(account: Account, names: MutableList<GameProfile>): GameProfile {
|
||||
return names.firstOrNull() ?: throw NoSelectedCharacterException(account)
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game
|
||||
|
||||
import javafx.application.Platform
|
||||
import org.jackhuang.hmcl.Main
|
||||
import org.jackhuang.hmcl.auth.AuthInfo
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException
|
||||
import org.jackhuang.hmcl.launch.DefaultLauncher
|
||||
import org.jackhuang.hmcl.launch.ProcessListener
|
||||
import org.jackhuang.hmcl.mod.CurseCompletionTask
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility
|
||||
import org.jackhuang.hmcl.setting.Settings
|
||||
import org.jackhuang.hmcl.setting.VersionSetting
|
||||
import org.jackhuang.hmcl.task.*
|
||||
import org.jackhuang.hmcl.ui.*
|
||||
import org.jackhuang.hmcl.util.Log4jLevel
|
||||
import org.jackhuang.hmcl.util.ManagedProcess
|
||||
import org.jackhuang.hmcl.util.task
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
object LauncherHelper {
|
||||
private val launchingStepsPane = LaunchingStepsPane()
|
||||
val PROCESS = ConcurrentLinkedQueue<ManagedProcess>()
|
||||
|
||||
fun launch() {
|
||||
val profile = Settings.selectedProfile
|
||||
val selectedVersion = profile.selectedVersion ?: throw IllegalStateException("No version here")
|
||||
val repository = profile.repository
|
||||
val dependency = profile.dependency
|
||||
val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
|
||||
val version = repository.getVersion(selectedVersion)
|
||||
val setting = profile.getVersionSetting(selectedVersion)
|
||||
|
||||
Controllers.dialog(launchingStepsPane)
|
||||
task(Schedulers.javafx()) { emitStatus(LoadingState.DEPENDENCIES) }
|
||||
.then(dependency.checkGameCompletionAsync(version))
|
||||
|
||||
.then(task(Schedulers.javafx()) { emitStatus(LoadingState.MODS) })
|
||||
.then(CurseCompletionTask(dependency, selectedVersion))
|
||||
|
||||
.then(task(Schedulers.javafx()) { emitStatus(LoadingState.LOGIN) })
|
||||
.then(task {
|
||||
try {
|
||||
it["account"] = account.logIn(Settings.proxy)
|
||||
} catch (e: AuthenticationException) {
|
||||
it["account"] = DialogController.logIn(account)
|
||||
runOnUiThread { Controllers.dialog(launchingStepsPane) }
|
||||
}
|
||||
})
|
||||
|
||||
.then(task(Schedulers.javafx()) { emitStatus(LoadingState.LAUNCHING) })
|
||||
.then(task {
|
||||
it["launcher"] = HMCLGameLauncher(
|
||||
repository = repository,
|
||||
versionId = selectedVersion,
|
||||
options = setting.toLaunchOptions(profile.gameDir),
|
||||
listener = HMCLProcessListener(it["account"], setting),
|
||||
account = it["account"]
|
||||
)
|
||||
})
|
||||
.then { it.get<DefaultLauncher>("launcher").launchAsync() }
|
||||
.then(task {
|
||||
PROCESS.add(it[DefaultLauncher.LAUNCH_ASYNC_ID])
|
||||
if (setting.launcherVisibility == LauncherVisibility.CLOSE)
|
||||
Main.stop()
|
||||
})
|
||||
|
||||
.executor()
|
||||
.apply {
|
||||
taskListener = object : TaskListener() {
|
||||
var finished = 0
|
||||
|
||||
override fun onFinished(task: Task) {
|
||||
++finished
|
||||
runOnUiThread { launchingStepsPane.pgsTasks.progress = 1.0 * finished / runningTasks }
|
||||
}
|
||||
|
||||
override fun onTerminate() {
|
||||
runOnUiThread(Controllers::closeDialog)
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun emitStatus(state: LoadingState) {
|
||||
if (state == LoadingState.DONE) {
|
||||
Controllers.closeDialog()
|
||||
}
|
||||
|
||||
launchingStepsPane.lblCurrentState.text = state.toString()
|
||||
launchingStepsPane.lblSteps.text = "${state.ordinal + 1} / ${LoadingState.values().size}"
|
||||
}
|
||||
|
||||
/**
|
||||
* The managed process listener.
|
||||
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
|
||||
* Because every time we launched a game, we generates a new [HMCLProcessListener]
|
||||
*/
|
||||
class HMCLProcessListener(authInfo: AuthInfo?, private val setting: VersionSetting) : ProcessListener {
|
||||
val forbiddenTokens: List<Pair<String, String>> = if (authInfo == null) emptyList() else
|
||||
listOf(
|
||||
authInfo.authToken to "<access token>",
|
||||
authInfo.userId to "<uuid>",
|
||||
authInfo.username to "<player>"
|
||||
)
|
||||
private val launcherVisibility = setting.launcherVisibility
|
||||
private lateinit var process: ManagedProcess
|
||||
private var lwjgl = false
|
||||
private var logWindow: LogWindow? = null
|
||||
private val logs = LinkedList<Pair<String, Log4jLevel>>()
|
||||
override fun setProcess(process: ManagedProcess) {
|
||||
this.process = process
|
||||
|
||||
if (setting.showLogs) {
|
||||
runOnUiThread { logWindow = LogWindow(); logWindow?.show() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLog(log: String, level: Log4jLevel) {
|
||||
var newLog = log
|
||||
for ((original, replacement) in forbiddenTokens)
|
||||
newLog = newLog.replace(original, replacement)
|
||||
|
||||
if (level.lessOrEqual(Log4jLevel.ERROR))
|
||||
System.err.print(log)
|
||||
else
|
||||
System.out.print(log)
|
||||
|
||||
runOnUiThread {
|
||||
logs += log to level
|
||||
if (logs.size > Settings.logLines)
|
||||
logs.removeFirst()
|
||||
logWindow?.logLine(log, level)
|
||||
}
|
||||
|
||||
if (!lwjgl && log.contains("LWJGL Version: ")) {
|
||||
lwjgl = true
|
||||
when (launcherVisibility) {
|
||||
LauncherVisibility.HIDE_AND_REOPEN -> {
|
||||
runOnUiThread {
|
||||
Controllers.stage.hide()
|
||||
emitStatus(LoadingState.DONE)
|
||||
}
|
||||
}
|
||||
LauncherVisibility.CLOSE -> {
|
||||
throw Error("Never come to here")
|
||||
}
|
||||
LauncherVisibility.KEEP -> {
|
||||
// No operations here.
|
||||
}
|
||||
LauncherVisibility.HIDE -> {
|
||||
runOnUiThread {
|
||||
Controllers.stage.close()
|
||||
emitStatus(LoadingState.DONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onExit(exitCode: Int, exitType: ProcessListener.ExitType) {
|
||||
if (exitType != ProcessListener.ExitType.NORMAL && logWindow == null){
|
||||
runOnUiThread {
|
||||
LogWindow().apply {
|
||||
show()
|
||||
Schedulers.newThread().schedule {
|
||||
waitForShown()
|
||||
runOnUiThread {
|
||||
for ((line, level) in logs)
|
||||
logLine(line, level)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkExit(launcherVisibility)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun checkExit(launcherVisibility: LauncherVisibility) {
|
||||
when (launcherVisibility) {
|
||||
LauncherVisibility.HIDE_AND_REOPEN -> runOnUiThread { Controllers.stage.show() }
|
||||
LauncherVisibility.KEEP -> { /* no operations here. */ }
|
||||
LauncherVisibility.CLOSE -> { throw Error("Never get to here") }
|
||||
LauncherVisibility.HIDE -> runOnUiThread {
|
||||
// Shut down the platform when user closed log window.
|
||||
Platform.setImplicitExit(true)
|
||||
// If we use Main.stop(), log window will be halt immediately.
|
||||
Main.stopWithoutJavaFXPlatform()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopManagedProcesses() {
|
||||
synchronized(PROCESS) {
|
||||
while (PROCESS.isNotEmpty())
|
||||
PROCESS.poll()?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
enum class LoadingState {
|
||||
DEPENDENCIES,
|
||||
MODS,
|
||||
LOGIN,
|
||||
LAUNCHING,
|
||||
DONE
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ fun readModpackManifest(f: File): Modpack {
|
||||
}
|
||||
|
||||
fun MultiMCInstanceConfiguration.toVersionSetting(vs: VersionSetting) {
|
||||
vs.usesGlobal = false
|
||||
vs.isUsesGlobal = false
|
||||
vs.gameDirType = EnumGameDirectory.VERSION_FOLDER
|
||||
|
||||
if (isOverrideJavaLocation) {
|
||||
@@ -67,7 +67,7 @@ fun MultiMCInstanceConfiguration.toVersionSetting(vs: VersionSetting) {
|
||||
|
||||
if (isOverrideCommands) {
|
||||
vs.wrapper = wrapperCommand.orEmpty()
|
||||
vs.precalledCommand = preLaunchCommand.orEmpty()
|
||||
vs.preLaunchCommand = preLaunchCommand.orEmpty()
|
||||
}
|
||||
|
||||
if (isOverrideJavaArgs) {
|
||||
@@ -75,11 +75,11 @@ fun MultiMCInstanceConfiguration.toVersionSetting(vs: VersionSetting) {
|
||||
}
|
||||
|
||||
if (isOverrideConsole) {
|
||||
vs.showLogs = isShowConsole
|
||||
vs.isShowLogs = isShowConsole
|
||||
}
|
||||
|
||||
if (isOverrideWindow) {
|
||||
vs.fullscreen = isFullscreen
|
||||
vs.isFullscreen = isFullscreen
|
||||
if (width != null)
|
||||
vs.width = width!!
|
||||
if (height != null)
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting
|
||||
|
||||
import com.google.gson.*
|
||||
import javafx.beans.InvalidationListener
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager
|
||||
import org.jackhuang.hmcl.event.EventBus
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository
|
||||
import org.jackhuang.hmcl.mod.ModManager
|
||||
import org.jackhuang.hmcl.ui.runOnUiThread
|
||||
import org.jackhuang.hmcl.util.*
|
||||
import java.io.File
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class Profile(name: String = "Default", initialGameDir: File = File(".minecraft"), initialSelectedVersion: String = "") {
|
||||
val nameProperty = ImmediateStringProperty(this, "name", name)
|
||||
var name: String by nameProperty
|
||||
|
||||
val globalProperty = ImmediateObjectProperty<VersionSetting>(this, "global", VersionSetting())
|
||||
var global: VersionSetting by globalProperty
|
||||
|
||||
val selectedVersionProperty = ImmediateObjectProperty<String?>(this, "selectedVersion", initialSelectedVersion)
|
||||
var selectedVersion: String? by selectedVersionProperty
|
||||
|
||||
val gameDirProperty = ImmediateObjectProperty<File>(this, "gameDir", initialGameDir)
|
||||
var gameDir: File by gameDirProperty
|
||||
|
||||
var repository = HMCLGameRepository(this, initialGameDir)
|
||||
val dependency: DefaultDependencyManager get() = DefaultDependencyManager(repository, Settings.downloadProvider, Settings.proxy)
|
||||
var modManager = ModManager(repository)
|
||||
|
||||
init {
|
||||
gameDirProperty.onChange { repository.changeDirectory(it!!) }
|
||||
selectedVersionProperty.onInvalidated(this::verifySelectedVersion)
|
||||
EventBus.EVENT_BUS.channel<RefreshedVersionsEvent>().register { event -> if (event.source == repository) verifySelectedVersion() }
|
||||
}
|
||||
|
||||
private fun verifySelectedVersion() = runOnUiThread {
|
||||
// To prevent not loaded profile's selectedVersion being changed.
|
||||
if (repository.isLoaded && ((selectedVersion == null && repository.getVersions().isNotEmpty()) || (selectedVersion != null && !repository.hasVersion(selectedVersion!!)))) {
|
||||
val newVersion = repository.getVersions().firstOrNull()
|
||||
// will cause anthor change event, we must ensure that there will not be dead recursion.
|
||||
selectedVersion = newVersion?.id
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null if the given version id does not exist.
|
||||
*/
|
||||
fun specializeVersionSetting(id: String): VersionSetting? {
|
||||
var vs = repository.getVersionSetting(id)
|
||||
if (vs == null)
|
||||
vs = repository.createVersionSetting(id) ?: return null
|
||||
vs.usesGlobal = false
|
||||
return vs
|
||||
}
|
||||
|
||||
fun globalizeVersionSetting(id: String) {
|
||||
repository.getVersionSetting(id)?.usesGlobal = true
|
||||
}
|
||||
|
||||
fun isVersionGlobal(id: String): Boolean {
|
||||
return repository.getVersionSetting(id)?.usesGlobal ?: true
|
||||
}
|
||||
|
||||
fun getVersionSetting(id: String): VersionSetting {
|
||||
val vs = repository.getVersionSetting(id)
|
||||
if (vs == null || vs.usesGlobal) {
|
||||
global.isGlobal = true // always keep global.isGlobal = true
|
||||
return global
|
||||
} else
|
||||
return vs
|
||||
}
|
||||
|
||||
fun getSelectedVersionSetting(): VersionSetting? = if (selectedVersion == null) null else getVersionSetting(selectedVersion!!)
|
||||
|
||||
fun addPropertyChangedListener(listener: InvalidationListener) {
|
||||
nameProperty.addListener(listener)
|
||||
globalProperty.addListener(listener)
|
||||
selectedVersionProperty.addListener(listener)
|
||||
gameDirProperty.addListener(listener)
|
||||
}
|
||||
|
||||
companion object Serializer: JsonSerializer<Profile>, JsonDeserializer<Profile> {
|
||||
override fun serialize(src: Profile?, typeOfSrc: Type?, context: JsonSerializationContext): JsonElement {
|
||||
if (src == null) return JsonNull.INSTANCE
|
||||
val jsonObject = JsonObject()
|
||||
with(jsonObject) {
|
||||
add("global", context.serialize(src.global))
|
||||
addProperty("selectedVersion", src.selectedVersion)
|
||||
addProperty("gameDir", src.gameDir.path)
|
||||
}
|
||||
|
||||
return jsonObject
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext): Profile? {
|
||||
if (json == null || json == JsonNull.INSTANCE || json !is JsonObject) return null
|
||||
|
||||
return Profile(initialGameDir = File(json["gameDir"]?.asString ?: ""), initialSelectedVersion = json["selectedVersion"]?.asString ?: "").apply {
|
||||
global = context.deserialize(json["global"], VersionSetting::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,8 @@ import java.util.logging.Level
|
||||
|
||||
object Settings {
|
||||
val GSON = GsonBuilder()
|
||||
.registerTypeAdapter(VersionSetting::class.java, VersionSetting)
|
||||
.registerTypeAdapter(Profile::class.java, Profile)
|
||||
.registerTypeAdapter(VersionSetting::class.java, VersionSetting.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(Profile::class.java, Profile.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(File::class.java, FileTypeAdapter.INSTANCE)
|
||||
.setPrettyPrinting().create()
|
||||
|
||||
|
||||
@@ -1,336 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.setting
|
||||
|
||||
import com.google.gson.*
|
||||
import javafx.beans.InvalidationListener
|
||||
import org.jackhuang.hmcl.Main
|
||||
import org.jackhuang.hmcl.game.LaunchOptions
|
||||
import org.jackhuang.hmcl.setting.Settings.proxyHost
|
||||
import org.jackhuang.hmcl.setting.Settings.proxyPass
|
||||
import org.jackhuang.hmcl.setting.Settings.proxyPort
|
||||
import org.jackhuang.hmcl.setting.Settings.proxyUser
|
||||
import org.jackhuang.hmcl.util.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class VersionSetting() {
|
||||
|
||||
var isGlobal: Boolean = false
|
||||
|
||||
/**
|
||||
* HMCL Version Settings have been divided into 2 parts.
|
||||
* 1. Global settings.
|
||||
* 2. Version settings.
|
||||
* If a version claims that it uses global settings, its version setting will be disabled.
|
||||
*
|
||||
* Defaults false because if one version uses global first, custom version file will not be generated.
|
||||
*/
|
||||
val usesGlobalProperty = ImmediateBooleanProperty(this, "usesGlobal", false)
|
||||
var usesGlobal: Boolean by usesGlobalProperty
|
||||
|
||||
// java
|
||||
|
||||
/**
|
||||
* Java version or null if user customizes java directory.
|
||||
*/
|
||||
val javaProperty = ImmediateStringProperty(this, "java", "")
|
||||
var java: String by javaProperty
|
||||
|
||||
/**
|
||||
* User customized java directory or null if user uses system Java.
|
||||
*/
|
||||
val javaDirProperty = ImmediateStringProperty(this, "javaDir", "")
|
||||
var javaDir: String by javaDirProperty
|
||||
|
||||
/**
|
||||
* The command to launch java, i.e. optirun.
|
||||
*/
|
||||
val wrapperProperty = ImmediateStringProperty(this, "wrapper", "")
|
||||
var wrapper: String by wrapperProperty
|
||||
|
||||
/**
|
||||
* The permanent generation size of JVM garbage collection.
|
||||
*/
|
||||
val permSizeProperty = ImmediateStringProperty(this, "permSize", "")
|
||||
var permSize: String by permSizeProperty
|
||||
|
||||
/**
|
||||
* The maximum memory that JVM can allocate for heap.
|
||||
*/
|
||||
val maxMemoryProperty = ImmediateIntegerProperty(this, "maxMemory", OperatingSystem.SUGGESTED_MEMORY.toInt())
|
||||
var maxMemory: Int by maxMemoryProperty
|
||||
|
||||
/**
|
||||
* The minimum memory that JVM can allocate for heap.
|
||||
*/
|
||||
val minMemoryProperty = ImmediateObjectProperty<Int?>(this, "minMemory", null)
|
||||
var minMemory: Int? by minMemoryProperty
|
||||
|
||||
/**
|
||||
* The command that will be executed before launching the Minecraft.
|
||||
* Operating system relevant.
|
||||
*/
|
||||
val precalledCommandProperty = ImmediateStringProperty(this, "precalledCommand", "")
|
||||
var precalledCommand: String by precalledCommandProperty
|
||||
|
||||
// options
|
||||
|
||||
/**
|
||||
* The user customized arguments passed to JVM.
|
||||
*/
|
||||
val javaArgsProperty = ImmediateStringProperty(this, "javaArgs", "")
|
||||
var javaArgs: String by javaArgsProperty
|
||||
|
||||
/**
|
||||
* The user customized arguments passed to Minecraft.
|
||||
*/
|
||||
val minecraftArgsProperty = ImmediateStringProperty(this, "minecraftArgs", "")
|
||||
var minecraftArgs: String by minecraftArgsProperty
|
||||
|
||||
/**
|
||||
* True if disallow HMCL use default JVM arguments.
|
||||
*/
|
||||
val noJVMArgsProperty = ImmediateBooleanProperty(this, "noJVMArgs", false)
|
||||
var noJVMArgs: Boolean by noJVMArgsProperty
|
||||
|
||||
/**
|
||||
* True if HMCL does not check game's completeness.
|
||||
*/
|
||||
val notCheckGameProperty = ImmediateBooleanProperty(this, "notCheckGame", false)
|
||||
var notCheckGame: Boolean by notCheckGameProperty
|
||||
|
||||
/**
|
||||
* True if HMCL does not find/download libraries in/to common path.
|
||||
*/
|
||||
val noCommonProperty = ImmediateBooleanProperty(this, "noCommon", false)
|
||||
var noCommon: Boolean by noCommonProperty
|
||||
|
||||
/**
|
||||
* True if show the logs after game launched.
|
||||
*/
|
||||
val showLogsProperty = ImmediateBooleanProperty(this, "showLogs", false)
|
||||
var showLogs: Boolean by showLogsProperty
|
||||
|
||||
// Minecraft settings.
|
||||
|
||||
/**
|
||||
* The server ip that will be entered after Minecraft successfully loaded immediately.
|
||||
*
|
||||
* Format: ip:port or without port.
|
||||
*/
|
||||
val serverIpProperty = ImmediateStringProperty(this, "serverIp", "")
|
||||
var serverIp: String by serverIpProperty
|
||||
|
||||
/**
|
||||
* True if Minecraft started in fullscreen mode.
|
||||
*/
|
||||
val fullscreenProperty = ImmediateBooleanProperty(this, "fullscreen", false)
|
||||
var fullscreen: Boolean by fullscreenProperty
|
||||
|
||||
/**
|
||||
* The width of Minecraft window, defaults 800.
|
||||
*
|
||||
* The field saves int value.
|
||||
* String type prevents unexpected value from causing JsonSyntaxException.
|
||||
* We can only reset this field instead of recreating the whole setting file.
|
||||
*/
|
||||
val widthProperty = ImmediateIntegerProperty(this, "width", 854)
|
||||
var width: Int by widthProperty
|
||||
|
||||
|
||||
/**
|
||||
* The height of Minecraft window, defaults 480.
|
||||
*
|
||||
* The field saves int value.
|
||||
* String type prevents unexpected value from causing JsonSyntaxException.
|
||||
* We can only reset this field instead of recreating the whole setting file.
|
||||
*/
|
||||
val heightProperty = ImmediateIntegerProperty(this, "height", 480)
|
||||
var height: Int by heightProperty
|
||||
|
||||
|
||||
/**
|
||||
* 0 - .minecraft<br/>
|
||||
* 1 - .minecraft/versions/<version>/<br/>
|
||||
*/
|
||||
val gameDirTypeProperty = ImmediateObjectProperty<EnumGameDirectory>(this, "gameDirType", EnumGameDirectory.ROOT_FOLDER)
|
||||
var gameDirType: EnumGameDirectory by gameDirTypeProperty
|
||||
|
||||
/**
|
||||
* Your custom gameDir
|
||||
*/
|
||||
val gameDirProperty = ImmediateStringProperty(this, "gameDir", "")
|
||||
var gameDir: String by gameDirProperty
|
||||
|
||||
// launcher settings
|
||||
|
||||
/**
|
||||
* 0 - Close the launcher when the game starts.<br/>
|
||||
* 1 - Hide the launcher when the game starts.<br/>
|
||||
* 2 - Keep the launcher open.<br/>
|
||||
*/
|
||||
val launcherVisibilityProperty = ImmediateObjectProperty<LauncherVisibility>(this, "launcherVisibility", LauncherVisibility.HIDE)
|
||||
var launcherVisibility: LauncherVisibility by launcherVisibilityProperty
|
||||
|
||||
val javaVersion: JavaVersion? get() {
|
||||
// TODO: lazy initialization may result in UI suspension.
|
||||
if (java.isBlank())
|
||||
java = if (javaDir.isBlank()) "Default" else "Custom"
|
||||
if (java == "Default") return JavaVersion.fromCurrentEnvironment()
|
||||
else if (java == "Custom") {
|
||||
try {
|
||||
return JavaVersion.fromExecutable(File(javaDir))
|
||||
} catch (e: IOException) {
|
||||
return null // Custom Java Directory not found,
|
||||
}
|
||||
} else if (java.isNotBlank()) {
|
||||
val c = JavaVersion.getJREs()[java]
|
||||
if (c == null) {
|
||||
java = "Default"
|
||||
return JavaVersion.fromCurrentEnvironment()
|
||||
} else
|
||||
return c
|
||||
} else throw Error()
|
||||
}
|
||||
|
||||
fun addPropertyChangedListener(listener: InvalidationListener) {
|
||||
usesGlobalProperty.addListener(listener)
|
||||
javaProperty.addListener(listener)
|
||||
javaDirProperty.addListener(listener)
|
||||
wrapperProperty.addListener(listener)
|
||||
permSizeProperty.addListener(listener)
|
||||
maxMemoryProperty.addListener(listener)
|
||||
minMemoryProperty.addListener(listener)
|
||||
precalledCommandProperty.addListener(listener)
|
||||
javaArgsProperty.addListener(listener)
|
||||
minecraftArgsProperty.addListener(listener)
|
||||
noJVMArgsProperty.addListener(listener)
|
||||
notCheckGameProperty.addListener(listener)
|
||||
noCommonProperty.addListener(listener)
|
||||
showLogsProperty.addListener(listener)
|
||||
serverIpProperty.addListener(listener)
|
||||
fullscreenProperty.addListener(listener)
|
||||
widthProperty.addListener(listener)
|
||||
heightProperty.addListener(listener)
|
||||
gameDirTypeProperty.addListener(listener)
|
||||
gameDirProperty.addListener(listener)
|
||||
launcherVisibilityProperty.addListener(listener)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun toLaunchOptions(gameDir: File): LaunchOptions {
|
||||
return LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
.setJava(javaVersion ?: JavaVersion.fromCurrentEnvironment())
|
||||
.setVersionName(Main.TITLE)
|
||||
.setProfileName(Main.TITLE)
|
||||
.setMinecraftArgs(minecraftArgs)
|
||||
.setJavaArgs(javaArgs)
|
||||
.setMaxMemory(maxMemory)
|
||||
.setMinMemory(minMemory)
|
||||
.setMetaspace(permSize.toIntOrNull())
|
||||
.setWidth(width)
|
||||
.setHeight(height)
|
||||
.setFullscreen(fullscreen)
|
||||
.setServerIp(serverIp)
|
||||
.setWrapper(wrapper)
|
||||
.setProxyHost(Settings.proxyHost)
|
||||
.setProxyPort(Settings.proxyPort)
|
||||
.setProxyUser(Settings.proxyUser)
|
||||
.setProxyPass(Settings.proxyPass)
|
||||
.setPrecalledCommand(precalledCommand)
|
||||
.setNoGeneratedJVMArgs(noJVMArgs)
|
||||
.create()
|
||||
}
|
||||
|
||||
companion object Serializer: JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
|
||||
override fun serialize(src: VersionSetting?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
|
||||
if (src == null) return JsonNull.INSTANCE
|
||||
val jsonObject = JsonObject()
|
||||
with(jsonObject) {
|
||||
addProperty("usesGlobal", src.usesGlobal)
|
||||
addProperty("javaArgs", src.javaArgs)
|
||||
addProperty("minecraftArgs", src.minecraftArgs)
|
||||
addProperty("maxMemory", if (src.maxMemory <= 0) OperatingSystem.SUGGESTED_MEMORY.toInt() else src.maxMemory)
|
||||
addProperty("minMemory", src.minMemory)
|
||||
addProperty("permSize", src.permSize)
|
||||
addProperty("width", src.width)
|
||||
addProperty("height", src.height)
|
||||
addProperty("javaDir", src.javaDir)
|
||||
addProperty("precalledCommand", src.precalledCommand)
|
||||
addProperty("serverIp", src.serverIp)
|
||||
addProperty("java", src.java)
|
||||
addProperty("wrapper", src.wrapper)
|
||||
addProperty("fullscreen", src.fullscreen)
|
||||
addProperty("noJVMArgs", src.noJVMArgs)
|
||||
addProperty("notCheckGame", src.notCheckGame)
|
||||
addProperty("noCommon", src.noCommon)
|
||||
addProperty("showLogs", src.showLogs)
|
||||
addProperty("gameDir", src.gameDir)
|
||||
addProperty("launcherVisibility", src.launcherVisibility.ordinal)
|
||||
addProperty("gameDirType", src.gameDirType.ordinal)
|
||||
}
|
||||
|
||||
return jsonObject
|
||||
}
|
||||
|
||||
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): VersionSetting? {
|
||||
if (json == null || json == JsonNull.INSTANCE || json !is JsonObject) return null
|
||||
|
||||
var maxMemoryN = parseJsonPrimitive(json["maxMemory"]?.asJsonPrimitive, OperatingSystem.SUGGESTED_MEMORY.toInt())
|
||||
if (maxMemoryN <= 0) maxMemoryN = OperatingSystem.SUGGESTED_MEMORY.toInt()
|
||||
|
||||
return VersionSetting().apply {
|
||||
usesGlobal = json["usesGlobal"]?.asBoolean ?: false
|
||||
javaArgs = json["javaArgs"]?.asString ?: ""
|
||||
minecraftArgs = json["minecraftArgs"]?.asString ?: ""
|
||||
maxMemory = maxMemoryN
|
||||
minMemory = json["minMemory"]?.asInt
|
||||
permSize = json["permSize"]?.asString ?: ""
|
||||
width = parseJsonPrimitive(json["width"]?.asJsonPrimitive)
|
||||
height = parseJsonPrimitive(json["height"]?.asJsonPrimitive)
|
||||
javaDir = json["javaDir"]?.asString ?: ""
|
||||
precalledCommand = json["precalledCommand"]?.asString ?: ""
|
||||
serverIp = json["serverIp"]?.asString ?: ""
|
||||
java = json["java"]?.asString ?: ""
|
||||
wrapper = json["wrapper"]?.asString ?: ""
|
||||
gameDir = json["gameDir"]?.asString ?: ""
|
||||
fullscreen = json["fullscreen"]?.asBoolean ?: false
|
||||
noJVMArgs = json["noJVMArgs"]?.asBoolean ?: false
|
||||
notCheckGame = json["notCheckGame"]?.asBoolean ?: false
|
||||
noCommon = json["noCommon"]?.asBoolean ?: false
|
||||
showLogs = json["showLogs"]?.asBoolean ?: false
|
||||
launcherVisibility = LauncherVisibility.values()[json["launcherVisibility"]?.asInt ?: 1]
|
||||
gameDirType = EnumGameDirectory.values()[json["gameDirType"]?.asInt ?: 0]
|
||||
}
|
||||
}
|
||||
|
||||
fun parseJsonPrimitive(primitive: JsonPrimitive?, defaultValue: Int = 0): Int {
|
||||
if (primitive != null)
|
||||
if (primitive.isNumber)
|
||||
return primitive.asInt
|
||||
else
|
||||
return primitive.asString.toIntOrNull() ?: defaultValue
|
||||
else
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,13 @@ import javafx.scene.control.ScrollPane
|
||||
import javafx.scene.control.ToggleGroup
|
||||
import javafx.scene.layout.StackPane
|
||||
import org.jackhuang.hmcl.auth.Account
|
||||
import org.jackhuang.hmcl.auth.MultiCharacterSelector
|
||||
import org.jackhuang.hmcl.auth.OfflineAccount
|
||||
import org.jackhuang.hmcl.auth.OfflineAccountFactory
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory
|
||||
import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector
|
||||
import org.jackhuang.hmcl.i18n
|
||||
import org.jackhuang.hmcl.setting.Settings
|
||||
import org.jackhuang.hmcl.task.Schedulers
|
||||
@@ -133,7 +135,7 @@ class AccountsPage() : StackPane(), DecoratorPage {
|
||||
else -> throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
account.logIn(Settings.proxy)
|
||||
account.logIn(HMCLMultiCharacterSelector, Settings.proxy)
|
||||
account
|
||||
} catch (e: Exception) {
|
||||
e
|
||||
|
||||
@@ -52,7 +52,7 @@ object Controllers {
|
||||
|
||||
decorator.isCustomMaximize = false
|
||||
|
||||
scene = Scene(decorator, 800.0, 480.0)
|
||||
scene = Scene(decorator, 804.0, 521.0)
|
||||
scene.stylesheets.addAll(*stylesheets)
|
||||
stage.minWidth = 800.0
|
||||
stage.maxWidth = 800.0
|
||||
|
||||
@@ -266,4 +266,4 @@ fun ImageView.limitSize(maxWidth: Double, maxHeight: Double) {
|
||||
}
|
||||
}
|
||||
|
||||
val DEFAULT_ICON = Image("/assets/img/icon.png")
|
||||
@JvmField val DEFAULT_ICON = Image("/assets/img/icon.png")
|
||||
@@ -97,9 +97,7 @@ class LeftPaneController(private val leftPane: AdvancedListBox) {
|
||||
fun onProfilesLoading() {
|
||||
val list = LinkedList<RipplerContainer>()
|
||||
Settings.getProfiles().forEach { profile ->
|
||||
val item = VersionListItem(profile.name).apply {
|
||||
lblGameVersion.textProperty().bind(profile.selectedVersionProperty)
|
||||
}
|
||||
val item = VersionListItem(profile.name)
|
||||
val ripplerContainer = RipplerContainer(item)
|
||||
item.onSettingsButtonClicked {
|
||||
Controllers.decorator.showPage(ProfilePage(profile))
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui
|
||||
|
||||
import com.jfoenix.controls.JFXComboBox
|
||||
import javafx.beans.Observable
|
||||
import javafx.beans.binding.Bindings
|
||||
import javafx.beans.property.SimpleIntegerProperty
|
||||
import javafx.concurrent.Worker
|
||||
import javafx.fxml.FXML
|
||||
import javafx.scene.Scene
|
||||
import javafx.scene.control.ToggleButton
|
||||
import javafx.scene.image.Image
|
||||
import javafx.scene.layout.StackPane
|
||||
import javafx.scene.web.WebEngine
|
||||
import javafx.scene.web.WebView
|
||||
import javafx.stage.Stage
|
||||
import netscape.javascript.JSObject
|
||||
import org.jackhuang.hmcl.game.LauncherHelper
|
||||
import org.jackhuang.hmcl.i18n
|
||||
import org.jackhuang.hmcl.setting.Settings
|
||||
import org.jackhuang.hmcl.util.*
|
||||
import org.w3c.dom.Document
|
||||
import org.w3c.dom.Node
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
class LogWindow : Stage() {
|
||||
val fatalProperty = SimpleIntegerProperty(0)
|
||||
val errorProperty = SimpleIntegerProperty(0)
|
||||
val warnProperty = SimpleIntegerProperty(0)
|
||||
val infoProperty = SimpleIntegerProperty(0)
|
||||
val debugProperty = SimpleIntegerProperty(0)
|
||||
|
||||
val impl = LogWindowImpl()
|
||||
val latch = CountDownLatch(1)
|
||||
|
||||
init {
|
||||
scene = Scene(impl, 800.0, 480.0)
|
||||
scene.stylesheets.addAll(*stylesheets)
|
||||
title = i18n("logwindow.title")
|
||||
icons += Image("/assets/img/icon.png")
|
||||
}
|
||||
|
||||
fun logLine(line: String, level: Log4jLevel) {
|
||||
impl.body.appendChild(impl.engine.document.createElement("div").apply {
|
||||
// a <pre> element to prevent multiple spaces and tabs being removed.
|
||||
appendChild(impl.engine.document.createElement("pre").apply {
|
||||
textContent = line
|
||||
})
|
||||
})
|
||||
impl.engine.executeScript("checkNewLog(\"${level.name.toLowerCase()}\");scrollToBottom();")
|
||||
|
||||
when (level) {
|
||||
Log4jLevel.FATAL -> fatalProperty.inc()
|
||||
Log4jLevel.ERROR -> errorProperty.inc()
|
||||
Log4jLevel.WARN -> warnProperty.inc()
|
||||
Log4jLevel.INFO -> infoProperty.inc()
|
||||
Log4jLevel.DEBUG -> debugProperty.inc()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
fun waitForShown() = latch.await()
|
||||
|
||||
inner class LogWindowImpl: StackPane() {
|
||||
@FXML lateinit var webView: WebView
|
||||
@FXML lateinit var btnFatals: ToggleButton
|
||||
@FXML lateinit var btnErrors: ToggleButton
|
||||
@FXML lateinit var btnWarns: ToggleButton
|
||||
@FXML lateinit var btnInfos: ToggleButton
|
||||
@FXML lateinit var btnDebugs: ToggleButton
|
||||
|
||||
@FXML lateinit var cboLines: JFXComboBox<String>
|
||||
val engine: WebEngine
|
||||
lateinit var body: Node
|
||||
lateinit var document: Document
|
||||
|
||||
init {
|
||||
loadFXML("/assets/fxml/log.fxml")
|
||||
|
||||
engine = webView.engine
|
||||
engine.loadContent(javaClass.getResourceAsStream("/assets/log-window-content.html").readFullyAsString().replace("\${FONT}", "${Settings.font.size}px \"${Settings.font.family}\""))
|
||||
engine.loadWorker.stateProperty().onChange {
|
||||
if (it == Worker.State.SUCCEEDED) {
|
||||
document = engine.document
|
||||
body = document.getElementsByTagName("body").item(0)
|
||||
engine.executeScript("limitedLogs=${Settings.logLines};")
|
||||
latch.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
var flag = false
|
||||
for (i in cboLines.items) {
|
||||
if (i == Settings.logLines.toString()) {
|
||||
cboLines.selectionModel.select(i)
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
cboLines.selectionModel.selectedItemProperty().onChange {
|
||||
Settings.logLines = it?.toInt() ?: 100
|
||||
engine.executeScript("limitedLogs=${Settings.logLines};")
|
||||
}
|
||||
if (!flag) {
|
||||
cboLines.selectionModel.select(0)
|
||||
}
|
||||
|
||||
btnFatals.textProperty().bind(Bindings.createStringBinding(Callable { fatalProperty.get().toString() + " fatals" }, fatalProperty))
|
||||
btnErrors.textProperty().bind(Bindings.createStringBinding(Callable { errorProperty.get().toString() + " errors" }, errorProperty))
|
||||
btnWarns.textProperty().bind(Bindings.createStringBinding(Callable { warnProperty.get().toString() + " warns" }, warnProperty))
|
||||
btnInfos.textProperty().bind(Bindings.createStringBinding(Callable { infoProperty.get().toString() + " infos" }, infoProperty))
|
||||
btnDebugs.textProperty().bind(Bindings.createStringBinding(Callable { debugProperty.get().toString() + " debugs" }, debugProperty))
|
||||
|
||||
btnFatals.selectedProperty().onInvalidated(this::specificChanged)
|
||||
btnErrors.selectedProperty().onInvalidated(this::specificChanged)
|
||||
btnWarns.selectedProperty().onInvalidated(this::specificChanged)
|
||||
btnInfos.selectedProperty().onInvalidated(this::specificChanged)
|
||||
btnDebugs.selectedProperty().onInvalidated(this::specificChanged)
|
||||
}
|
||||
|
||||
private fun specificChanged() {
|
||||
var res = ""
|
||||
if (btnFatals.isSelected) res += "\"fatal\", "
|
||||
if (btnErrors.isSelected) res += "\"error\", "
|
||||
if (btnWarns.isSelected) res += "\"warn\", "
|
||||
if (btnInfos.isSelected) res += "\"info\", "
|
||||
if (btnDebugs.isSelected) res += "\"debug\", "
|
||||
if (res.isNotBlank())
|
||||
res = res.substringBeforeLast(", ")
|
||||
engine.executeScript("specific([$res])")
|
||||
}
|
||||
|
||||
fun onTerminateGame() {
|
||||
LauncherHelper.stopManagedProcesses()
|
||||
}
|
||||
|
||||
fun onClear() {
|
||||
engine.executeScript("clear()")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,8 @@ import com.jfoenix.controls.JFXButton
|
||||
import com.jfoenix.controls.JFXMasonryPane
|
||||
import javafx.application.Platform
|
||||
import javafx.beans.property.SimpleStringProperty
|
||||
import javafx.beans.property.StringProperty
|
||||
import javafx.fxml.FXML
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.control.ToggleGroup
|
||||
import javafx.scene.image.Image
|
||||
import javafx.scene.layout.StackPane
|
||||
import org.jackhuang.hmcl.ProfileChangedEvent
|
||||
@@ -36,11 +34,9 @@ import org.jackhuang.hmcl.game.LauncherHelper
|
||||
import org.jackhuang.hmcl.i18n
|
||||
import org.jackhuang.hmcl.setting.Profile
|
||||
import org.jackhuang.hmcl.setting.Settings
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer
|
||||
import org.jackhuang.hmcl.ui.download.DownloadWizardProvider
|
||||
import org.jackhuang.hmcl.ui.wizard.DecoratorPage
|
||||
import org.jackhuang.hmcl.util.channel
|
||||
import org.jackhuang.hmcl.util.onChange
|
||||
import org.jackhuang.hmcl.util.plusAssign
|
||||
|
||||
/**
|
||||
@@ -49,7 +45,6 @@ import org.jackhuang.hmcl.util.plusAssign
|
||||
class MainPage : StackPane(), DecoratorPage {
|
||||
override val titleProperty = SimpleStringProperty(this, "title", i18n("launcher.title.main"))
|
||||
|
||||
@FXML lateinit var btnLaunch: JFXButton
|
||||
@FXML lateinit var btnRefresh: JFXButton
|
||||
@FXML lateinit var btnAdd: JFXButton
|
||||
@FXML lateinit var masonryPane: JFXMasonryPane
|
||||
@@ -57,32 +52,24 @@ class MainPage : StackPane(), DecoratorPage {
|
||||
init {
|
||||
loadFXML("/assets/fxml/main.fxml")
|
||||
|
||||
btnLaunch.graphic = SVG.launch("white", 15.0, 15.0)
|
||||
btnLaunch.limitWidth(40.0)
|
||||
btnLaunch.limitHeight(40.0)
|
||||
|
||||
EventBus.EVENT_BUS.channel<RefreshedVersionsEvent>() += { -> loadVersions() }
|
||||
EventBus.EVENT_BUS.channel<RefreshedVersionsEvent>() += { -> runOnUiThread { loadVersions() } }
|
||||
EventBus.EVENT_BUS.channel<ProfileLoadingEvent>() += this::onProfilesLoading
|
||||
EventBus.EVENT_BUS.channel<ProfileChangedEvent>() += this::onProfileChanged
|
||||
|
||||
btnAdd.setOnMouseClicked { Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game") }
|
||||
btnRefresh.setOnMouseClicked { Settings.selectedProfile.repository.refreshVersions() }
|
||||
btnLaunch.setOnMouseClicked {
|
||||
if (Settings.selectedAccount == null) {
|
||||
Controllers.dialog(i18n("login.no_Player007"))
|
||||
} else if (Settings.selectedProfile.selectedVersion == null) {
|
||||
Controllers.dialog(i18n("minecraft.no_selected_version"))
|
||||
} else
|
||||
LauncherHelper.launch()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNode(i: Int, profile: Profile, version: String, game: String, group: ToggleGroup): Node {
|
||||
return VersionItem(i, group).apply {
|
||||
chkSelected.properties["version"] = version
|
||||
chkSelected.isSelected = profile.selectedVersion == version
|
||||
private fun buildNode(i: Int, profile: Profile, version: String, game: String): Node {
|
||||
return VersionItem().apply {
|
||||
lblGameVersion.text = game
|
||||
lblVersionName.text = version
|
||||
btnLaunch.setOnMouseClicked {
|
||||
if (Settings.selectedAccount == null) {
|
||||
Controllers.dialog(i18n("login.no_Player007"))
|
||||
} else
|
||||
LauncherHelper.INSTANCE.launch(version)
|
||||
}
|
||||
btnDelete.setOnMouseClicked {
|
||||
profile.repository.removeVersionFromDisk(version)
|
||||
Platform.runLater { loadVersions() }
|
||||
@@ -103,30 +90,15 @@ class MainPage : StackPane(), DecoratorPage {
|
||||
|
||||
fun onProfileChanged(event: ProfileChangedEvent) = runOnUiThread {
|
||||
val profile = event.value
|
||||
profile.selectedVersionProperty.setChangedListener {
|
||||
versionChanged(profile.selectedVersion)
|
||||
}
|
||||
loadVersions(profile)
|
||||
}
|
||||
|
||||
private fun loadVersions(profile: Profile = Settings.selectedProfile) {
|
||||
val group = ToggleGroup()
|
||||
val children = mutableListOf<Node>()
|
||||
var i = 0
|
||||
profile.repository.getVersions().forEach { version ->
|
||||
children += buildNode(++i, profile, version.id, minecraftVersion(profile.repository.getVersionJar(version.id)) ?: "Unknown", group)
|
||||
}
|
||||
group.selectedToggleProperty().onChange {
|
||||
if (it != null)
|
||||
profile.selectedVersion = it.properties["version"] as String
|
||||
profile.repository.versions.forEach { version ->
|
||||
children += buildNode(++i, profile, version.id, minecraftVersion(profile.repository.getVersionJar(version.id)) ?: "Unknown")
|
||||
}
|
||||
masonryPane.resetChildren(children)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun versionChanged(selectedVersion: String?) {
|
||||
masonryPane.children
|
||||
.filter { it is RipplerContainer && it.properties["version"] is Pair<*, *> }
|
||||
.forEach { (it as RipplerContainer).selected = (it.properties["version"] as Pair<String, VersionListItem>).first == selectedVersion }
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class ProfilePage(private val profile: Profile?): StackPane(), DecoratorPage {
|
||||
if (locationProperty.get().isNullOrBlank()) {
|
||||
gameDir.onExplore()
|
||||
}
|
||||
Settings.putProfile(Profile(name = txtProfileName.text, initialGameDir = File(locationProperty.get())))
|
||||
Settings.putProfile(Profile(txtProfileName.text, File(locationProperty.get())))
|
||||
}
|
||||
|
||||
Settings.onProfileLoading()
|
||||
|
||||
@@ -32,15 +32,15 @@ import javafx.scene.layout.VBox
|
||||
import javafx.scene.paint.Color
|
||||
import java.util.concurrent.Callable
|
||||
|
||||
class VersionItem(i: Int, group: ToggleGroup) : StackPane() {
|
||||
class VersionItem() : StackPane() {
|
||||
@FXML lateinit var icon: Pane
|
||||
@FXML lateinit var content: VBox
|
||||
@FXML lateinit var header: StackPane
|
||||
@FXML lateinit var body: StackPane
|
||||
@FXML lateinit var btnDelete: JFXButton
|
||||
@FXML lateinit var btnSettings: JFXButton
|
||||
@FXML lateinit var btnLaunch: JFXButton
|
||||
@FXML lateinit var lblVersionName: Label
|
||||
@FXML lateinit var chkSelected: JFXRadioButton
|
||||
@FXML lateinit var lblGameVersion: Label
|
||||
@FXML lateinit var iconView: ImageView
|
||||
|
||||
@@ -52,20 +52,20 @@ class VersionItem(i: Int, group: ToggleGroup) : StackPane() {
|
||||
|
||||
effect = DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.26), 5.0, 0.12, -1.0, 1.0)
|
||||
|
||||
chkSelected.toggleGroup = group
|
||||
btnSettings.graphic = SVG.gear("black", 15.0, 15.0)
|
||||
btnDelete.graphic = SVG.delete("black", 15.0, 15.0)
|
||||
btnLaunch.graphic = SVG.launch("black", 15.0, 15.0)
|
||||
|
||||
// create content
|
||||
val headerColor = getDefaultColor(i % 12)
|
||||
header.style = "-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor
|
||||
//val headerColor = getDefaultColor(i % 12)
|
||||
//header.style = "-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor
|
||||
|
||||
// create image view
|
||||
icon.translateYProperty().bind(Bindings.createDoubleBinding(Callable { header.boundsInParent.height - icon.height / 2 - 32.0 }, header.boundsInParentProperty(), icon.heightProperty()))
|
||||
icon.translateYProperty().bind(Bindings.createDoubleBinding(Callable { header.boundsInParent.height - icon.height / 2 - 16.0 }, header.boundsInParentProperty(), icon.heightProperty()))
|
||||
iconView.limitSize(32.0, 32.0)
|
||||
}
|
||||
|
||||
private fun getDefaultColor(i: Int): String {
|
||||
/*private fun getDefaultColor(i: Int): String {
|
||||
var color = "#FFFFFF"
|
||||
when (i) {
|
||||
0 -> color = "#8F3F7E"
|
||||
@@ -85,5 +85,5 @@ class VersionItem(i: Int, group: ToggleGroup) : StackPane() {
|
||||
}
|
||||
}
|
||||
return color
|
||||
}
|
||||
}*/
|
||||
}
|
||||
@@ -117,39 +117,39 @@ class VersionSettingsController {
|
||||
this.versionId = versionId
|
||||
|
||||
lastVersionSetting?.apply {
|
||||
widthProperty.unbind()
|
||||
heightProperty.unbind()
|
||||
maxMemoryProperty.unbind()
|
||||
javaArgsProperty.unbind()
|
||||
minecraftArgsProperty.unbind()
|
||||
permSizeProperty.unbind()
|
||||
wrapperProperty.unbind()
|
||||
precalledCommandProperty.unbind()
|
||||
serverIpProperty.unbind()
|
||||
fullscreenProperty.unbind()
|
||||
notCheckGameProperty.unbind()
|
||||
noCommonProperty.unbind()
|
||||
javaDirProperty.unbind()
|
||||
showLogsProperty.unbind()
|
||||
widthProperty().unbind()
|
||||
heightProperty().unbind()
|
||||
maxMemoryProperty().unbind()
|
||||
javaArgsProperty().unbind()
|
||||
minecraftArgsProperty().unbind()
|
||||
permSizeProperty().unbind()
|
||||
wrapperProperty().unbind()
|
||||
preLaunchCommandProperty().unbind()
|
||||
serverIpProperty().unbind()
|
||||
fullscreenProperty().unbind()
|
||||
notCheckGameProperty().unbind()
|
||||
noCommonProperty().unbind()
|
||||
javaDirProperty().unbind()
|
||||
showLogsProperty().unbind()
|
||||
unbindEnum(cboLauncherVisibility)
|
||||
}
|
||||
|
||||
bindInt(txtWidth, version.widthProperty)
|
||||
bindInt(txtHeight, version.heightProperty)
|
||||
bindInt(txtMaxMemory, version.maxMemoryProperty)
|
||||
bindString(javaItem.txtCustom, version.javaDirProperty)
|
||||
bindString(gameDirItem.txtCustom, version.gameDirProperty)
|
||||
bindString(txtJVMArgs, version.javaArgsProperty)
|
||||
bindString(txtGameArgs, version.minecraftArgsProperty)
|
||||
bindString(txtMetaspace, version.permSizeProperty)
|
||||
bindString(txtWrapper, version.wrapperProperty)
|
||||
bindString(txtPrecallingCommand, version.precalledCommandProperty)
|
||||
bindString(txtServerIP, version.serverIpProperty)
|
||||
bindEnum(cboLauncherVisibility, version.launcherVisibilityProperty)
|
||||
bindBoolean(chkFullscreen, version.fullscreenProperty)
|
||||
bindBoolean(chkNoGameCheck, version.notCheckGameProperty)
|
||||
bindBoolean(chkNoCommon, version.noCommonProperty)
|
||||
bindBoolean(chkShowLogs, version.showLogsProperty)
|
||||
bindInt(txtWidth, version.widthProperty())
|
||||
bindInt(txtHeight, version.heightProperty())
|
||||
bindInt(txtMaxMemory, version.maxMemoryProperty())
|
||||
bindString(javaItem.txtCustom, version.javaDirProperty())
|
||||
bindString(gameDirItem.txtCustom, version.gameDirProperty())
|
||||
bindString(txtJVMArgs, version.javaArgsProperty())
|
||||
bindString(txtGameArgs, version.minecraftArgsProperty())
|
||||
bindString(txtMetaspace, version.permSizeProperty())
|
||||
bindString(txtWrapper, version.wrapperProperty())
|
||||
bindString(txtPrecallingCommand, version.preLaunchCommandProperty())
|
||||
bindString(txtServerIP, version.serverIpProperty())
|
||||
bindEnum(cboLauncherVisibility, version.launcherVisibilityProperty())
|
||||
bindBoolean(chkFullscreen, version.fullscreenProperty())
|
||||
bindBoolean(chkNoGameCheck, version.notCheckGameProperty())
|
||||
bindBoolean(chkNoCommon, version.noCommonProperty())
|
||||
bindBoolean(chkShowLogs, version.showLogsProperty())
|
||||
|
||||
val javaGroupKey = "java_group.listener"
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@@ -181,8 +181,8 @@ class VersionSettingsController {
|
||||
defaultToggle?.isSelected = true
|
||||
}
|
||||
|
||||
version.javaDirProperty.setChangedListener { initJavaSubtitle(version) }
|
||||
version.javaProperty.setChangedListener { initJavaSubtitle(version) }
|
||||
version.javaDirProperty().setChangedListener { initJavaSubtitle(version) }
|
||||
version.javaProperty().setChangedListener { initJavaSubtitle(version) }
|
||||
initJavaSubtitle(version)
|
||||
|
||||
val gameDirKey = "game_dir.listener"
|
||||
@@ -205,8 +205,8 @@ class VersionSettingsController {
|
||||
gameDirItem.group.properties[gameDirKey] = gameDirListener
|
||||
gameDirItem.group.selectedToggleProperty().addListener(gameDirListener)
|
||||
|
||||
version.gameDirProperty.setChangedListener { initGameDirSubtitle(version) }
|
||||
version.gameDirTypeProperty.setChangedListener { initGameDirSubtitle(version) }
|
||||
version.gameDirProperty().setChangedListener { initGameDirSubtitle(version) }
|
||||
version.gameDirTypeProperty().setChangedListener { initGameDirSubtitle(version) }
|
||||
initGameDirSubtitle(version)
|
||||
|
||||
lastVersionSetting = version
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.auth.AuthInfo
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory
|
||||
import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector
|
||||
import org.jackhuang.hmcl.i18n
|
||||
import org.jackhuang.hmcl.setting.Settings
|
||||
import org.jackhuang.hmcl.task.Schedulers
|
||||
@@ -56,7 +57,7 @@ class YggdrasilAccountLoginPane(private val oldAccount: YggdrasilAccount, privat
|
||||
taskResult("login") {
|
||||
try {
|
||||
val account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password)
|
||||
account.logIn(Settings.proxy)
|
||||
account.logIn(HMCLMultiCharacterSelector, Settings.proxy)
|
||||
} catch (e: Exception) {
|
||||
e
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.google.gson.JsonParseException
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import javafx.beans.property.Property
|
||||
import javafx.event.Event.fireEvent
|
||||
import org.jackhuang.hmcl.event.Event
|
||||
import org.jackhuang.hmcl.event.EventBus
|
||||
import org.jackhuang.hmcl.event.EventManager
|
||||
import org.jackhuang.hmcl.task.Scheduler
|
||||
@@ -113,10 +114,10 @@ fun <V> taskResult(id: String, callable: Callable<V>): TaskResult<V> = Task.ofRe
|
||||
fun <V> taskResult(id: String, callable: (AutoTypingMap<String>) -> V): TaskResult<V> = Task.ofResult(id, callable)
|
||||
|
||||
fun InputStream.readFullyAsString() = IOUtils.readFullyAsString(this)
|
||||
inline fun <reified T : EventObject> EventBus.channel() = channel(T::class.java)
|
||||
inline fun <reified T : Event> EventBus.channel() = channel(T::class.java)
|
||||
|
||||
operator fun <T : EventObject> EventManager<T>.plusAssign(func: (T) -> Unit) = register(func)
|
||||
operator fun <T : EventObject> EventManager<T>.plusAssign(func: () -> Unit) = register(func)
|
||||
operator fun <T : EventObject> EventManager<T>.minusAssign(func: (T) -> Unit) = unregister(func)
|
||||
operator fun <T : EventObject> EventManager<T>.minusAssign(func: () -> Unit) = unregister(func)
|
||||
operator fun <T : EventObject> EventManager<T>.invoke(event: T) = fireEvent(event)
|
||||
operator fun <T : Event> EventManager<T>.plusAssign(func: (T) -> Unit) = register(func)
|
||||
operator fun <T : Event> EventManager<T>.plusAssign(func: () -> Unit) = register(func)
|
||||
operator fun <T : Event> EventManager<T>.minusAssign(func: (T) -> Unit) = unregister(func)
|
||||
operator fun <T : Event> EventManager<T>.minusAssign(func: () -> Unit) = unregister(func)
|
||||
operator fun <T : Event> EventManager<T>.invoke(event: T) = fireEvent(event)
|
||||
@@ -968,6 +968,11 @@
|
||||
-fx-background-color: -fx-decorator-color;
|
||||
}
|
||||
|
||||
.jfx-decorator-drawer {
|
||||
-fx-background-image: url("/assets/img/background.jpg");
|
||||
-fx-background-size: cover;
|
||||
}
|
||||
|
||||
.resize-border {
|
||||
-fx-border-color: #5264AE;
|
||||
-fx-border-width: 0 2 2 2;
|
||||
@@ -1136,6 +1141,7 @@
|
||||
******************************************************************************/
|
||||
|
||||
.scroll-pane {
|
||||
-fx-background-color: null;
|
||||
-fx-background-insets: 0;
|
||||
-fx-padding: 0;
|
||||
}
|
||||
@@ -1148,6 +1154,10 @@
|
||||
-fx-background-insets: 0;
|
||||
}
|
||||
|
||||
.scroll-pane > .viewport {
|
||||
-fx-background-color: null;
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Error Facade *
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
<?import javafx.scene.shape.Rectangle?>
|
||||
<?import org.jackhuang.hmcl.ui.AdvancedListBox?>
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<fx:root xmlns="http://javafx.com/javafx"
|
||||
type="StackPane"
|
||||
xmlns:fx="http://javafx.com/fxml">
|
||||
@@ -16,14 +18,14 @@
|
||||
</styleClass>
|
||||
<BorderPane>
|
||||
<center>
|
||||
<StackPane fx:id="drawerWrapper">
|
||||
<StackPane fx:id="drawerWrapper" styleClass="jfx-decorator-drawer">
|
||||
<JFXDialog fx:id="dialog" overlayClose="false" />
|
||||
<BorderPane>
|
||||
<left>
|
||||
<StackPane minWidth="200" maxWidth="200" styleClass="jfx-decorator-content-container">
|
||||
<BorderPane fx:id="leftRootPane">
|
||||
<center>
|
||||
<BorderPane>
|
||||
<BorderPane style="-fx-background-color: rgba(244, 244, 244, 0.5);">
|
||||
<center>
|
||||
<AdvancedListBox fx:id="leftPane"/>
|
||||
</center>
|
||||
@@ -50,9 +52,6 @@
|
||||
<StackPane fx:id="contentPlaceHolderRoot" styleClass="jfx-decorator-content-container"
|
||||
VBox.vgrow="ALWAYS">
|
||||
<StackPane fx:id="contentPlaceHolder" styleClass="jfx-decorator-content-container">
|
||||
<styleClass>
|
||||
<String fx:value="jfx-decorator-content-container"/>
|
||||
</styleClass>
|
||||
<!-- Node -->
|
||||
</StackPane>
|
||||
</StackPane>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<?import javafx.scene.control.ScrollPane?>
|
||||
<fx:root
|
||||
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
|
||||
styleClass="transparent" type="StackPane" pickOnBounds="false"
|
||||
type="StackPane" pickOnBounds="false"
|
||||
xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<ScrollPane fitToHeight="true" fitToWidth="true" fx:id="scrollPane" hbarPolicy="NEVER">
|
||||
<JFXMasonryPane fx:id="masonryPane" HSpacing="3" VSpacing="3" cellWidth="182" cellHeight="160">
|
||||
@@ -24,12 +24,6 @@
|
||||
<fx:include source="/assets/svg/plus.fxml" />
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
<JFXButton prefWidth="40" prefHeight="40" buttonType="RAISED" fx:id="btnLaunch"
|
||||
style="-fx-background-color:#5264AE;-fx-background-radius: 50px;">
|
||||
<graphic>
|
||||
<fx:include source="/assets/svg/rocket.fxml" />
|
||||
</graphic>
|
||||
</JFXButton>
|
||||
</VBox>
|
||||
|
||||
</fx:root>
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
type="StackPane" pickOnBounds="false">
|
||||
<VBox fx:id="content" pickOnBounds="false">
|
||||
<StackPane fx:id="header" VBox.vgrow="ALWAYS" pickOnBounds="false">
|
||||
<StackPane fx:id="header" VBox.vgrow="ALWAYS" pickOnBounds="false" style="-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;">
|
||||
<BorderPane>
|
||||
<top>
|
||||
<HBox alignment="CENTER_RIGHT">
|
||||
</HBox>
|
||||
</top>
|
||||
<center>
|
||||
<VBox style="-fx-padding: 20 20 0 20;">
|
||||
<VBox style="-fx-padding: 8 8 0 8;">
|
||||
<Label fx:id="lblVersionName" style="-fx-font-size: 15;" textAlignment="JUSTIFY" wrapText="true" />
|
||||
<Label fx:id="lblGameVersion" style="-fx-font-size: 10;" textAlignment="JUSTIFY" wrapText="true" />
|
||||
</VBox>
|
||||
@@ -35,7 +35,7 @@
|
||||
</HBox>
|
||||
</left>
|
||||
<right>
|
||||
<JFXRadioButton fx:id="chkSelected" BorderPane.alignment="CENTER_RIGHT" />
|
||||
<JFXButton fx:id="btnLaunch" styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minHeight="30" minWidth="30" prefWidth="30" prefHeight="30" />
|
||||
</right>
|
||||
</BorderPane>
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<StackPane xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="org.jackhuang.hmcl.ui.VersionSettingsController">
|
||||
<ScrollPane fx:id="scroll" fitToHeight="true" fitToWidth="true">
|
||||
<ScrollPane fx:id="scroll" fitToHeight="true" fitToWidth="true" vbarPolicy="ALWAYS">
|
||||
<VBox fx:id="rootPane" style="-fx-padding: 20;">
|
||||
|
||||
<ComponentList depth="1">
|
||||
@@ -109,7 +109,7 @@
|
||||
</BorderPane>
|
||||
</ComponentList>
|
||||
<HBox alignment="CENTER" style="-fx-padding: 10 0 10 0;">
|
||||
<JFXButton text="%advancedsettings" onMouseClicked="#onShowAdvanced"/>
|
||||
<JFXButton text="%advancedsettings" onMouseClicked="#onShowAdvanced" buttonType="RAISED" styleClass="jfx-button-raised"/>
|
||||
</HBox>
|
||||
<ComponentList fx:id="advancedSettingsPane" depth="1">
|
||||
<JFXTextField labelFloat="true" promptText="%advancedsettings.jvm_args" styleClass="fit-width"
|
||||
|
||||
@@ -28,11 +28,11 @@ public abstract class Account {
|
||||
|
||||
public abstract String getUsername();
|
||||
|
||||
public AuthInfo logIn() throws AuthenticationException {
|
||||
return logIn(Proxy.NO_PROXY);
|
||||
public AuthInfo logIn(MultiCharacterSelector selector) throws AuthenticationException {
|
||||
return logIn(selector, Proxy.NO_PROXY);
|
||||
}
|
||||
|
||||
public abstract AuthInfo logIn(Proxy proxy) throws AuthenticationException;
|
||||
public abstract AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException;
|
||||
|
||||
public abstract void logOut();
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This interface is for your application to open a GUI for user to choose the character
|
||||
* when a having-multi-character yggdrasil account is being logging in..
|
||||
*/
|
||||
public interface MultiCharacterSelector {
|
||||
|
||||
/**
|
||||
* Select one of {@code names} GameProfiles to login.
|
||||
* @param names available game profiles.
|
||||
* @throws NoSelectedCharacterException if cannot select any character may because user close the selection window or cancel the selection.
|
||||
* @return your choice of game profile.
|
||||
*/
|
||||
GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException;
|
||||
|
||||
MultiCharacterSelector DEFAULT = (account, names) -> names.stream().findFirst().orElseThrow(() -> new NoSelectedCharacterException(account));
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
/**
|
||||
* This exception gets threw when authenticating a yggdrasil account and there is no valid character.
|
||||
* (A account may hold more than one characters.)
|
||||
*/
|
||||
public final class NoCharacterException extends AuthenticationException {
|
||||
private final Account account;
|
||||
|
||||
public NoCharacterException(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
/**
|
||||
* This exception gets threw when a monitor of {@link MultiCharacterSelector} cannot select a
|
||||
* valid character.
|
||||
*
|
||||
* @see org.jackhuang.hmcl.auth.MultiCharacterSelector
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class NoSelectedCharacterException extends AuthenticationException {
|
||||
private final Account account;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param account the error yggdrasil account.
|
||||
*/
|
||||
public NoSelectedCharacterException(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public class OfflineAccount extends Account {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthInfo logIn(Proxy proxy) throws AuthenticationException {
|
||||
public AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException {
|
||||
if (StringUtils.isBlank(username) || StringUtils.isBlank(uuid))
|
||||
throw new AuthenticationException("Username cannot be empty");
|
||||
|
||||
|
||||
@@ -24,13 +24,16 @@ import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huang
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class GameProfile {
|
||||
|
||||
private final UUID id;
|
||||
|
||||
@@ -23,14 +23,9 @@ import com.google.gson.JsonParseException;
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.UserType;
|
||||
import java.util.*;
|
||||
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.util.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.UUIDTypeAdapter;
|
||||
@@ -111,7 +106,7 @@ public final class YggdrasilAccount extends Account {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthInfo logIn(Proxy proxy) throws AuthenticationException {
|
||||
public AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException {
|
||||
if (canPlayOnline())
|
||||
return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties));
|
||||
else {
|
||||
@@ -119,11 +114,13 @@ public final class YggdrasilAccount extends Account {
|
||||
if (!isLoggedIn())
|
||||
throw new AuthenticationException("Wrong password for account " + username);
|
||||
|
||||
if (selectedProfile == null)
|
||||
// TODO: multi-available-profiles support
|
||||
throw new UnsupportedOperationException("Do not support multi-available-profiles account yet.");
|
||||
else
|
||||
return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties));
|
||||
if (selectedProfile == null) {
|
||||
if (profiles == null || profiles.length <= 0)
|
||||
throw new NoCharacterException(this);
|
||||
|
||||
selectedProfile = selector.select(this, Arrays.asList(profiles));
|
||||
}
|
||||
return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.util.Immutable;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class Install {
|
||||
public final class ForgeInstall {
|
||||
|
||||
private final String profileName;
|
||||
private final String target;
|
||||
@@ -36,11 +36,11 @@ public final class Install {
|
||||
private final String mirrorList;
|
||||
private final String logo;
|
||||
|
||||
public Install() {
|
||||
public ForgeInstall() {
|
||||
this(null, null, null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public Install(String profileName, String target, String path, String version, String filePath, String welcome, String minecraft, String mirrorList, String logo) {
|
||||
public ForgeInstall(String profileName, String target, String path, String version, String filePath, String welcome, String minecraft, String mirrorList, String logo) {
|
||||
this.profileName = profileName;
|
||||
this.target = target;
|
||||
this.path = path;
|
||||
@@ -28,20 +28,20 @@ import org.jackhuang.hmcl.util.Validation;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class InstallProfile implements Validation {
|
||||
public final class ForgeInstallProfile implements Validation {
|
||||
|
||||
@SerializedName("install")
|
||||
private final Install install;
|
||||
private final ForgeInstall install;
|
||||
|
||||
@SerializedName("versionInfo")
|
||||
private final Version versionInfo;
|
||||
|
||||
public InstallProfile(Install install, Version versionInfo) {
|
||||
public ForgeInstallProfile(ForgeInstall install, Version versionInfo) {
|
||||
this.install = install;
|
||||
this.versionInfo = versionInfo;
|
||||
}
|
||||
|
||||
public Install getInstall() {
|
||||
public ForgeInstall getInstall() {
|
||||
return install;
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ public final class ForgeInstallTask extends TaskResult<Version> {
|
||||
if (stream == null)
|
||||
throw new IOException("Malformed forge installer file, install_profile.json does not exist.");
|
||||
String json = IOUtils.readFullyAsString(stream);
|
||||
InstallProfile installProfile = Constants.GSON.fromJson(json, InstallProfile.class);
|
||||
ForgeInstallProfile installProfile = Constants.GSON.fromJson(json, ForgeInstallProfile.class);
|
||||
if (installProfile == null)
|
||||
throw new IOException("Malformed forge installer file, install_profile.json does not exist.");
|
||||
|
||||
|
||||
@@ -57,14 +57,13 @@ public final class GameLibrariesTask extends Task {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
for (Library library : version.getLibraries())
|
||||
if (library.appliesToCurrentEnvironment()) {
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
|
||||
if (!file.exists())
|
||||
dependencies.add(new FileDownloadTask(
|
||||
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(library.getDownload().getUrl())),
|
||||
file, dependencyManager.getProxy(), library.getDownload().getSha1()));
|
||||
}
|
||||
version.getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
||||
File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
|
||||
if (!file.exists())
|
||||
dependencies.add(new FileDownloadTask(
|
||||
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(library.getDownload().getUrl())),
|
||||
file, dependencyManager.getProxy(), library.getDownload().getSha1()));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public final class GameRemoteVersions {
|
||||
private final GameRemoteLatestVersions latest;
|
||||
|
||||
public GameRemoteVersions() {
|
||||
this(Collections.EMPTY_LIST, null);
|
||||
this(Collections.emptyList(), null);
|
||||
}
|
||||
|
||||
public GameRemoteVersions(List<GameRemoteVersion> versions, GameRemoteLatestVersions latest) {
|
||||
|
||||
@@ -61,7 +61,7 @@ public final class VersionJsonDownloadTask extends Task {
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
RemoteVersion<?> remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Cannot find specific version "+gameVersion+" in remote repository"));
|
||||
.orElseThrow(() -> new IllegalStateException("Cannot find specific version " + gameVersion + " in remote repository"));
|
||||
String jsonURL = dependencyManager.getDownloadProvider().injectURL(remoteVersion.getUrl());
|
||||
dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL), Proxy.NO_PROXY, ID));
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public final class LiteLoaderBranch {
|
||||
private final Map<String, LiteLoaderVersion> liteLoader;
|
||||
|
||||
public LiteLoaderBranch() {
|
||||
this(Collections.EMPTY_SET, Collections.EMPTY_MAP);
|
||||
this(Collections.emptySet(), Collections.emptyMap());
|
||||
}
|
||||
|
||||
public LiteLoaderBranch(Collection<Library> libraries, Map<String, LiteLoaderVersion> liteLoader) {
|
||||
|
||||
@@ -96,11 +96,11 @@ public final class LiteLoaderInstallTask extends TaskResult<Version> {
|
||||
new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl()))
|
||||
);
|
||||
|
||||
Version tempVersion = version.setLibraries(Lang.merge(remote.getTag().getLibraries(), Arrays.asList(library)));
|
||||
Version tempVersion = version.setLibraries(Lang.merge(remote.getTag().getLibraries(), Collections.singleton(library)));
|
||||
setResult(version
|
||||
.setMainClass("net.minecraft.launchwrapper.Launch")
|
||||
.setLibraries(Lang.merge(tempVersion.getLibraries(), version.getLibraries()))
|
||||
.setLogging(Collections.EMPTY_MAP)
|
||||
.setLogging(Collections.emptyMap())
|
||||
.setMinecraftArguments(version.getMinecraftArguments().orElse("") + " --tweakClass " + remote.getTag().getTweakClass())
|
||||
//.setArguments(Arguments.addGameArguments(Lang.get(version.getArguments()), "--tweakClass", remote.getTag().getTweakClass()))
|
||||
);
|
||||
|
||||
@@ -113,7 +113,7 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
|
||||
hasFMLTweaker = true;
|
||||
if (version.getArguments().isPresent()) {
|
||||
List<Argument> game = version.getArguments().get().getGame();
|
||||
if (game.stream().anyMatch(arg -> arg.toString(Collections.EMPTY_MAP, Collections.EMPTY_MAP).contains("FMLTweaker")))
|
||||
if (game.stream().anyMatch(arg -> arg.toString(Collections.emptyMap(), Collections.emptyMap()).contains("FMLTweaker")))
|
||||
hasFMLTweaker = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,10 +80,13 @@ public final class OptiFineVersionList extends VersionList<Void> {
|
||||
if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineFile"))
|
||||
version = td.getTextContent();
|
||||
}
|
||||
if (version == null || url == null)
|
||||
continue;
|
||||
|
||||
Matcher matcher = PATTERN.matcher(version);
|
||||
while (matcher.find())
|
||||
gameVersion = matcher.group(1);
|
||||
if (gameVersion == null || version == null || url == null)
|
||||
if (gameVersion == null)
|
||||
continue;
|
||||
versions.put(gameVersion, new RemoteVersion<>(gameVersion, version, url, null));
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public class Event extends EventObject {
|
||||
return false;
|
||||
}
|
||||
|
||||
private Result result;
|
||||
private Result result = Result.DEFAULT;
|
||||
|
||||
/**
|
||||
* Retutns the value set as the result of this event
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.event;
|
||||
|
||||
import java.util.EventObject;
|
||||
import java.util.HashMap;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
|
||||
@@ -29,14 +28,14 @@ public final class EventBus {
|
||||
|
||||
private final HashMap<Class<?>, EventManager<?>> events = new HashMap<>();
|
||||
|
||||
public <T extends EventObject> EventManager<T> channel(Class<T> clazz) {
|
||||
public <T extends Event> EventManager<T> channel(Class<T> clazz) {
|
||||
if (!events.containsKey(clazz))
|
||||
events.put(clazz, new EventManager<>(Schedulers.computation()));
|
||||
events.put(clazz, new EventManager<>());
|
||||
return (EventManager<T>) events.get(clazz);
|
||||
}
|
||||
|
||||
public void fireEvent(EventObject obj) {
|
||||
channel((Class<EventObject>) obj.getClass()).fireEvent(obj);
|
||||
public Event.Result fireEvent(Event obj) {
|
||||
return channel((Class<Event>) obj.getClass()).fireEvent(obj);
|
||||
}
|
||||
|
||||
public static final EventBus EVENT_BUS = new EventBus();
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.event;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.EventObject;
|
||||
import java.util.HashSet;
|
||||
import java.util.function.Consumer;
|
||||
import org.jackhuang.hmcl.task.Scheduler;
|
||||
@@ -29,22 +28,13 @@ import org.jackhuang.hmcl.util.SimpleMultimap;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class EventManager<T extends EventObject> {
|
||||
public final class EventManager<T extends Event> {
|
||||
|
||||
private final Scheduler scheduler;
|
||||
private final SimpleMultimap<EventPriority, Consumer<T>> handlers
|
||||
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), HashSet::new);
|
||||
private final SimpleMultimap<EventPriority, Runnable> handlers2
|
||||
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), HashSet::new);
|
||||
|
||||
public EventManager() {
|
||||
this(Schedulers.immediate());
|
||||
}
|
||||
|
||||
public EventManager(Scheduler scheduler) {
|
||||
this.scheduler = scheduler;
|
||||
}
|
||||
|
||||
public void register(Consumer<T> consumer) {
|
||||
register(consumer, EventPriority.NORMAL);
|
||||
}
|
||||
@@ -71,15 +61,18 @@ public final class EventManager<T extends EventObject> {
|
||||
handlers2.removeValue(runnable);
|
||||
}
|
||||
|
||||
public void fireEvent(T event) {
|
||||
scheduler.schedule(() -> {
|
||||
for (EventPriority priority : EventPriority.values()) {
|
||||
for (Consumer<T> handler : handlers.get(priority))
|
||||
handler.accept(event);
|
||||
for (Runnable runnable : handlers2.get(priority))
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
public Event.Result fireEvent(T event) {
|
||||
for (EventPriority priority : EventPriority.values()) {
|
||||
for (Consumer<T> handler : handlers.get(priority))
|
||||
handler.accept(event);
|
||||
for (Runnable runnable : handlers2.get(priority))
|
||||
runnable.run();
|
||||
}
|
||||
|
||||
if (event.hasResult())
|
||||
return event.getResult();
|
||||
else
|
||||
return Event.Result.DEFAULT;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import java.util.EventObject;
|
||||
*
|
||||
* @author huang
|
||||
*/
|
||||
public class FailedEvent<T> extends EventObject {
|
||||
public class FailedEvent<T> extends Event {
|
||||
|
||||
private final int failedTime;
|
||||
private T newResult;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.event;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* This event gets fired when json of a game version is malformed. You can do something here.
|
||||
* auto making up for the missing json, don't forget to set result to {@link Event.Result#ALLOW}.
|
||||
* and even asking for removing the redundant version folder.
|
||||
*
|
||||
* The result ALLOW means you have corrected the json.
|
||||
*/
|
||||
public final class GameJsonParseFailedEvent extends Event {
|
||||
private final String version;
|
||||
private final File jsonFile;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.game.DefaultGameRepository}
|
||||
* @param jsonFile the minecraft.json file.
|
||||
* @param version the version name
|
||||
*/
|
||||
public GameJsonParseFailedEvent(Object source, File jsonFile, String version) {
|
||||
super(source);
|
||||
this.version = version;
|
||||
this.jsonFile = jsonFile;
|
||||
}
|
||||
|
||||
public File getJsonFile() {
|
||||
return jsonFile;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -23,16 +23,20 @@ import org.jackhuang.hmcl.util.ManagedProcess;
|
||||
/**
|
||||
* This event gets fired when we launch the JVM and it got crashed.
|
||||
* <br>
|
||||
* This event is fired on the [org.jackhuang.hmcl.event.EVENT_BUS]
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
|
||||
*
|
||||
* @param source [org.jackhuang.hmcl.launch.ExitWaiter]
|
||||
* @param value the crashed process.
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class JVMLaunchFailedEvent extends EventObject {
|
||||
public class JVMLaunchFailedEvent extends Event {
|
||||
|
||||
private final ManagedProcess process;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
|
||||
* @param process the crashed process.
|
||||
*/
|
||||
public JVMLaunchFailedEvent(Object source, ManagedProcess process) {
|
||||
super(source);
|
||||
this.process = process;
|
||||
|
||||
@@ -17,28 +17,37 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.event;
|
||||
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
|
||||
import java.util.EventObject;
|
||||
|
||||
/**
|
||||
* This event gets fired when a minecraft version has been loaded.
|
||||
* <br>
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EVENT_BUS}
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.game.GameRepository}
|
||||
* @param version the version id.
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class LoadedOneVersionEvent extends EventObject {
|
||||
public final class LoadedOneVersionEvent extends Event {
|
||||
|
||||
private final String version;
|
||||
private final Version version;
|
||||
|
||||
public LoadedOneVersionEvent(Object source, String version) {
|
||||
/**
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.game.GameRepository}
|
||||
* @param version the version id.
|
||||
*/
|
||||
public LoadedOneVersionEvent(Object source, Version version) {
|
||||
super(source);
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
public Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasResult() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,16 +23,20 @@ import org.jackhuang.hmcl.util.ManagedProcess;
|
||||
/**
|
||||
* This event gets fired when a JavaProcess exited abnormally and the exit code is not zero.
|
||||
* <br></br>
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EVENT_BUS}
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
|
||||
* @param value The process that exited abnormally.
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class ProcessExitedAbnormallyEvent extends EventObject {
|
||||
public final class ProcessExitedAbnormallyEvent extends Event {
|
||||
|
||||
private final ManagedProcess process;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
|
||||
* @param process The process that exited abnormally.
|
||||
*/
|
||||
public ProcessExitedAbnormallyEvent(Object source, ManagedProcess process) {
|
||||
super(source);
|
||||
this.process = process;
|
||||
|
||||
@@ -23,16 +23,20 @@ import org.jackhuang.hmcl.util.ManagedProcess;
|
||||
/**
|
||||
* This event gets fired when minecraft process exited successfully and the exit code is 0.
|
||||
* <br>
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EVENT_BUS}
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
|
||||
* @param value minecraft process
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class ProcessStoppedEvent extends EventObject {
|
||||
public class ProcessStoppedEvent extends Event {
|
||||
|
||||
private final ManagedProcess process;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
|
||||
* @param process minecraft process
|
||||
*/
|
||||
public ProcessStoppedEvent(Object source, ManagedProcess process) {
|
||||
super(source);
|
||||
this.process = process;
|
||||
|
||||
@@ -22,14 +22,17 @@ import java.util.EventObject;
|
||||
/**
|
||||
* This event gets fired when all the versions in .minecraft folder are loaded.
|
||||
* <br>
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.api.HMCLApi#EVENT_BUS}
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.game.GameRepository]
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class RefreshedVersionsEvent extends EventObject {
|
||||
public final class RefreshedVersionsEvent extends Event {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.game.GameRepository]
|
||||
*/
|
||||
public RefreshedVersionsEvent(Object source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
@@ -22,14 +22,17 @@ import java.util.EventObject;
|
||||
/**
|
||||
* This event gets fired when loading versions in a .minecraft folder.
|
||||
* <br>
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EVENT_BUS}
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.game.GameRepository}
|
||||
* This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class RefreshingVersionsEvent extends EventObject {
|
||||
public final class RefreshingVersionsEvent extends Event {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param source {@link org.jackhuang.hmcl.game.GameRepository}
|
||||
*/
|
||||
public RefreshingVersionsEvent(Object source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
@@ -50,11 +50,11 @@ public final class Arguments {
|
||||
}
|
||||
|
||||
public List<Argument> getGame() {
|
||||
return game == null ? Collections.EMPTY_LIST : Collections.unmodifiableList(game);
|
||||
return game == null ? Collections.emptyList() : Collections.unmodifiableList(game);
|
||||
}
|
||||
|
||||
public List<Argument> getJvm() {
|
||||
return jvm == null ? Collections.EMPTY_LIST : Collections.unmodifiableList(jvm);
|
||||
return jvm == null ? Collections.emptyList() : Collections.unmodifiableList(jvm);
|
||||
}
|
||||
|
||||
public static Arguments addGameArguments(Arguments arguments, String... gameArguments) {
|
||||
@@ -83,7 +83,7 @@ public final class Arguments {
|
||||
}
|
||||
|
||||
public static List<String> parseArguments(List<Argument> arguments, Map<String, String> keys) {
|
||||
return parseArguments(arguments, keys, Collections.EMPTY_MAP);
|
||||
return parseArguments(arguments, keys, Collections.emptyMap());
|
||||
}
|
||||
|
||||
public static List<String> parseArguments(List<Argument> arguments, Map<String, String> keys, Map<String, Boolean> features) {
|
||||
|
||||
@@ -35,7 +35,7 @@ public final class AssetIndex {
|
||||
private final Map<String, AssetObject> objects;
|
||||
|
||||
public AssetIndex() {
|
||||
this(false, Collections.EMPTY_MAP);
|
||||
this(false, Collections.emptyMap());
|
||||
}
|
||||
|
||||
public AssetIndex(boolean virtual, Map<String, AssetObject> objects) {
|
||||
|
||||
@@ -21,16 +21,11 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.LoadedOneVersionEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
|
||||
|
||||
import org.jackhuang.hmcl.event.*;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
@@ -94,7 +89,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
@Override
|
||||
public File getVersionJar(Version version) {
|
||||
Version v = version.resolve(this);
|
||||
String id = Lang.nonNull(v.getJar(), v.getId());
|
||||
String id = Optional.ofNullable(v.getJar()).orElse(v.getId());
|
||||
return new File(getVersionRoot(id), id + ".jar");
|
||||
}
|
||||
|
||||
@@ -184,9 +179,15 @@ public class DefaultGameRepository implements GameRepository {
|
||||
version = Objects.requireNonNull(readVersionJson(json));
|
||||
} catch (Exception e) {
|
||||
// JsonSyntaxException or IOException or NullPointerException(!!)
|
||||
// TODO: auto making up for the missing json
|
||||
// TODO: and even asking for removing the redundant version folder.
|
||||
continue;
|
||||
if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
|
||||
continue;
|
||||
|
||||
try {
|
||||
version = Objects.requireNonNull(readVersionJson(json));
|
||||
} catch (Exception e2) {
|
||||
Logging.LOG.log(Level.SEVERE, "User corrected version json is still malformed");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!id.equals(version.getId())) {
|
||||
@@ -199,18 +200,20 @@ public class DefaultGameRepository implements GameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
versions.put(id, version);
|
||||
EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, id));
|
||||
if (EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, version)) != Event.Result.DENY)
|
||||
versions.put(id, version);
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void refreshVersions() {
|
||||
public void refreshVersions() {
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this));
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
Schedulers.newThread().schedule(() -> {
|
||||
refreshVersionsImpl();
|
||||
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -29,6 +29,8 @@ import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.OperatingSystem;
|
||||
@@ -99,7 +101,7 @@ public class Library {
|
||||
+ (this.classifier == null ? "" : "-" + this.classifier) + ".jar";
|
||||
|
||||
download = new LibraryDownloadInfo(path,
|
||||
Lang.nonNull(Lang.nonNull(temp != null ? temp.getUrl() : null), Lang.nonNull(url, Constants.DEFAULT_LIBRARY_URL) + path),
|
||||
Optional.ofNullable(temp).map(LibraryDownloadInfo::getUrl).orElse(Optional.ofNullable(url).orElse(Constants.DEFAULT_LIBRARY_URL) + path),
|
||||
temp != null ? temp.getSha1() : null,
|
||||
temp != null ? temp.getSize() : 0
|
||||
);
|
||||
|
||||
@@ -75,7 +75,7 @@ public class RuledArgument implements Argument {
|
||||
.map(StringArgument::new)
|
||||
.map(str -> str.toString(keys, features).get(0))
|
||||
.collect(Collectors.toList());
|
||||
return Collections.EMPTY_LIST;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<RuledArgument>, JsonDeserializer<RuledArgument> {
|
||||
|
||||
@@ -155,8 +155,6 @@ public class Version implements Comparable<Version>, Validation {
|
||||
|
||||
/**
|
||||
* Resolve given version
|
||||
*
|
||||
* @throws CircleDependencyException
|
||||
*/
|
||||
public Version resolve(VersionProvider provider) {
|
||||
return resolve(provider, new HashSet<>());
|
||||
@@ -231,10 +229,7 @@ public class Version implements Comparable<Version>, Validation {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Version)
|
||||
return Objects.equals(id, ((Version) obj).id);
|
||||
else
|
||||
return false;
|
||||
return obj instanceof Version && Objects.equals(id, ((Version) obj).id);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,11 +22,7 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
@@ -207,7 +203,7 @@ public class DefaultLauncher extends Launcher {
|
||||
.filter(it -> !getForbiddens().containsKey(it) || !getForbiddens().get(it).get())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
//http://jenkins.liteloader.com/job/LiteLoader%201.12.2/lastSuccessfulBuild/artifact/build/libs/liteloader-1.12.2-SNAPSHOT-release.jar
|
||||
|
||||
public Map<String, Boolean> getFeatures() {
|
||||
return Collections.singletonMap(
|
||||
"has_custom_resolution",
|
||||
@@ -251,14 +247,14 @@ public class DefaultLauncher extends Launcher {
|
||||
false);
|
||||
}
|
||||
|
||||
public Map<String, String> getConfigurations() {
|
||||
protected Map<String, String> getConfigurations() {
|
||||
return Lang.mapOf(
|
||||
new Pair<>("${auth_player_name}", authInfo.getUsername()),
|
||||
new Pair<>("${auth_session}", authInfo.getAuthToken()),
|
||||
new Pair<>("${auth_access_token}", authInfo.getAuthToken()),
|
||||
new Pair<>("${auth_uuid}", authInfo.getUserId()),
|
||||
new Pair<>("${version_name}", Lang.nonNull(options.getVersionName(), version.getId())),
|
||||
new Pair<>("${profile_name}", Lang.nonNull(options.getProfileName(), "Minecraft")),
|
||||
new Pair<>("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())),
|
||||
new Pair<>("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")),
|
||||
new Pair<>("${version_type}", version.getType().getId()),
|
||||
new Pair<>("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()),
|
||||
new Pair<>("${user_type}", authInfo.getUserType().toString().toLowerCase()),
|
||||
@@ -275,11 +271,8 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
decompressNatives();
|
||||
|
||||
if (StringUtils.isNotBlank(options.getPrecalledCommand())) {
|
||||
Process process = Runtime.getRuntime().exec(options.getPrecalledCommand());
|
||||
if (process.isAlive())
|
||||
process.waitFor();
|
||||
}
|
||||
if (StringUtils.isNotBlank(options.getPrecalledCommand()))
|
||||
Runtime.getRuntime().exec(options.getPrecalledCommand()).waitFor();
|
||||
|
||||
builder.directory(repository.getRunDirectory(version.getId()))
|
||||
.environment().put("APPDATA", options.getGameDir().getAbsoluteFile().getParent());
|
||||
@@ -372,7 +365,7 @@ public class DefaultLauncher extends Launcher {
|
||||
private void startMonitorsWithoutLoggingInfo(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
|
||||
processListener.setProcess(managedProcess);
|
||||
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> {
|
||||
processListener.onLog(it + OperatingSystem.LINE_SEPARATOR, Lang.nonNull(Log4jLevel.guessLevel(it), Log4jLevel.INFO));
|
||||
processListener.onLog(it + OperatingSystem.LINE_SEPARATOR, Optional.ofNullable(Log4jLevel.guessLevel(it)).orElse(Log4jLevel.INFO));
|
||||
managedProcess.addLine(it);
|
||||
}), "stdout-pump", isDaemon);
|
||||
managedProcess.addRelatedThread(stdout);
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.launch;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.io.PipedInputStream;
|
||||
import java.io.PipedOutputStream;
|
||||
import java.io.*;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -55,6 +53,7 @@ final class Log4jHandler extends Thread {
|
||||
|
||||
public Log4jHandler(BiConsumer<String, Log4jLevel> callback) {
|
||||
this.callback = callback;
|
||||
newLine("<output>");
|
||||
|
||||
reader = Lang.invoke(() -> XMLReaderFactory.createXMLReader());
|
||||
reader.setContentHandler(new Log4jHandlerImpl());
|
||||
@@ -63,7 +62,6 @@ final class Log4jHandler extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
setName("log4j-handler");
|
||||
newLine("<output>");
|
||||
|
||||
try {
|
||||
reader.parse(new InputSource(inputStream));
|
||||
@@ -88,17 +86,14 @@ final class Log4jHandler extends Thread {
|
||||
}).get());
|
||||
}
|
||||
|
||||
public Future<?> newLine(String content) {
|
||||
public Future<?> newLine(String log) {
|
||||
return Schedulers.computation().schedule(() -> {
|
||||
String log = content;
|
||||
if (!log.trim().startsWith("<"))
|
||||
log = "<![CDATA[" + log.replace("]]>", "") + "]]>";
|
||||
outputStream.write((log + OperatingSystem.LINE_SEPARATOR)
|
||||
byte[] bytes = (log + OperatingSystem.LINE_SEPARATOR)
|
||||
.replace("log4j:Event", "log4j_Event")
|
||||
.replace("log4j:Message", "log4j_Message")
|
||||
.replace("log4j:Throwable", "log4j_Throwable")
|
||||
.getBytes()
|
||||
);
|
||||
.getBytes();
|
||||
outputStream.write(bytes);
|
||||
outputStream.flush();
|
||||
});
|
||||
}
|
||||
@@ -155,7 +150,7 @@ final class Log4jHandler extends Thread {
|
||||
if (readingMessage)
|
||||
message.append(line).append(OperatingSystem.LINE_SEPARATOR);
|
||||
else
|
||||
callback.accept(line, Lang.nonNull(Log4jLevel.guessLevel(line), Log4jLevel.INFO));
|
||||
callback.accept(line, Optional.ofNullable(Log4jLevel.guessLevel(line)).orElse(Log4jLevel.INFO));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public final class CurseInstallTask extends Task {
|
||||
* @param zipFile the CurseForge modpack file.
|
||||
* @param manifest The manifest content of given CurseForge modpack.
|
||||
* @param name the new version name
|
||||
* @see readCurseForgeModpackManifest
|
||||
* @see CurseManifest#readCurseForgeModpackManifest
|
||||
*/
|
||||
public CurseInstallTask(DefaultDependencyManager dependencyManager, File zipFile, CurseManifest manifest, String name) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
|
||||
@@ -23,6 +23,8 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.jackhuang.hmcl.util.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
@@ -122,7 +124,7 @@ public final class CurseManifest {
|
||||
if (manifest == null)
|
||||
throw new JsonParseException("`manifest.json` not found. Not a valid Curse modpack.");
|
||||
return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(),
|
||||
Lang.nonNull(CompressingUtils.readTextZipEntryQuietly(f, "modlist.html"), "No description"), manifest);
|
||||
Optional.ofNullable(CompressingUtils.readTextZipEntryQuietly(f, "modlist.html")).orElse( "No description"), manifest);
|
||||
}
|
||||
|
||||
public static final String MINECRAFT_MODPACK = "minecraftModpack";
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.mod;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
@@ -83,7 +84,7 @@ public final class MultiMCInstanceConfiguration {
|
||||
showConsoleOnError = Boolean.parseBoolean(p.getProperty("ShowConsoleOnError"));
|
||||
wrapperCommand = p.getProperty("WrapperCommand");
|
||||
name = defaultName;
|
||||
notes = Lang.nonNull(p.getProperty("notes"), "");
|
||||
notes = Optional.ofNullable(p.getProperty("notes")).orElse("");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.task;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import org.jackhuang.hmcl.util.AutoTypingMap;
|
||||
import org.jackhuang.hmcl.util.ExceptionalConsumer;
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -26,16 +27,18 @@ import org.jackhuang.hmcl.util.AutoTypingMap;
|
||||
*/
|
||||
class SimpleTask extends Task {
|
||||
|
||||
private final Consumer<AutoTypingMap<String>> consumer;
|
||||
private final ExceptionalConsumer<AutoTypingMap<String>, ?> consumer;
|
||||
private final Scheduler scheduler;
|
||||
|
||||
public SimpleTask(Consumer<AutoTypingMap<String>> consumer) {
|
||||
public SimpleTask(ExceptionalConsumer<AutoTypingMap<String>, ?> consumer) {
|
||||
this(consumer, Schedulers.defaultScheduler());
|
||||
}
|
||||
|
||||
public SimpleTask(Consumer<AutoTypingMap<String>> consumer, Scheduler scheduler) {
|
||||
public SimpleTask(ExceptionalConsumer<AutoTypingMap<String>, ?> consumer, Scheduler scheduler) {
|
||||
this.consumer = consumer;
|
||||
this.scheduler = scheduler;
|
||||
|
||||
setName(consumer.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -29,6 +29,8 @@ import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import org.jackhuang.hmcl.util.AutoTypingMap;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.util.ExceptionalConsumer;
|
||||
import org.jackhuang.hmcl.util.ExceptionalRunnable;
|
||||
import org.jackhuang.hmcl.util.Properties;
|
||||
|
||||
/**
|
||||
@@ -204,11 +206,11 @@ public abstract class Task {
|
||||
return executor;
|
||||
}
|
||||
|
||||
public final TaskExecutor subscribe(Scheduler scheduler, Consumer<AutoTypingMap<String>> closure) {
|
||||
public final TaskExecutor subscribe(Scheduler scheduler, ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
|
||||
return subscribe(of(closure, scheduler));
|
||||
}
|
||||
|
||||
public final TaskExecutor subscribe(Consumer<AutoTypingMap<String>> closure) {
|
||||
public final TaskExecutor subscribe(ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
|
||||
return subscribe(of(closure));
|
||||
}
|
||||
|
||||
@@ -228,15 +230,15 @@ public abstract class Task {
|
||||
return new CoupleTask<>(this, b, false);
|
||||
}
|
||||
|
||||
public static Task of(Runnable runnable) {
|
||||
public static Task of(ExceptionalRunnable<?> runnable) {
|
||||
return of(s -> runnable.run());
|
||||
}
|
||||
|
||||
public static Task of(Consumer<AutoTypingMap<String>> closure) {
|
||||
public static Task of(ExceptionalConsumer<AutoTypingMap<String>, ?> closure) {
|
||||
return of(closure, Schedulers.defaultScheduler());
|
||||
}
|
||||
|
||||
public static Task of(Consumer<AutoTypingMap<String>> closure, Scheduler scheduler) {
|
||||
public static Task of(ExceptionalConsumer<AutoTypingMap<String>, ?> closure, Scheduler scheduler) {
|
||||
return new SimpleTask(closure, scheduler);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.task;
|
||||
|
||||
import java.util.EventObject;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huang
|
||||
*/
|
||||
public class TaskEvent extends EventObject {
|
||||
public class TaskEvent extends Event {
|
||||
|
||||
private final Task task;
|
||||
private final boolean failed;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher.
|
||||
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public interface ExceptionalConsumer<T, E extends Exception> {
|
||||
void accept(T t) throws E;
|
||||
}
|
||||
@@ -59,7 +59,7 @@ public final class Lang {
|
||||
*
|
||||
* @param <T> type of argument.
|
||||
* @param <R> type of result.
|
||||
* @param supplier your method.
|
||||
* @param function your method.
|
||||
* @return the result of the method to invoke.
|
||||
*/
|
||||
public static <T, R, E extends Exception> R invoke(ExceptionalFunction<T, R, E> function, T t) {
|
||||
@@ -190,19 +190,19 @@ public final class Lang {
|
||||
return convert(map.get(key), clazz, defaultValue);
|
||||
}
|
||||
|
||||
public static <T> List<T> merge(Collection<T>... collections) {
|
||||
public static <T> List<T> merge(Collection<T> a, Collection<T> b) {
|
||||
LinkedList<T> result = new LinkedList<>();
|
||||
for (Collection<T> collection : collections)
|
||||
if (collection != null)
|
||||
result.addAll(collection);
|
||||
result.addAll(a);
|
||||
result.addAll(b);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T> T nonNull(T... t) {
|
||||
for (T a : t)
|
||||
if (a != null)
|
||||
return a;
|
||||
return null;
|
||||
public static <T> List<T> merge(Collection<T> a, Collection<T> b, Collection<T> c) {
|
||||
LinkedList<T> result = new LinkedList<>();
|
||||
result.addAll(a);
|
||||
result.addAll(b);
|
||||
result.addAll(c);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Thread thread(Runnable runnable) {
|
||||
@@ -223,10 +223,6 @@ public final class Lang {
|
||||
return thread;
|
||||
}
|
||||
|
||||
public static <T> T get(Optional<T> optional) {
|
||||
return optional.isPresent() ? optional.get() : null;
|
||||
}
|
||||
|
||||
public static <T> Iterator<T> asIterator(Enumeration<T> enumeration) {
|
||||
return new Iterator<T>() {
|
||||
@Override
|
||||
@@ -244,4 +240,12 @@ public final class Lang {
|
||||
public static <T> Iterable<T> asIterable(Enumeration<T> enumeration) {
|
||||
return () -> asIterator(enumeration);
|
||||
}
|
||||
|
||||
public static int parseInt(String string, int defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(string);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,12 +65,12 @@ public enum OperatingSystem {
|
||||
/**
|
||||
* The total memory/MB this computer have.
|
||||
*/
|
||||
public static final long TOTAL_MEMORY;
|
||||
public static final int TOTAL_MEMORY;
|
||||
|
||||
/**
|
||||
* The suggested memory size/MB for Minecraft to allocate.
|
||||
*/
|
||||
public static final long SUGGESTED_MEMORY;
|
||||
public static final int SUGGESTED_MEMORY;
|
||||
|
||||
public static final String PATH_SEPARATOR = File.pathSeparator;
|
||||
public static final String FILE_SEPARATOR = File.separator;
|
||||
@@ -104,11 +104,11 @@ public enum OperatingSystem {
|
||||
|
||||
Object bytes = ReflectionHelper.call(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize");
|
||||
if (bytes instanceof Long)
|
||||
TOTAL_MEMORY = ((Long) bytes) / 1024 / 1024;
|
||||
TOTAL_MEMORY = (int) (((Long) bytes) / 1024 / 1024);
|
||||
else
|
||||
TOTAL_MEMORY = 1024;
|
||||
|
||||
SUGGESTED_MEMORY = Math.round(1.0 * TOTAL_MEMORY / 4.0 / 128.0) * 128;
|
||||
SUGGESTED_MEMORY = (int) (Math.round(1.0 * TOTAL_MEMORY / 4.0 / 128.0) * 128);
|
||||
|
||||
String arch = System.getProperty("sun.arch.data.model");
|
||||
if (arch == null)
|
||||
|
||||
@@ -29,7 +29,7 @@ public abstract class VersionNumber implements Comparable<VersionNumber> {
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException if there are some characters excluding digits and dots.
|
||||
* @param version
|
||||
* @param version version string in form x.x.x
|
||||
* @return the int version number
|
||||
*/
|
||||
public static IntVersionNumber asIntVersionNumber(String version) {
|
||||
|
||||
Reference in New Issue
Block a user