Make background transparent?

This commit is contained in:
huangyuhui
2018-01-09 00:03:34 +08:00
parent a40c5fdd40
commit 4b65d4da06
80 changed files with 2201 additions and 1406 deletions

View 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));
}
}
}

View File

@@ -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);
}
}

View File

@@ -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\"}";
}

View 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);
}
}
}

View 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
}

View 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;
}
}
}

View File

@@ -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/&lt;version&gt;/<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);
}
}
}

View 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()");
}
}
}

View File

@@ -17,6 +17,7 @@
*/ */
package org.jackhuang.hmcl package org.jackhuang.hmcl
import org.jackhuang.hmcl.event.Event
import org.jackhuang.hmcl.setting.Profile import org.jackhuang.hmcl.setting.Profile
import java.util.* import java.util.*
@@ -30,7 +31,7 @@ import java.util.*
* * * *
* @author huangyuhui * @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. * This event gets fired when loading profiles.
@@ -40,4 +41,4 @@ class ProfileChangedEvent(source: Any, val value: Profile) : EventObject(source)
* * * *
* @author huangyuhui * @author huangyuhui
*/ */
class ProfileLoadingEvent(source: Any) : EventObject(source) class ProfileLoadingEvent(source: Any) : Event(source)

View File

@@ -57,10 +57,10 @@ class Main : Application() {
companion object { companion object {
val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@" @JvmField val VERSION = "@HELLO_MINECRAFT_LAUNCHER_VERSION_FOR_GRADLE_REPLACING@"
val NAME = "HMCL" @JvmField val NAME = "HMCL"
val TITLE = "$NAME $VERSION" @JvmField val TITLE = "$NAME $VERSION"
val APPDATA = getWorkingDirectory("hmcl") @JvmField val APPDATA = getWorkingDirectory("hmcl")
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {

View File

@@ -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)
}
}

View File

@@ -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()
}
}

View File

@@ -17,18 +17,13 @@
*/ */
package org.jackhuang.hmcl.game package org.jackhuang.hmcl.game
import org.jackhuang.hmcl.Main import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.AuthInfo import org.jackhuang.hmcl.auth.MultiCharacterSelector
import org.jackhuang.hmcl.launch.DefaultLauncher import org.jackhuang.hmcl.auth.NoSelectedCharacterException
import org.jackhuang.hmcl.launch.ProcessListener import org.jackhuang.hmcl.auth.yggdrasil.GameProfile
class HMCLGameLauncher(repository: GameRepository, versionId: String, account: AuthInfo, options: LaunchOptions, listener: ProcessListener? = null, isDaemon: Boolean = true) object HMCLMultiCharacterSelector : MultiCharacterSelector {
: DefaultLauncher(repository, versionId, account, options, listener, isDaemon) { override fun select(account: Account, names: MutableList<GameProfile>): GameProfile {
return names.firstOrNull() ?: throw NoSelectedCharacterException(account)
override fun appendJvmArgs(res: MutableList<String>) {
super.appendJvmArgs(res)
res.add("-Dminecraft.launcher.version=" + Main.VERSION);
res.add("-Dminecraft.launcher.brand=" + Main.NAME);
} }
} }

View File

@@ -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
}
}

View File

@@ -51,7 +51,7 @@ fun readModpackManifest(f: File): Modpack {
} }
fun MultiMCInstanceConfiguration.toVersionSetting(vs: VersionSetting) { fun MultiMCInstanceConfiguration.toVersionSetting(vs: VersionSetting) {
vs.usesGlobal = false vs.isUsesGlobal = false
vs.gameDirType = EnumGameDirectory.VERSION_FOLDER vs.gameDirType = EnumGameDirectory.VERSION_FOLDER
if (isOverrideJavaLocation) { if (isOverrideJavaLocation) {
@@ -67,7 +67,7 @@ fun MultiMCInstanceConfiguration.toVersionSetting(vs: VersionSetting) {
if (isOverrideCommands) { if (isOverrideCommands) {
vs.wrapper = wrapperCommand.orEmpty() vs.wrapper = wrapperCommand.orEmpty()
vs.precalledCommand = preLaunchCommand.orEmpty() vs.preLaunchCommand = preLaunchCommand.orEmpty()
} }
if (isOverrideJavaArgs) { if (isOverrideJavaArgs) {
@@ -75,11 +75,11 @@ fun MultiMCInstanceConfiguration.toVersionSetting(vs: VersionSetting) {
} }
if (isOverrideConsole) { if (isOverrideConsole) {
vs.showLogs = isShowConsole vs.isShowLogs = isShowConsole
} }
if (isOverrideWindow) { if (isOverrideWindow) {
vs.fullscreen = isFullscreen vs.isFullscreen = isFullscreen
if (width != null) if (width != null)
vs.width = width!! vs.width = width!!
if (height != null) if (height != null)

View File

@@ -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)
}
}
}
}

View File

@@ -42,8 +42,8 @@ import java.util.logging.Level
object Settings { object Settings {
val GSON = GsonBuilder() val GSON = GsonBuilder()
.registerTypeAdapter(VersionSetting::class.java, VersionSetting) .registerTypeAdapter(VersionSetting::class.java, VersionSetting.Serializer.INSTANCE)
.registerTypeAdapter(Profile::class.java, Profile) .registerTypeAdapter(Profile::class.java, Profile.Serializer.INSTANCE)
.registerTypeAdapter(File::class.java, FileTypeAdapter.INSTANCE) .registerTypeAdapter(File::class.java, FileTypeAdapter.INSTANCE)
.setPrettyPrinting().create() .setPrettyPrinting().create()

View File

@@ -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/&lt;version&gt;/<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
}
}
}

View File

@@ -28,11 +28,13 @@ import javafx.scene.control.ScrollPane
import javafx.scene.control.ToggleGroup import javafx.scene.control.ToggleGroup
import javafx.scene.layout.StackPane import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.auth.Account import org.jackhuang.hmcl.auth.Account
import org.jackhuang.hmcl.auth.MultiCharacterSelector
import org.jackhuang.hmcl.auth.OfflineAccount import org.jackhuang.hmcl.auth.OfflineAccount
import org.jackhuang.hmcl.auth.OfflineAccountFactory import org.jackhuang.hmcl.auth.OfflineAccountFactory
import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory
import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector
import org.jackhuang.hmcl.i18n import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Schedulers import org.jackhuang.hmcl.task.Schedulers
@@ -133,7 +135,7 @@ class AccountsPage() : StackPane(), DecoratorPage {
else -> throw UnsupportedOperationException() else -> throw UnsupportedOperationException()
} }
account.logIn(Settings.proxy) account.logIn(HMCLMultiCharacterSelector, Settings.proxy)
account account
} catch (e: Exception) { } catch (e: Exception) {
e e

View File

@@ -52,7 +52,7 @@ object Controllers {
decorator.isCustomMaximize = false decorator.isCustomMaximize = false
scene = Scene(decorator, 800.0, 480.0) scene = Scene(decorator, 804.0, 521.0)
scene.stylesheets.addAll(*stylesheets) scene.stylesheets.addAll(*stylesheets)
stage.minWidth = 800.0 stage.minWidth = 800.0
stage.maxWidth = 800.0 stage.maxWidth = 800.0

View File

@@ -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")

View File

@@ -97,9 +97,7 @@ class LeftPaneController(private val leftPane: AdvancedListBox) {
fun onProfilesLoading() { fun onProfilesLoading() {
val list = LinkedList<RipplerContainer>() val list = LinkedList<RipplerContainer>()
Settings.getProfiles().forEach { profile -> Settings.getProfiles().forEach { profile ->
val item = VersionListItem(profile.name).apply { val item = VersionListItem(profile.name)
lblGameVersion.textProperty().bind(profile.selectedVersionProperty)
}
val ripplerContainer = RipplerContainer(item) val ripplerContainer = RipplerContainer(item)
item.onSettingsButtonClicked { item.onSettingsButtonClicked {
Controllers.decorator.showPage(ProfilePage(profile)) Controllers.decorator.showPage(ProfilePage(profile))

View File

@@ -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()")
}
}
}

View File

@@ -21,10 +21,8 @@ import com.jfoenix.controls.JFXButton
import com.jfoenix.controls.JFXMasonryPane import com.jfoenix.controls.JFXMasonryPane
import javafx.application.Platform import javafx.application.Platform
import javafx.beans.property.SimpleStringProperty import javafx.beans.property.SimpleStringProperty
import javafx.beans.property.StringProperty
import javafx.fxml.FXML import javafx.fxml.FXML
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.control.ToggleGroup
import javafx.scene.image.Image import javafx.scene.image.Image
import javafx.scene.layout.StackPane import javafx.scene.layout.StackPane
import org.jackhuang.hmcl.ProfileChangedEvent import org.jackhuang.hmcl.ProfileChangedEvent
@@ -36,11 +34,9 @@ import org.jackhuang.hmcl.game.LauncherHelper
import org.jackhuang.hmcl.i18n import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Profile import org.jackhuang.hmcl.setting.Profile
import org.jackhuang.hmcl.setting.Settings 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.download.DownloadWizardProvider
import org.jackhuang.hmcl.ui.wizard.DecoratorPage import org.jackhuang.hmcl.ui.wizard.DecoratorPage
import org.jackhuang.hmcl.util.channel import org.jackhuang.hmcl.util.channel
import org.jackhuang.hmcl.util.onChange
import org.jackhuang.hmcl.util.plusAssign import org.jackhuang.hmcl.util.plusAssign
/** /**
@@ -49,7 +45,6 @@ import org.jackhuang.hmcl.util.plusAssign
class MainPage : StackPane(), DecoratorPage { class MainPage : StackPane(), DecoratorPage {
override val titleProperty = SimpleStringProperty(this, "title", i18n("launcher.title.main")) override val titleProperty = SimpleStringProperty(this, "title", i18n("launcher.title.main"))
@FXML lateinit var btnLaunch: JFXButton
@FXML lateinit var btnRefresh: JFXButton @FXML lateinit var btnRefresh: JFXButton
@FXML lateinit var btnAdd: JFXButton @FXML lateinit var btnAdd: JFXButton
@FXML lateinit var masonryPane: JFXMasonryPane @FXML lateinit var masonryPane: JFXMasonryPane
@@ -57,32 +52,24 @@ class MainPage : StackPane(), DecoratorPage {
init { init {
loadFXML("/assets/fxml/main.fxml") loadFXML("/assets/fxml/main.fxml")
btnLaunch.graphic = SVG.launch("white", 15.0, 15.0) EventBus.EVENT_BUS.channel<RefreshedVersionsEvent>() += { -> runOnUiThread { loadVersions() } }
btnLaunch.limitWidth(40.0)
btnLaunch.limitHeight(40.0)
EventBus.EVENT_BUS.channel<RefreshedVersionsEvent>() += { -> loadVersions() }
EventBus.EVENT_BUS.channel<ProfileLoadingEvent>() += this::onProfilesLoading EventBus.EVENT_BUS.channel<ProfileLoadingEvent>() += this::onProfilesLoading
EventBus.EVENT_BUS.channel<ProfileChangedEvent>() += this::onProfileChanged EventBus.EVENT_BUS.channel<ProfileChangedEvent>() += this::onProfileChanged
btnAdd.setOnMouseClicked { Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game") } btnAdd.setOnMouseClicked { Controllers.decorator.startWizard(DownloadWizardProvider(), "Install New Game") }
btnRefresh.setOnMouseClicked { Settings.selectedProfile.repository.refreshVersions() } 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 { private fun buildNode(i: Int, profile: Profile, version: String, game: String): Node {
return VersionItem(i, group).apply { return VersionItem().apply {
chkSelected.properties["version"] = version
chkSelected.isSelected = profile.selectedVersion == version
lblGameVersion.text = game lblGameVersion.text = game
lblVersionName.text = version lblVersionName.text = version
btnLaunch.setOnMouseClicked {
if (Settings.selectedAccount == null) {
Controllers.dialog(i18n("login.no_Player007"))
} else
LauncherHelper.INSTANCE.launch(version)
}
btnDelete.setOnMouseClicked { btnDelete.setOnMouseClicked {
profile.repository.removeVersionFromDisk(version) profile.repository.removeVersionFromDisk(version)
Platform.runLater { loadVersions() } Platform.runLater { loadVersions() }
@@ -103,30 +90,15 @@ class MainPage : StackPane(), DecoratorPage {
fun onProfileChanged(event: ProfileChangedEvent) = runOnUiThread { fun onProfileChanged(event: ProfileChangedEvent) = runOnUiThread {
val profile = event.value val profile = event.value
profile.selectedVersionProperty.setChangedListener {
versionChanged(profile.selectedVersion)
}
loadVersions(profile) loadVersions(profile)
} }
private fun loadVersions(profile: Profile = Settings.selectedProfile) { private fun loadVersions(profile: Profile = Settings.selectedProfile) {
val group = ToggleGroup()
val children = mutableListOf<Node>() val children = mutableListOf<Node>()
var i = 0 var i = 0
profile.repository.getVersions().forEach { version -> profile.repository.versions.forEach { version ->
children += buildNode(++i, profile, version.id, minecraftVersion(profile.repository.getVersionJar(version.id)) ?: "Unknown", group) children += buildNode(++i, profile, version.id, minecraftVersion(profile.repository.getVersionJar(version.id)) ?: "Unknown")
}
group.selectedToggleProperty().onChange {
if (it != null)
profile.selectedVersion = it.properties["version"] as String
} }
masonryPane.resetChildren(children) 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 }
}
} }

View File

@@ -75,7 +75,7 @@ class ProfilePage(private val profile: Profile?): StackPane(), DecoratorPage {
if (locationProperty.get().isNullOrBlank()) { if (locationProperty.get().isNullOrBlank()) {
gameDir.onExplore() gameDir.onExplore()
} }
Settings.putProfile(Profile(name = txtProfileName.text, initialGameDir = File(locationProperty.get()))) Settings.putProfile(Profile(txtProfileName.text, File(locationProperty.get())))
} }
Settings.onProfileLoading() Settings.onProfileLoading()

View File

@@ -32,15 +32,15 @@ import javafx.scene.layout.VBox
import javafx.scene.paint.Color import javafx.scene.paint.Color
import java.util.concurrent.Callable import java.util.concurrent.Callable
class VersionItem(i: Int, group: ToggleGroup) : StackPane() { class VersionItem() : StackPane() {
@FXML lateinit var icon: Pane @FXML lateinit var icon: Pane
@FXML lateinit var content: VBox @FXML lateinit var content: VBox
@FXML lateinit var header: StackPane @FXML lateinit var header: StackPane
@FXML lateinit var body: StackPane @FXML lateinit var body: StackPane
@FXML lateinit var btnDelete: JFXButton @FXML lateinit var btnDelete: JFXButton
@FXML lateinit var btnSettings: JFXButton @FXML lateinit var btnSettings: JFXButton
@FXML lateinit var btnLaunch: JFXButton
@FXML lateinit var lblVersionName: Label @FXML lateinit var lblVersionName: Label
@FXML lateinit var chkSelected: JFXRadioButton
@FXML lateinit var lblGameVersion: Label @FXML lateinit var lblGameVersion: Label
@FXML lateinit var iconView: ImageView @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) 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) btnSettings.graphic = SVG.gear("black", 15.0, 15.0)
btnDelete.graphic = SVG.delete("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 // create content
val headerColor = getDefaultColor(i % 12) //val headerColor = getDefaultColor(i % 12)
header.style = "-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor //header.style = "-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor
// create image view // 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) iconView.limitSize(32.0, 32.0)
} }
private fun getDefaultColor(i: Int): String { /*private fun getDefaultColor(i: Int): String {
var color = "#FFFFFF" var color = "#FFFFFF"
when (i) { when (i) {
0 -> color = "#8F3F7E" 0 -> color = "#8F3F7E"
@@ -85,5 +85,5 @@ class VersionItem(i: Int, group: ToggleGroup) : StackPane() {
} }
} }
return color return color
} }*/
} }

View File

@@ -117,39 +117,39 @@ class VersionSettingsController {
this.versionId = versionId this.versionId = versionId
lastVersionSetting?.apply { lastVersionSetting?.apply {
widthProperty.unbind() widthProperty().unbind()
heightProperty.unbind() heightProperty().unbind()
maxMemoryProperty.unbind() maxMemoryProperty().unbind()
javaArgsProperty.unbind() javaArgsProperty().unbind()
minecraftArgsProperty.unbind() minecraftArgsProperty().unbind()
permSizeProperty.unbind() permSizeProperty().unbind()
wrapperProperty.unbind() wrapperProperty().unbind()
precalledCommandProperty.unbind() preLaunchCommandProperty().unbind()
serverIpProperty.unbind() serverIpProperty().unbind()
fullscreenProperty.unbind() fullscreenProperty().unbind()
notCheckGameProperty.unbind() notCheckGameProperty().unbind()
noCommonProperty.unbind() noCommonProperty().unbind()
javaDirProperty.unbind() javaDirProperty().unbind()
showLogsProperty.unbind() showLogsProperty().unbind()
unbindEnum(cboLauncherVisibility) unbindEnum(cboLauncherVisibility)
} }
bindInt(txtWidth, version.widthProperty) bindInt(txtWidth, version.widthProperty())
bindInt(txtHeight, version.heightProperty) bindInt(txtHeight, version.heightProperty())
bindInt(txtMaxMemory, version.maxMemoryProperty) bindInt(txtMaxMemory, version.maxMemoryProperty())
bindString(javaItem.txtCustom, version.javaDirProperty) bindString(javaItem.txtCustom, version.javaDirProperty())
bindString(gameDirItem.txtCustom, version.gameDirProperty) bindString(gameDirItem.txtCustom, version.gameDirProperty())
bindString(txtJVMArgs, version.javaArgsProperty) bindString(txtJVMArgs, version.javaArgsProperty())
bindString(txtGameArgs, version.minecraftArgsProperty) bindString(txtGameArgs, version.minecraftArgsProperty())
bindString(txtMetaspace, version.permSizeProperty) bindString(txtMetaspace, version.permSizeProperty())
bindString(txtWrapper, version.wrapperProperty) bindString(txtWrapper, version.wrapperProperty())
bindString(txtPrecallingCommand, version.precalledCommandProperty) bindString(txtPrecallingCommand, version.preLaunchCommandProperty())
bindString(txtServerIP, version.serverIpProperty) bindString(txtServerIP, version.serverIpProperty())
bindEnum(cboLauncherVisibility, version.launcherVisibilityProperty) bindEnum(cboLauncherVisibility, version.launcherVisibilityProperty())
bindBoolean(chkFullscreen, version.fullscreenProperty) bindBoolean(chkFullscreen, version.fullscreenProperty())
bindBoolean(chkNoGameCheck, version.notCheckGameProperty) bindBoolean(chkNoGameCheck, version.notCheckGameProperty())
bindBoolean(chkNoCommon, version.noCommonProperty) bindBoolean(chkNoCommon, version.noCommonProperty())
bindBoolean(chkShowLogs, version.showLogsProperty) bindBoolean(chkShowLogs, version.showLogsProperty())
val javaGroupKey = "java_group.listener" val javaGroupKey = "java_group.listener"
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@@ -181,8 +181,8 @@ class VersionSettingsController {
defaultToggle?.isSelected = true defaultToggle?.isSelected = true
} }
version.javaDirProperty.setChangedListener { initJavaSubtitle(version) } version.javaDirProperty().setChangedListener { initJavaSubtitle(version) }
version.javaProperty.setChangedListener { initJavaSubtitle(version) } version.javaProperty().setChangedListener { initJavaSubtitle(version) }
initJavaSubtitle(version) initJavaSubtitle(version)
val gameDirKey = "game_dir.listener" val gameDirKey = "game_dir.listener"
@@ -205,8 +205,8 @@ class VersionSettingsController {
gameDirItem.group.properties[gameDirKey] = gameDirListener gameDirItem.group.properties[gameDirKey] = gameDirListener
gameDirItem.group.selectedToggleProperty().addListener(gameDirListener) gameDirItem.group.selectedToggleProperty().addListener(gameDirListener)
version.gameDirProperty.setChangedListener { initGameDirSubtitle(version) } version.gameDirProperty().setChangedListener { initGameDirSubtitle(version) }
version.gameDirTypeProperty.setChangedListener { initGameDirSubtitle(version) } version.gameDirTypeProperty().setChangedListener { initGameDirSubtitle(version) }
initGameDirSubtitle(version) initGameDirSubtitle(version)
lastVersionSetting = version lastVersionSetting = version

View File

@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException import org.jackhuang.hmcl.auth.yggdrasil.InvalidCredentialsException
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory
import org.jackhuang.hmcl.game.HMCLMultiCharacterSelector
import org.jackhuang.hmcl.i18n import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Schedulers import org.jackhuang.hmcl.task.Schedulers
@@ -56,7 +57,7 @@ class YggdrasilAccountLoginPane(private val oldAccount: YggdrasilAccount, privat
taskResult("login") { taskResult("login") {
try { try {
val account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password) val account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password)
account.logIn(Settings.proxy) account.logIn(HMCLMultiCharacterSelector, Settings.proxy)
} catch (e: Exception) { } catch (e: Exception) {
e e
} }

View File

@@ -22,6 +22,7 @@ import com.google.gson.JsonParseException
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import javafx.beans.property.Property import javafx.beans.property.Property
import javafx.event.Event.fireEvent import javafx.event.Event.fireEvent
import org.jackhuang.hmcl.event.Event
import org.jackhuang.hmcl.event.EventBus import org.jackhuang.hmcl.event.EventBus
import org.jackhuang.hmcl.event.EventManager import org.jackhuang.hmcl.event.EventManager
import org.jackhuang.hmcl.task.Scheduler 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 <V> taskResult(id: String, callable: (AutoTypingMap<String>) -> V): TaskResult<V> = Task.ofResult(id, callable)
fun InputStream.readFullyAsString() = IOUtils.readFullyAsString(this) 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 : Event> EventManager<T>.plusAssign(func: (T) -> Unit) = register(func)
operator fun <T : EventObject> EventManager<T>.plusAssign(func: () -> Unit) = register(func) operator fun <T : Event> EventManager<T>.plusAssign(func: () -> Unit) = register(func)
operator fun <T : EventObject> EventManager<T>.minusAssign(func: (T) -> Unit) = unregister(func) operator fun <T : Event> EventManager<T>.minusAssign(func: (T) -> Unit) = unregister(func)
operator fun <T : EventObject> EventManager<T>.minusAssign(func: () -> Unit) = unregister(func) operator fun <T : Event> EventManager<T>.minusAssign(func: () -> Unit) = unregister(func)
operator fun <T : EventObject> EventManager<T>.invoke(event: T) = fireEvent(event) operator fun <T : Event> EventManager<T>.invoke(event: T) = fireEvent(event)

View File

@@ -968,6 +968,11 @@
-fx-background-color: -fx-decorator-color; -fx-background-color: -fx-decorator-color;
} }
.jfx-decorator-drawer {
-fx-background-image: url("/assets/img/background.jpg");
-fx-background-size: cover;
}
.resize-border { .resize-border {
-fx-border-color: #5264AE; -fx-border-color: #5264AE;
-fx-border-width: 0 2 2 2; -fx-border-width: 0 2 2 2;
@@ -1136,6 +1141,7 @@
******************************************************************************/ ******************************************************************************/
.scroll-pane { .scroll-pane {
-fx-background-color: null;
-fx-background-insets: 0; -fx-background-insets: 0;
-fx-padding: 0; -fx-padding: 0;
} }
@@ -1148,6 +1154,10 @@
-fx-background-insets: 0; -fx-background-insets: 0;
} }
.scroll-pane > .viewport {
-fx-background-color: null;
}
/******************************************************************************* /*******************************************************************************
* * * *
* Error Facade * * Error Facade *

View File

@@ -7,6 +7,8 @@
<?import javafx.scene.shape.Rectangle?> <?import javafx.scene.shape.Rectangle?>
<?import org.jackhuang.hmcl.ui.AdvancedListBox?> <?import org.jackhuang.hmcl.ui.AdvancedListBox?>
<?import java.lang.String?> <?import java.lang.String?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.image.Image?>
<fx:root xmlns="http://javafx.com/javafx" <fx:root xmlns="http://javafx.com/javafx"
type="StackPane" type="StackPane"
xmlns:fx="http://javafx.com/fxml"> xmlns:fx="http://javafx.com/fxml">
@@ -16,14 +18,14 @@
</styleClass> </styleClass>
<BorderPane> <BorderPane>
<center> <center>
<StackPane fx:id="drawerWrapper"> <StackPane fx:id="drawerWrapper" styleClass="jfx-decorator-drawer">
<JFXDialog fx:id="dialog" overlayClose="false" /> <JFXDialog fx:id="dialog" overlayClose="false" />
<BorderPane> <BorderPane>
<left> <left>
<StackPane minWidth="200" maxWidth="200" styleClass="jfx-decorator-content-container"> <StackPane minWidth="200" maxWidth="200" styleClass="jfx-decorator-content-container">
<BorderPane fx:id="leftRootPane"> <BorderPane fx:id="leftRootPane">
<center> <center>
<BorderPane> <BorderPane style="-fx-background-color: rgba(244, 244, 244, 0.5);">
<center> <center>
<AdvancedListBox fx:id="leftPane"/> <AdvancedListBox fx:id="leftPane"/>
</center> </center>
@@ -50,9 +52,6 @@
<StackPane fx:id="contentPlaceHolderRoot" styleClass="jfx-decorator-content-container" <StackPane fx:id="contentPlaceHolderRoot" styleClass="jfx-decorator-content-container"
VBox.vgrow="ALWAYS"> VBox.vgrow="ALWAYS">
<StackPane fx:id="contentPlaceHolder" styleClass="jfx-decorator-content-container"> <StackPane fx:id="contentPlaceHolder" styleClass="jfx-decorator-content-container">
<styleClass>
<String fx:value="jfx-decorator-content-container"/>
</styleClass>
<!-- Node --> <!-- Node -->
</StackPane> </StackPane>
</StackPane> </StackPane>

View File

@@ -5,7 +5,7 @@
<?import javafx.scene.control.ScrollPane?> <?import javafx.scene.control.ScrollPane?>
<fx:root <fx:root
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" 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"> 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"> <ScrollPane fitToHeight="true" fitToWidth="true" fx:id="scrollPane" hbarPolicy="NEVER">
<JFXMasonryPane fx:id="masonryPane" HSpacing="3" VSpacing="3" cellWidth="182" cellHeight="160"> <JFXMasonryPane fx:id="masonryPane" HSpacing="3" VSpacing="3" cellWidth="182" cellHeight="160">
@@ -24,12 +24,6 @@
<fx:include source="/assets/svg/plus.fxml" /> <fx:include source="/assets/svg/plus.fxml" />
</graphic> </graphic>
</JFXButton> </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> </VBox>
</fx:root> </fx:root>

View File

@@ -12,14 +12,14 @@
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
type="StackPane" pickOnBounds="false"> type="StackPane" pickOnBounds="false">
<VBox fx:id="content" 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> <BorderPane>
<top> <top>
<HBox alignment="CENTER_RIGHT"> <HBox alignment="CENTER_RIGHT">
</HBox> </HBox>
</top> </top>
<center> <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="lblVersionName" style="-fx-font-size: 15;" textAlignment="JUSTIFY" wrapText="true" />
<Label fx:id="lblGameVersion" style="-fx-font-size: 10;" textAlignment="JUSTIFY" wrapText="true" /> <Label fx:id="lblGameVersion" style="-fx-font-size: 10;" textAlignment="JUSTIFY" wrapText="true" />
</VBox> </VBox>
@@ -35,7 +35,7 @@
</HBox> </HBox>
</left> </left>
<right> <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> </right>
</BorderPane> </BorderPane>

View File

@@ -12,7 +12,7 @@
<StackPane xmlns="http://javafx.com/javafx" <StackPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="org.jackhuang.hmcl.ui.VersionSettingsController"> 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;"> <VBox fx:id="rootPane" style="-fx-padding: 20;">
<ComponentList depth="1"> <ComponentList depth="1">
@@ -109,7 +109,7 @@
</BorderPane> </BorderPane>
</ComponentList> </ComponentList>
<HBox alignment="CENTER" style="-fx-padding: 10 0 10 0;"> <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> </HBox>
<ComponentList fx:id="advancedSettingsPane" depth="1"> <ComponentList fx:id="advancedSettingsPane" depth="1">
<JFXTextField labelFloat="true" promptText="%advancedsettings.jvm_args" styleClass="fit-width" <JFXTextField labelFloat="true" promptText="%advancedsettings.jvm_args" styleClass="fit-width"

View File

@@ -28,11 +28,11 @@ public abstract class Account {
public abstract String getUsername(); public abstract String getUsername();
public AuthInfo logIn() throws AuthenticationException { public AuthInfo logIn(MultiCharacterSelector selector) throws AuthenticationException {
return logIn(Proxy.NO_PROXY); 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(); public abstract void logOut();

View File

@@ -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));
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -54,7 +54,7 @@ public class OfflineAccount extends Account {
} }
@Override @Override
public AuthInfo logIn(Proxy proxy) throws AuthenticationException { public AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException {
if (StringUtils.isBlank(username) || StringUtils.isBlank(uuid)) if (StringUtils.isBlank(username) || StringUtils.isBlank(uuid))
throw new AuthenticationException("Username cannot be empty"); throw new AuthenticationException("Username cannot be empty");

View File

@@ -24,13 +24,16 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer; import com.google.gson.JsonSerializer;
import org.jackhuang.hmcl.util.Immutable;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.UUID; import java.util.UUID;
/** /**
* *
* @author huang * @author huangyuhui
*/ */
@Immutable
public final class GameProfile { public final class GameProfile {
private final UUID id; private final UUID id;

View File

@@ -23,14 +23,9 @@ import com.google.gson.JsonParseException;
import java.io.IOException; import java.io.IOException;
import java.net.Proxy; import java.net.Proxy;
import java.net.URL; import java.net.URL;
import java.util.HashMap; import java.util.*;
import java.util.Map;
import java.util.Optional; import org.jackhuang.hmcl.auth.*;
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 org.jackhuang.hmcl.util.NetworkUtils; import org.jackhuang.hmcl.util.NetworkUtils;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.UUIDTypeAdapter; import org.jackhuang.hmcl.util.UUIDTypeAdapter;
@@ -111,7 +106,7 @@ public final class YggdrasilAccount extends Account {
} }
@Override @Override
public AuthInfo logIn(Proxy proxy) throws AuthenticationException { public AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException {
if (canPlayOnline()) if (canPlayOnline())
return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties));
else { else {
@@ -119,11 +114,13 @@ public final class YggdrasilAccount extends Account {
if (!isLoggedIn()) if (!isLoggedIn())
throw new AuthenticationException("Wrong password for account " + username); throw new AuthenticationException("Wrong password for account " + username);
if (selectedProfile == null) if (selectedProfile == null) {
// TODO: multi-available-profiles support if (profiles == null || profiles.length <= 0)
throw new UnsupportedOperationException("Do not support multi-available-profiles account yet."); throw new NoCharacterException(this);
else
return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties)); selectedProfile = selector.select(this, Arrays.asList(profiles));
}
return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties));
} }
} }

View File

@@ -24,7 +24,7 @@ import org.jackhuang.hmcl.util.Immutable;
* @author huangyuhui * @author huangyuhui
*/ */
@Immutable @Immutable
public final class Install { public final class ForgeInstall {
private final String profileName; private final String profileName;
private final String target; private final String target;
@@ -36,11 +36,11 @@ public final class Install {
private final String mirrorList; private final String mirrorList;
private final String logo; private final String logo;
public Install() { public ForgeInstall() {
this(null, null, null, null, null, null, null, null, null); 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.profileName = profileName;
this.target = target; this.target = target;
this.path = path; this.path = path;

View File

@@ -28,20 +28,20 @@ import org.jackhuang.hmcl.util.Validation;
* @author huangyuhui * @author huangyuhui
*/ */
@Immutable @Immutable
public final class InstallProfile implements Validation { public final class ForgeInstallProfile implements Validation {
@SerializedName("install") @SerializedName("install")
private final Install install; private final ForgeInstall install;
@SerializedName("versionInfo") @SerializedName("versionInfo")
private final Version versionInfo; private final Version versionInfo;
public InstallProfile(Install install, Version versionInfo) { public ForgeInstallProfile(ForgeInstall install, Version versionInfo) {
this.install = install; this.install = install;
this.versionInfo = versionInfo; this.versionInfo = versionInfo;
} }
public Install getInstall() { public ForgeInstall getInstall() {
return install; return install;
} }

View File

@@ -107,7 +107,7 @@ public final class ForgeInstallTask extends TaskResult<Version> {
if (stream == null) if (stream == null)
throw new IOException("Malformed forge installer file, install_profile.json does not exist."); throw new IOException("Malformed forge installer file, install_profile.json does not exist.");
String json = IOUtils.readFullyAsString(stream); String json = IOUtils.readFullyAsString(stream);
InstallProfile installProfile = Constants.GSON.fromJson(json, InstallProfile.class); ForgeInstallProfile installProfile = Constants.GSON.fromJson(json, ForgeInstallProfile.class);
if (installProfile == null) if (installProfile == null)
throw new IOException("Malformed forge installer file, install_profile.json does not exist."); throw new IOException("Malformed forge installer file, install_profile.json does not exist.");

View File

@@ -57,14 +57,13 @@ public final class GameLibrariesTask extends Task {
@Override @Override
public void execute() throws Exception { public void execute() throws Exception {
for (Library library : version.getLibraries()) version.getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
if (library.appliesToCurrentEnvironment()) { File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
File file = dependencyManager.getGameRepository().getLibraryFile(version, library); if (!file.exists())
if (!file.exists()) dependencies.add(new FileDownloadTask(
dependencies.add(new FileDownloadTask( NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(library.getDownload().getUrl())),
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(library.getDownload().getUrl())), file, dependencyManager.getProxy(), library.getDownload().getSha1()));
file, dependencyManager.getProxy(), library.getDownload().getSha1())); });
}
} }
} }

View File

@@ -36,7 +36,7 @@ public final class GameRemoteVersions {
private final GameRemoteLatestVersions latest; private final GameRemoteLatestVersions latest;
public GameRemoteVersions() { public GameRemoteVersions() {
this(Collections.EMPTY_LIST, null); this(Collections.emptyList(), null);
} }
public GameRemoteVersions(List<GameRemoteVersion> versions, GameRemoteLatestVersions latest) { public GameRemoteVersions(List<GameRemoteVersion> versions, GameRemoteLatestVersions latest) {

View File

@@ -61,7 +61,7 @@ public final class VersionJsonDownloadTask extends Task {
@Override @Override
public void execute() throws Exception { public void execute() throws Exception {
RemoteVersion<?> remoteVersion = gameVersionList.getVersions(gameVersion).stream().findFirst() 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()); String jsonURL = dependencyManager.getDownloadProvider().injectURL(remoteVersion.getUrl());
dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL), Proxy.NO_PROXY, ID)); dependencies.add(new GetTask(NetworkUtils.toURL(jsonURL), Proxy.NO_PROXY, ID));
} }

View File

@@ -38,7 +38,7 @@ public final class LiteLoaderBranch {
private final Map<String, LiteLoaderVersion> liteLoader; private final Map<String, LiteLoaderVersion> liteLoader;
public LiteLoaderBranch() { public LiteLoaderBranch() {
this(Collections.EMPTY_SET, Collections.EMPTY_MAP); this(Collections.emptySet(), Collections.emptyMap());
} }
public LiteLoaderBranch(Collection<Library> libraries, Map<String, LiteLoaderVersion> liteLoader) { public LiteLoaderBranch(Collection<Library> libraries, Map<String, LiteLoaderVersion> liteLoader) {

View File

@@ -96,11 +96,11 @@ public final class LiteLoaderInstallTask extends TaskResult<Version> {
new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrl())) 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 setResult(version
.setMainClass("net.minecraft.launchwrapper.Launch") .setMainClass("net.minecraft.launchwrapper.Launch")
.setLibraries(Lang.merge(tempVersion.getLibraries(), version.getLibraries())) .setLibraries(Lang.merge(tempVersion.getLibraries(), version.getLibraries()))
.setLogging(Collections.EMPTY_MAP) .setLogging(Collections.emptyMap())
.setMinecraftArguments(version.getMinecraftArguments().orElse("") + " --tweakClass " + remote.getTag().getTweakClass()) .setMinecraftArguments(version.getMinecraftArguments().orElse("") + " --tweakClass " + remote.getTag().getTweakClass())
//.setArguments(Arguments.addGameArguments(Lang.get(version.getArguments()), "--tweakClass", remote.getTag().getTweakClass())) //.setArguments(Arguments.addGameArguments(Lang.get(version.getArguments()), "--tweakClass", remote.getTag().getTweakClass()))
); );

View File

@@ -113,7 +113,7 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
hasFMLTweaker = true; hasFMLTweaker = true;
if (version.getArguments().isPresent()) { if (version.getArguments().isPresent()) {
List<Argument> game = version.getArguments().get().getGame(); 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; hasFMLTweaker = true;
} }

View File

@@ -80,10 +80,13 @@ public final class OptiFineVersionList extends VersionList<Void> {
if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineFile")) if (td.getAttribute("class") != null && td.getAttribute("class").startsWith("downloadLineFile"))
version = td.getTextContent(); version = td.getTextContent();
} }
if (version == null || url == null)
continue;
Matcher matcher = PATTERN.matcher(version); Matcher matcher = PATTERN.matcher(version);
while (matcher.find()) while (matcher.find())
gameVersion = matcher.group(1); gameVersion = matcher.group(1);
if (gameVersion == null || version == null || url == null) if (gameVersion == null)
continue; continue;
versions.put(gameVersion, new RemoteVersion<>(gameVersion, version, url, null)); versions.put(gameVersion, new RemoteVersion<>(gameVersion, version, url, null));
} }

View File

@@ -62,7 +62,7 @@ public class Event extends EventObject {
return false; return false;
} }
private Result result; private Result result = Result.DEFAULT;
/** /**
* Retutns the value set as the result of this event * Retutns the value set as the result of this event

View File

@@ -17,7 +17,6 @@
*/ */
package org.jackhuang.hmcl.event; package org.jackhuang.hmcl.event;
import java.util.EventObject;
import java.util.HashMap; import java.util.HashMap;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
@@ -29,14 +28,14 @@ public final class EventBus {
private final HashMap<Class<?>, EventManager<?>> events = new HashMap<>(); 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)) if (!events.containsKey(clazz))
events.put(clazz, new EventManager<>(Schedulers.computation())); events.put(clazz, new EventManager<>());
return (EventManager<T>) events.get(clazz); return (EventManager<T>) events.get(clazz);
} }
public void fireEvent(EventObject obj) { public Event.Result fireEvent(Event obj) {
channel((Class<EventObject>) obj.getClass()).fireEvent(obj); return channel((Class<Event>) obj.getClass()).fireEvent(obj);
} }
public static final EventBus EVENT_BUS = new EventBus(); public static final EventBus EVENT_BUS = new EventBus();

View File

@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.event; package org.jackhuang.hmcl.event;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.EventObject;
import java.util.HashSet; import java.util.HashSet;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.jackhuang.hmcl.task.Scheduler; import org.jackhuang.hmcl.task.Scheduler;
@@ -29,22 +28,13 @@ import org.jackhuang.hmcl.util.SimpleMultimap;
* *
* @author huangyuhui * @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 private final SimpleMultimap<EventPriority, Consumer<T>> handlers
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), HashSet::new); = new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), HashSet::new);
private final SimpleMultimap<EventPriority, Runnable> handlers2 private final SimpleMultimap<EventPriority, Runnable> handlers2
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), HashSet::new); = 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) { public void register(Consumer<T> consumer) {
register(consumer, EventPriority.NORMAL); register(consumer, EventPriority.NORMAL);
} }
@@ -71,15 +61,18 @@ public final class EventManager<T extends EventObject> {
handlers2.removeValue(runnable); handlers2.removeValue(runnable);
} }
public void fireEvent(T event) { public Event.Result fireEvent(T event) {
scheduler.schedule(() -> { for (EventPriority priority : EventPriority.values()) {
for (EventPriority priority : EventPriority.values()) { for (Consumer<T> handler : handlers.get(priority))
for (Consumer<T> handler : handlers.get(priority)) handler.accept(event);
handler.accept(event); for (Runnable runnable : handlers2.get(priority))
for (Runnable runnable : handlers2.get(priority)) runnable.run();
runnable.run(); }
}
}); if (event.hasResult())
return event.getResult();
else
return Event.Result.DEFAULT;
} }
} }

View File

@@ -23,7 +23,7 @@ import java.util.EventObject;
* *
* @author huang * @author huang
*/ */
public class FailedEvent<T> extends EventObject { public class FailedEvent<T> extends Event {
private final int failedTime; private final int failedTime;
private T newResult; private T newResult;

View File

@@ -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;
}
}

View File

@@ -23,16 +23,20 @@ import org.jackhuang.hmcl.util.ManagedProcess;
/** /**
* This event gets fired when we launch the JVM and it got crashed. * This event gets fired when we launch the JVM and it got crashed.
* <br> * <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 * @author huangyuhui
*/ */
public class JVMLaunchFailedEvent extends EventObject { public class JVMLaunchFailedEvent extends Event {
private final ManagedProcess process; private final ManagedProcess process;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
* @param process the crashed process.
*/
public JVMLaunchFailedEvent(Object source, ManagedProcess process) { public JVMLaunchFailedEvent(Object source, ManagedProcess process) {
super(source); super(source);
this.process = process; this.process = process;

View File

@@ -17,28 +17,37 @@
*/ */
package org.jackhuang.hmcl.event; package org.jackhuang.hmcl.event;
import org.jackhuang.hmcl.game.Version;
import java.util.EventObject; import java.util.EventObject;
/** /**
* This event gets fired when a minecraft version has been loaded. * This event gets fired when a minecraft version has been loaded.
* <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.game.GameRepository}
* @param version the version id.
* *
* @author huangyuhui * @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); super(source);
this.version = version; this.version = version;
} }
public String getVersion() { public Version getVersion() {
return version; return version;
} }
@Override
public boolean hasResult() {
return true;
}
} }

View File

@@ -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. * This event gets fired when a JavaProcess exited abnormally and the exit code is not zero.
* <br></br> * <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 * @author huangyuhui
*/ */
public final class ProcessExitedAbnormallyEvent extends EventObject { public final class ProcessExitedAbnormallyEvent extends Event {
private final ManagedProcess process; 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) { public ProcessExitedAbnormallyEvent(Object source, ManagedProcess process) {
super(source); super(source);
this.process = process; this.process = process;

View File

@@ -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. * This event gets fired when minecraft process exited successfully and the exit code is 0.
* <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 minecraft process
* @author huangyuhui * @author huangyuhui
*/ */
public class ProcessStoppedEvent extends EventObject { public class ProcessStoppedEvent extends Event {
private final ManagedProcess process; private final ManagedProcess process;
/**
* Constructor.
*
* @param source {@link org.jackhuang.hmcl.launch.ExitWaiter}
* @param process minecraft process
*/
public ProcessStoppedEvent(Object source, ManagedProcess process) { public ProcessStoppedEvent(Object source, ManagedProcess process) {
super(source); super(source);
this.process = process; this.process = process;

View File

@@ -22,14 +22,17 @@ import java.util.EventObject;
/** /**
* This event gets fired when all the versions in .minecraft folder are loaded. * This event gets fired when all the versions in .minecraft folder are loaded.
* <br> * <br>
* This event is fired on the {@link org.jackhuang.hmcl.api.HMCLApi#EVENT_BUS} * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS}
*
* @param source {@link org.jackhuang.hmcl.game.GameRepository]
* *
* @author huangyuhui * @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) { public RefreshedVersionsEvent(Object source) {
super(source); super(source);
} }

View File

@@ -22,14 +22,17 @@ import java.util.EventObject;
/** /**
* This event gets fired when loading versions in a .minecraft folder. * This event gets fired when loading versions in a .minecraft folder.
* <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.game.GameRepository}
* *
* @author huangyuhui * @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) { public RefreshingVersionsEvent(Object source) {
super(source); super(source);
} }

View File

@@ -50,11 +50,11 @@ public final class Arguments {
} }
public List<Argument> getGame() { 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() { 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) { 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) { 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) { public static List<String> parseArguments(List<Argument> arguments, Map<String, String> keys, Map<String, Boolean> features) {

View File

@@ -35,7 +35,7 @@ public final class AssetIndex {
private final Map<String, AssetObject> objects; private final Map<String, AssetObject> objects;
public AssetIndex() { public AssetIndex() {
this(false, Collections.EMPTY_MAP); this(false, Collections.emptyMap());
} }
public AssetIndex(boolean virtual, Map<String, AssetObject> objects) { public AssetIndex(boolean virtual, Map<String, AssetObject> objects) {

View File

@@ -21,16 +21,11 @@ import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Collection; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.logging.Level; import java.util.logging.Level;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.LoadedOneVersionEvent; import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent; import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.FileUtils;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
@@ -94,7 +89,7 @@ public class DefaultGameRepository implements GameRepository {
@Override @Override
public File getVersionJar(Version version) { public File getVersionJar(Version version) {
Version v = version.resolve(this); 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"); return new File(getVersionRoot(id), id + ".jar");
} }
@@ -184,9 +179,15 @@ public class DefaultGameRepository implements GameRepository {
version = Objects.requireNonNull(readVersionJson(json)); version = Objects.requireNonNull(readVersionJson(json));
} catch (Exception e) { } catch (Exception e) {
// JsonSyntaxException or IOException or NullPointerException(!!) // JsonSyntaxException or IOException or NullPointerException(!!)
// TODO: auto making up for the missing json if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
// TODO: and even asking for removing the redundant version folder. continue;
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())) { if (!id.equals(version.getId())) {
@@ -199,18 +200,20 @@ public class DefaultGameRepository implements GameRepository {
} }
} }
versions.put(id, version); if (EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, version)) != Event.Result.DENY)
EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, id)); versions.put(id, version);
} }
loaded = true; loaded = true;
} }
@Override @Override
public final void refreshVersions() { public void refreshVersions() {
EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)); EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this));
refreshVersionsImpl(); Schedulers.newThread().schedule(() -> {
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this)); refreshVersionsImpl();
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
});
} }
@Override @Override

View File

@@ -29,6 +29,8 @@ import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.OperatingSystem; import org.jackhuang.hmcl.util.OperatingSystem;
@@ -99,7 +101,7 @@ public class Library {
+ (this.classifier == null ? "" : "-" + this.classifier) + ".jar"; + (this.classifier == null ? "" : "-" + this.classifier) + ".jar";
download = new LibraryDownloadInfo(path, 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.getSha1() : null,
temp != null ? temp.getSize() : 0 temp != null ? temp.getSize() : 0
); );

View File

@@ -75,7 +75,7 @@ public class RuledArgument implements Argument {
.map(StringArgument::new) .map(StringArgument::new)
.map(str -> str.toString(keys, features).get(0)) .map(str -> str.toString(keys, features).get(0))
.collect(Collectors.toList()); .collect(Collectors.toList());
return Collections.EMPTY_LIST; return Collections.emptyList();
} }
public static class Serializer implements JsonSerializer<RuledArgument>, JsonDeserializer<RuledArgument> { public static class Serializer implements JsonSerializer<RuledArgument>, JsonDeserializer<RuledArgument> {

View File

@@ -155,8 +155,6 @@ public class Version implements Comparable<Version>, Validation {
/** /**
* Resolve given version * Resolve given version
*
* @throws CircleDependencyException
*/ */
public Version resolve(VersionProvider provider) { public Version resolve(VersionProvider provider) {
return resolve(provider, new HashSet<>()); return resolve(provider, new HashSet<>());
@@ -231,10 +229,7 @@ public class Version implements Comparable<Version>, Validation {
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (obj instanceof Version) return obj instanceof Version && Objects.equals(id, ((Version) obj).id);
return Objects.equals(id, ((Version) obj).id);
else
return false;
} }
@Override @Override

View File

@@ -22,11 +22,7 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.util.Arrays; import java.util.*;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.AuthInfo;
@@ -207,7 +203,7 @@ public class DefaultLauncher extends Launcher {
.filter(it -> !getForbiddens().containsKey(it) || !getForbiddens().get(it).get()) .filter(it -> !getForbiddens().containsKey(it) || !getForbiddens().get(it).get())
.collect(Collectors.toList()); .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() { public Map<String, Boolean> getFeatures() {
return Collections.singletonMap( return Collections.singletonMap(
"has_custom_resolution", "has_custom_resolution",
@@ -251,14 +247,14 @@ public class DefaultLauncher extends Launcher {
false); false);
} }
public Map<String, String> getConfigurations() { protected Map<String, String> getConfigurations() {
return Lang.mapOf( return Lang.mapOf(
new Pair<>("${auth_player_name}", authInfo.getUsername()), new Pair<>("${auth_player_name}", authInfo.getUsername()),
new Pair<>("${auth_session}", authInfo.getAuthToken()), new Pair<>("${auth_session}", authInfo.getAuthToken()),
new Pair<>("${auth_access_token}", authInfo.getAuthToken()), new Pair<>("${auth_access_token}", authInfo.getAuthToken()),
new Pair<>("${auth_uuid}", authInfo.getUserId()), new Pair<>("${auth_uuid}", authInfo.getUserId()),
new Pair<>("${version_name}", Lang.nonNull(options.getVersionName(), version.getId())), new Pair<>("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())),
new Pair<>("${profile_name}", Lang.nonNull(options.getProfileName(), "Minecraft")), new Pair<>("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")),
new Pair<>("${version_type}", version.getType().getId()), new Pair<>("${version_type}", version.getType().getId()),
new Pair<>("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()), new Pair<>("${game_directory}", repository.getRunDirectory(version.getId()).getAbsolutePath()),
new Pair<>("${user_type}", authInfo.getUserType().toString().toLowerCase()), new Pair<>("${user_type}", authInfo.getUserType().toString().toLowerCase()),
@@ -275,11 +271,8 @@ public class DefaultLauncher extends Launcher {
decompressNatives(); decompressNatives();
if (StringUtils.isNotBlank(options.getPrecalledCommand())) { if (StringUtils.isNotBlank(options.getPrecalledCommand()))
Process process = Runtime.getRuntime().exec(options.getPrecalledCommand()); Runtime.getRuntime().exec(options.getPrecalledCommand()).waitFor();
if (process.isAlive())
process.waitFor();
}
builder.directory(repository.getRunDirectory(version.getId())) builder.directory(repository.getRunDirectory(version.getId()))
.environment().put("APPDATA", options.getGameDir().getAbsoluteFile().getParent()); .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) { private void startMonitorsWithoutLoggingInfo(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
processListener.setProcess(managedProcess); processListener.setProcess(managedProcess);
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> { 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); managedProcess.addLine(it);
}), "stdout-pump", isDaemon); }), "stdout-pump", isDaemon);
managedProcess.addRelatedThread(stdout); managedProcess.addRelatedThread(stdout);

View File

@@ -17,11 +17,9 @@
*/ */
package org.jackhuang.hmcl.launch; package org.jackhuang.hmcl.launch;
import java.io.IOException; import java.io.*;
import java.io.InterruptedIOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Date; import java.util.Date;
import java.util.Optional;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
@@ -55,6 +53,7 @@ final class Log4jHandler extends Thread {
public Log4jHandler(BiConsumer<String, Log4jLevel> callback) { public Log4jHandler(BiConsumer<String, Log4jLevel> callback) {
this.callback = callback; this.callback = callback;
newLine("<output>");
reader = Lang.invoke(() -> XMLReaderFactory.createXMLReader()); reader = Lang.invoke(() -> XMLReaderFactory.createXMLReader());
reader.setContentHandler(new Log4jHandlerImpl()); reader.setContentHandler(new Log4jHandlerImpl());
@@ -63,7 +62,6 @@ final class Log4jHandler extends Thread {
@Override @Override
public void run() { public void run() {
setName("log4j-handler"); setName("log4j-handler");
newLine("<output>");
try { try {
reader.parse(new InputSource(inputStream)); reader.parse(new InputSource(inputStream));
@@ -88,17 +86,14 @@ final class Log4jHandler extends Thread {
}).get()); }).get());
} }
public Future<?> newLine(String content) { public Future<?> newLine(String log) {
return Schedulers.computation().schedule(() -> { return Schedulers.computation().schedule(() -> {
String log = content; byte[] bytes = (log + OperatingSystem.LINE_SEPARATOR)
if (!log.trim().startsWith("<"))
log = "<![CDATA[" + log.replace("]]>", "") + "]]>";
outputStream.write((log + OperatingSystem.LINE_SEPARATOR)
.replace("log4j:Event", "log4j_Event") .replace("log4j:Event", "log4j_Event")
.replace("log4j:Message", "log4j_Message") .replace("log4j:Message", "log4j_Message")
.replace("log4j:Throwable", "log4j_Throwable") .replace("log4j:Throwable", "log4j_Throwable")
.getBytes() .getBytes();
); outputStream.write(bytes);
outputStream.flush(); outputStream.flush();
}); });
} }
@@ -155,7 +150,7 @@ final class Log4jHandler extends Thread {
if (readingMessage) if (readingMessage)
message.append(line).append(OperatingSystem.LINE_SEPARATOR); message.append(line).append(OperatingSystem.LINE_SEPARATOR);
else else
callback.accept(line, Lang.nonNull(Log4jLevel.guessLevel(line), Log4jLevel.INFO)); callback.accept(line, Optional.ofNullable(Log4jLevel.guessLevel(line)).orElse(Log4jLevel.INFO));
} }
} }
} }

View File

@@ -49,7 +49,7 @@ public final class CurseInstallTask extends Task {
* @param zipFile the CurseForge modpack file. * @param zipFile the CurseForge modpack file.
* @param manifest The manifest content of given CurseForge modpack. * @param manifest The manifest content of given CurseForge modpack.
* @param name the new version name * @param name the new version name
* @see readCurseForgeModpackManifest * @see CurseManifest#readCurseForgeModpackManifest
*/ */
public CurseInstallTask(DefaultDependencyManager dependencyManager, File zipFile, CurseManifest manifest, String name) { public CurseInstallTask(DefaultDependencyManager dependencyManager, File zipFile, CurseManifest manifest, String name) {
this.dependencyManager = dependencyManager; this.dependencyManager = dependencyManager;

View File

@@ -23,6 +23,8 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.jackhuang.hmcl.util.CompressingUtils; import org.jackhuang.hmcl.util.CompressingUtils;
import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Constants;
import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Immutable;
@@ -122,7 +124,7 @@ public final class CurseManifest {
if (manifest == null) if (manifest == null)
throw new JsonParseException("`manifest.json` not found. Not a valid Curse modpack."); throw new JsonParseException("`manifest.json` not found. Not a valid Curse modpack.");
return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(), 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"; public static final String MINECRAFT_MODPACK = "minecraftModpack";

View File

@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.mod;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Optional;
import java.util.Properties; import java.util.Properties;
import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.compress.archivers.zip.ZipFile;
@@ -83,7 +84,7 @@ public final class MultiMCInstanceConfiguration {
showConsoleOnError = Boolean.parseBoolean(p.getProperty("ShowConsoleOnError")); showConsoleOnError = Boolean.parseBoolean(p.getProperty("ShowConsoleOnError"));
wrapperCommand = p.getProperty("WrapperCommand"); wrapperCommand = p.getProperty("WrapperCommand");
name = defaultName; name = defaultName;
notes = Lang.nonNull(p.getProperty("notes"), ""); notes = Optional.ofNullable(p.getProperty("notes")).orElse("");
} }
/** /**

View File

@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.task;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.jackhuang.hmcl.util.AutoTypingMap; 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 { class SimpleTask extends Task {
private final Consumer<AutoTypingMap<String>> consumer; private final ExceptionalConsumer<AutoTypingMap<String>, ?> consumer;
private final Scheduler scheduler; private final Scheduler scheduler;
public SimpleTask(Consumer<AutoTypingMap<String>> consumer) { public SimpleTask(ExceptionalConsumer<AutoTypingMap<String>, ?> consumer) {
this(consumer, Schedulers.defaultScheduler()); this(consumer, Schedulers.defaultScheduler());
} }
public SimpleTask(Consumer<AutoTypingMap<String>> consumer, Scheduler scheduler) { public SimpleTask(ExceptionalConsumer<AutoTypingMap<String>, ?> consumer, Scheduler scheduler) {
this.consumer = consumer; this.consumer = consumer;
this.scheduler = scheduler; this.scheduler = scheduler;
setName(consumer.toString());
} }
@Override @Override

View File

@@ -29,6 +29,8 @@ import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.property.ReadOnlyStringWrapper;
import org.jackhuang.hmcl.util.AutoTypingMap; import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.event.EventManager; import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.ExceptionalConsumer;
import org.jackhuang.hmcl.util.ExceptionalRunnable;
import org.jackhuang.hmcl.util.Properties; import org.jackhuang.hmcl.util.Properties;
/** /**
@@ -204,11 +206,11 @@ public abstract class Task {
return executor; 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)); 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)); return subscribe(of(closure));
} }
@@ -228,15 +230,15 @@ public abstract class Task {
return new CoupleTask<>(this, b, false); return new CoupleTask<>(this, b, false);
} }
public static Task of(Runnable runnable) { public static Task of(ExceptionalRunnable<?> runnable) {
return of(s -> runnable.run()); 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()); 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); return new SimpleTask(closure, scheduler);
} }

View File

@@ -17,13 +17,13 @@
*/ */
package org.jackhuang.hmcl.task; package org.jackhuang.hmcl.task;
import java.util.EventObject; import org.jackhuang.hmcl.event.Event;
/** /**
* *
* @author huang * @author huang
*/ */
public class TaskEvent extends EventObject { public class TaskEvent extends Event {
private final Task task; private final Task task;
private final boolean failed; private final boolean failed;

View File

@@ -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;
}

View File

@@ -59,7 +59,7 @@ public final class Lang {
* *
* @param <T> type of argument. * @param <T> type of argument.
* @param <R> type of result. * @param <R> type of result.
* @param supplier your method. * @param function your method.
* @return the result of the method to invoke. * @return the result of the method to invoke.
*/ */
public static <T, R, E extends Exception> R invoke(ExceptionalFunction<T, R, E> function, T t) { 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); 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<>(); LinkedList<T> result = new LinkedList<>();
for (Collection<T> collection : collections) result.addAll(a);
if (collection != null) result.addAll(b);
result.addAll(collection);
return result; return result;
} }
public static <T> T nonNull(T... t) { public static <T> List<T> merge(Collection<T> a, Collection<T> b, Collection<T> c) {
for (T a : t) LinkedList<T> result = new LinkedList<>();
if (a != null) result.addAll(a);
return a; result.addAll(b);
return null; result.addAll(c);
return result;
} }
public static Thread thread(Runnable runnable) { public static Thread thread(Runnable runnable) {
@@ -223,10 +223,6 @@ public final class Lang {
return thread; 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) { public static <T> Iterator<T> asIterator(Enumeration<T> enumeration) {
return new Iterator<T>() { return new Iterator<T>() {
@Override @Override
@@ -244,4 +240,12 @@ public final class Lang {
public static <T> Iterable<T> asIterable(Enumeration<T> enumeration) { public static <T> Iterable<T> asIterable(Enumeration<T> enumeration) {
return () -> asIterator(enumeration); return () -> asIterator(enumeration);
} }
public static int parseInt(String string, int defaultValue) {
try {
return Integer.parseInt(string);
} catch (NumberFormatException e) {
return defaultValue;
}
}
} }

View File

@@ -65,12 +65,12 @@ public enum OperatingSystem {
/** /**
* The total memory/MB this computer have. * 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. * 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 PATH_SEPARATOR = File.pathSeparator;
public static final String FILE_SEPARATOR = File.separator; public static final String FILE_SEPARATOR = File.separator;
@@ -104,11 +104,11 @@ public enum OperatingSystem {
Object bytes = ReflectionHelper.call(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize"); Object bytes = ReflectionHelper.call(ManagementFactory.getOperatingSystemMXBean(), "getTotalPhysicalMemorySize");
if (bytes instanceof Long) if (bytes instanceof Long)
TOTAL_MEMORY = ((Long) bytes) / 1024 / 1024; TOTAL_MEMORY = (int) (((Long) bytes) / 1024 / 1024);
else else
TOTAL_MEMORY = 1024; 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"); String arch = System.getProperty("sun.arch.data.model");
if (arch == null) if (arch == null)

View File

@@ -29,7 +29,7 @@ public abstract class VersionNumber implements Comparable<VersionNumber> {
/** /**
* @throws IllegalArgumentException if there are some characters excluding digits and dots. * @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 * @return the int version number
*/ */
public static IntVersionNumber asIntVersionNumber(String version) { public static IntVersionNumber asIntVersionNumber(String version) {