Initial project

This commit is contained in:
Timi
2025-07-15 16:46:10 +08:00
parent cc236f7c10
commit 3c723bd443
116 changed files with 9981 additions and 94 deletions

View File

@ -0,0 +1,63 @@
package cn.forevermc.launcher;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.ctrl.Main;
import javafx.application.Application;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.config.ConfigLoader;
import com.imyeyu.fx.config.BindingsConfig;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.InjectApp;
import com.imyeyu.inject.annotation.TimiInjectApplication;
import com.imyeyu.java.TimiJava;
import com.imyeyu.lang.multi.ResourcesMultilingual;
import java.util.UUID;
/**
* Forever Minecraft 启动器
*
* @author 夜雨
* @since 2022-11-18 15:36
*/
@Slf4j
@TimiInjectApplication
public class ForeverMC {
public static final String VERSION = "2.2.3";
@Getter
private static Config config;
@Getter
private static ConfigLoader<Config> configLoader;
@Getter
private static InjectApp injectApp;
public static void main(String[] args) {
injectApp = new InjectApp(ForeverMC.class);
try {
{
configLoader = BindingsConfig.build("ForeverMC.yaml", Config.class);
config = configLoader.load();
if (TimiJava.isEmpty(config.getLauncher().getClientId().get())) {
config.getLauncher().getClientId().set(UUID.randomUUID().toString());
}
ResourcesMultilingual multilingual = TimiFXUI.MULTILINGUAL;
multilingual.addAll("lang/timi-fx-ui/%s.lang");
multilingual.addAll("lang/%s.lang");
multilingual.setActivated(config.getMain().getLanguage().get());
// 禁止系统 DPI 缩放
System.setProperty("prism.allowhidpi", "false");
System.setProperty("glass.win.minHiDPI", "1");
}
Application.launch(Main.class);
} catch (Exception e) {
log.error("fatal error", e);
}
}
}

View File

@ -0,0 +1,45 @@
package cn.forevermc.launcher.bean;
import lombok.Data;
import java.util.List;
/**
* @author 夜雨
* @since 2024-04-29 20:05
*/
@Data
public class APISetting {
public enum Key {
/** 闪烁标语 */
FMC_SPLASHES,
/** 启动器背景 */
FMC_BG,
FMC_BGM,
}
private DynamicList bg;
private DynamicList bgm;
private DynamicList splashes;
/**
*
* @author 夜雨
* @since 2022-12-01 15:59
*/
@Data
public static class DynamicList {
/** 激活的标语 */
private String active;
/** 标语列表 */
private List<String> list;
}
}

View File

@ -0,0 +1,22 @@
package cn.forevermc.launcher.bean;
/**
* 组件大小
*
* @author 夜雨
* @since 2023-06-09 16:43
*/
public class ComponentSize {
/** 小号 */
public static final double SMALL = 150;
/** 正常 */
public static final double NORMAL = 200;
/** 中等 */
public static final double MEDIUM = 300;
/** 大号 */
public static final double LARGE = 400;
}

View File

@ -0,0 +1,94 @@
package cn.forevermc.launcher.bean;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import lombok.Data;
import com.imyeyu.java.bean.Language;
/**
* 配置
*
* @author 夜雨
* @since 2024-04-18 17:55
*/
@Data
public class Config {
/** 主配置 */
private Main main;
/** 玩家配置 */
private Player player;
/** 启动配置 */
private Launcher launcher;
/**
* 主配置
*
* @author 夜雨
* @since 2024-04-18 18:00
*/
@Data
public static class Main {
/** 启动器语言 */
private ObjectProperty<Language> language;
/** 背景音乐音量,[0, 100] */
private DoubleProperty bgmVolume;
/** 音效音量,[0, 100] */
private DoubleProperty soundVolume;
/** 游戏下载源 */
private ObjectProperty<GameDownloadSource> gameDownloadSource;
/** 运行时下载源 */
private ObjectProperty<RuntimeDownloadSource> runtimeDownloadSource;
}
/**
* 玩家配置
*
* @author 夜雨
* @since 2024-04-18 18:00
*/
@Data
public static class Player {
/** 登录名 */
private StringProperty name;
/** 密码 */
private StringProperty password;
}
/**
* 启动器配置
*
* @author 夜雨
* @since 2024-04-18 17:59
*/
@Data
public static class Launcher {
/** 最大内存 */
private DoubleProperty memory;
/** 启动游戏 */
private StringProperty game;
/** 客户端 ID */
private StringProperty clientId;
/** true 为自动启动 */
private BooleanProperty autoStartup;
/** 多线程下载游戏 */
private IntegerProperty multiDownload;
}
}

View File

@ -0,0 +1,21 @@
package cn.forevermc.launcher.bean;
import lombok.Data;
/**
*
*
* @author 夜雨
* @since 2024-06-11 19:29
*/
@Data
public class FabricAPI {
private String name;
private String fabricVer;
private String minecraftVer;
private String mongoId;
}

View File

@ -0,0 +1,80 @@
package cn.forevermc.launcher.bean;
import cn.forevermc.launcher.util.Path;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import lombok.Data;
import com.imyeyu.io.IO;
import com.imyeyu.utils.Digest;
import java.io.File;
/**
* 文件下载对象
*
* @author 夜雨
* @since 2022-11-21 11:22
*/
@Data
public class FileDownload {
/** 本地文件对象 */
private File file;
/** 大小 */
private Long size;
/** 请求 URL */
private String url;
/** 哈希 */
private String hash;
/** 显示名称 */
private String displayName;
/** 进度 */
private DoubleProperty progress;
/** true 为下载错误 */
private BooleanProperty isError;
/** 重试次数 */
private int retry = 0;
public FileDownload(File file) {
this.file = file;
this.displayName = file.getAbsolutePath().substring(file.getAbsolutePath().indexOf(Path.P_ROOT));
progress = new SimpleDoubleProperty(0);
isError = new SimpleBooleanProperty(false);
}
/** @return true 为存在且 HASH 匹配 */
public boolean exist() throws Exception {
return file.exists() && hash.equals(Digest.sha1(IO.toBytes(file)));
}
/** @return 文件名 */
public String getFileName() {
return file.getName();
}
/** @return 进度监听 */
public DoubleProperty progressProperty() {
return progress;
}
/** @param isError true 为下载错误 */
public void setError(boolean isError) {
this.isError.set(isError);
}
/** @return 错误监听,只读 */
public ReadOnlyBooleanProperty errorProperty() {
return isError;
}
}

View File

@ -0,0 +1,350 @@
package cn.forevermc.launcher.bean;
import com.google.gson.JsonObject;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.StringProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.java.TimiJava;
import java.io.File;
import java.io.Serializable;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 游戏
*
* @author 夜雨
* @since 2021-11-26 11:13
*/
@Data
public class Game {
/**
* 游戏版本类型
*
* @author 夜雨
* @since 2022-11-21 14:54
*/
public enum Type {
/** 快照 */
SNAPSHOT,
/** 正式版 */
RELEASE,
/** 旧的测试版 */
OLD_BETA,
/** 旧的实验版 */
OLD_ALPHA
}
// ---------- 版本属性,这些属性在解析前就有效 ----------
/** 显示名称 */
private String name;
/** 版本路径(如:.minecraft/versions/1.20 */
private File path;
/** JAR 文件 */
private File jar;
/** JSON 文件 */
private File json;
/** yaml 文件ForeverMC 针对此整合包的配置) */
private File yaml;
/** JSON 文件的根节点 */
private JsonObject jsonRoot;
/** 发布时间 */
private Long releaseAt;
/** true 为原版 */
private boolean isOriginal;
/** true 为 Fabric 版本 */
private boolean isFabric;
/** true 为上古版本 */
private boolean isLegacy;
/** 版本设置 */
private Option option;
// ---------- 版本 JSON 数据,这些属性在解析后才有效 ----------
/** ID版本号可能会被修改 */
private String id;
/** 游戏版本类型 */
private Type type;
/** 元数据 URL */
private String url;
/** 版本元数据 */
private MetaData metaData;
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Game game = (Game) object;
return Objects.equals(path, game.path);
}
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + (path != null ? path.hashCode() : 0);
return result;
}
/**
* 版本元数据,通过 {@link cn.forevermc.launcher.service.GameService#readMetaData(Game)} 解析后有效
*
* @author 夜雨
* @since 2022-11-21 11:28
*/
@Data
public static class MetaData {
/**
* 真实版本号,目前仅在正式版有效
*
* @author 夜雨
* @since 2023-06-17 16:11
*/
@Getter
public static class RealVersion {
/** 版本号匹配正则 */
private static final Pattern PATTERN = Pattern.compile("(\\d)\\.(\\d+)\\.?(\\d+)?");
/** 版本号 */
private String value;
/** 根版本 */
private int root = 0;
/** 主版本 */
private int main = 0;
/** BUG 修正版本 */
private int bug = 0;
/**
* 设置版本号,通过此方法自动解析详细版本号
*
* @param value 版本号
*/
public void setValue(String value) {
this.value = value;
Matcher matcher = PATTERN.matcher(value);
if (matcher.find()) {
root = Integer.parseInt(matcher.group(1));
main = Integer.parseInt(matcher.group(2));
if (TimiJava.isNotEmpty(matcher.group(3))) {
bug = Integer.parseInt(matcher.group(3));
}
}
}
/** @return true 为支持 Fabric */
public boolean isSupportFabric() {
return main < 14;
}
/** @return true 为更现代的样式 */
public boolean isModernStyle() {
return 19 < main;
}
}
/** 版本号(可能因模组加载器而改变) */
private String version;
/** 真实的 Minecraft 版本号 */
private RealVersion realVersion;
/** 标题 */
private String title;
/** 启动类 */
private String mainClass;
/** 需要 Java 版本 */
private String javaVersion;
/** 游戏参数模板 */
private String argsGame;
/** JVM 参数模板 */
private String argsJVM;
/** 启动核心地址 */
private String coreURL;
/** 启动核心 SHA1 */
private String coreSHA1;
/** 核心大小 */
private Long coreSize;
/** 资源版本 */
private String assetsVersion;
/** 资源列表 */
private List<Assets> assetList;
/** 依赖列表 */
private List<Libraries> librarieList;
/** 需下载的文件列表(解析结果,包括资源和依赖文件) */
private List<FileDownload> downloadList;
/**
* 资源
*
* @author 夜雨
* @since 2022-11-21 11:04
*/
@Data
public static class Assets {
/** 哈希值 */
private String hash;
/** 下载 URL */
private String url;
/** 大小 */
private Long size;
}
/**
* 依赖
*
* @author 夜雨
* @since 2022-11-21 11:04
*/
@Data
public static class Libraries {
/** 哈希值 */
private String sha1;
/** 相对路径 */
private String path;
/** 下载 URL */
private String url;
/** 文件大小 */
private Long size;
/** true 为 JNA 依赖 */
private boolean isNatives;
/** @return true 为非 JNA 依赖 */
public boolean isNotNatives() {
return !isNatives;
}
}
}
/**
* 版本设置
*
* @author 夜雨
* @since 2022-12-09 20:31
*/
@Data
public static class Option implements Serializable {
/**
* 登录方式
*
* @author 夜雨
* @since 2022-12-05 23:05
*/
@Getter
@AllArgsConstructor
public enum LoginType implements Serializable {
/** 离线 */
OFFLINE(TimiFXUI.MULTILINGUAL.text("offline"), TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.account.type", TimiFXUI.MULTILINGUAL.text("offline"))),
/** ForeverMC */
FOREVER_MC("ForeverMC", TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.account.type", TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.account.fmc"))),
/** 官方正版验证(未支持) */
MOJANG("Mojang", TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.account.type", TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.account.mojang")));
/** 显示名称 */
final String name;
/** 提示 */
final String tips;
}
/**
* 可选规则
*
* @author 夜雨
* @since 2023-06-17 11:34
*/
@Getter
@AllArgsConstructor
public enum Rules {
/** 体验模式 */
DEMO("is_demo_user"),
/** 自定义尺寸 */
CUSTOM_RESOLUTION("has_custom_resolution"),
/** 快速游戏日志 */
QUICK_PLAYS_SUPPORT("has_quick_plays_support"),
/** 快速单人游戏 */
QUICK_PLAY_SINGLEPLAYER("is_quick_play_singleplayer"),
/** 快速多人游戏 */
QUICK_PLAY_MULTIPLAYER("is_quick_play_multiplayer"),
/** 快速领域游戏 */
QUICK_PLAY_REALMS("is_quick_play_realms");
/** 名称 */
final String name;
}
/** 名称,从整合版版本包获取并储存 */
private StringProperty name;
/** 支持的登录方式 */
private ObjectProperty<LoginType> loginType;
/** 启动 Java */
private StringProperty java;
/** 服务器 */
private StringProperty server;
/** 是否自动连接 */
private BooleanProperty autoConnect;
}
}

View File

@ -0,0 +1,25 @@
package cn.forevermc.launcher.bean;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author 夜雨
* @since 2023-06-20 15:21
*/
@Getter
@AllArgsConstructor
public enum GameDownloadSource {
/** 官方源 */
MOJANG("https://libraries.minecraft.net/", "https://resources.download.minecraft.net/"),
/** BMCLAPI */
BMCLAPI("https://bmclapi2.bangbang93.com/maven/", "https://bmclapi2.bangbang93.com/assets/");
/** 依赖 */
final String libraries;
/** 资源 */
final String resources;
}

View File

@ -0,0 +1,167 @@
package cn.forevermc.launcher.bean;
import cn.forevermc.launcher.service.FMCLoginService;
import cn.forevermc.launcher.service.GameService;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.SimpleObjectProperty;
import com.imyeyu.inject.annotation.Bean;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
/**
* 启动游戏对象
*
* @author 夜雨
* @since 2024-07-20 19:24
*/
@Bean
public class LaunchGame extends SimpleObjectProperty<Game> {
@Inject
private Config config;
@Inject
private GameService gameService;
@Inject
private FMCLoginService fmcLoginService;
/** true 为使用新标题 */
private final BooleanBinding newTitle;
/** true 为使用新 UI */
private final BooleanBinding newUI;
/** true 为远古版本 */
private final BooleanBinding legacy;
/** true 为官方原版 */
private final BooleanBinding original;
/** true 为使用 Fabric */
private final BooleanBinding fabric;
/** true 为支持 ForeverMC 登录 */
private final BooleanBinding supportFMCLogin;
public LaunchGame() {
newTitle = Bindings.createBooleanBinding(() -> {
if (get() == null) {
return false;
}
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(get());
return 1 <= realVersion.getRoot() && 20 <= realVersion.getMain(); // 1.20 <= 使用新标题
}, this);
newUI = Bindings.createBooleanBinding(() -> {
if (get() == null) {
return false;
}
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(get());
return 1 <= realVersion.getRoot() && 21 <= realVersion.getMain(); // 1.21 <= 使用新 UI
}, this);
legacy = Bindings.createBooleanBinding(() -> {
if (get() == null) {
return false;
}
return get().isLegacy();
}, this);
original = Bindings.createBooleanBinding(() -> {
if (get() == null) {
return false;
}
return get().isOriginal();
}, this);
fabric = Bindings.createBooleanBinding(() -> {
if (get() == null) {
return false;
}
return get().isFabric();
}, this);
supportFMCLogin = Bindings.createBooleanBinding(() -> {
if (fmcLoginService == null) {
return false;
}
return fmcLoginService.isSupportFMCLogin(get());
}, this, FMCLoginService.loginModMapProperty(), FMCLoginService.fabricAPIMapProperty());
}
@InvokeForInjected
private void injected() {
String defLaunch = config.getLauncher().getGame().get();
config.getLauncher().getGame().bind(Bindings.createStringBinding(() -> {
if (get() == null) {
return defLaunch;
}
return get().getName();
}, this));
}
public boolean isOffline() {
return get().getOption().getLoginType().get() == Game.Option.LoginType.OFFLINE;
}
public boolean isOnline() {
return !isOffline();
}
public boolean isFMCLogin() {
return get().getOption().getLoginType().get() == Game.Option.LoginType.FOREVER_MC;
}
public boolean isMojangLogin() {
return get().getOption().getLoginType().get() == Game.Option.LoginType.MOJANG;
}
public boolean isNewTitle() {
return newTitle.get();
}
public BooleanBinding newTitleProperty() {
return newTitle;
}
public boolean isNewUI() {
return newUI.get();
}
public BooleanBinding newUIProperty() {
return newUI;
}
public boolean isLegacy() {
return legacy.get();
}
public BooleanBinding legacyProperty() {
return legacy;
}
public boolean isOriginal() {
return original.get();
}
public BooleanBinding originalProperty() {
return original;
}
public boolean isFabric() {
return fabric.get();
}
public BooleanBinding fabricProperty() {
return fabric;
}
public boolean isSupportFMCLogin() {
return supportFMCLogin.get();
}
public boolean isNotSupportFMCLogin() {
return !supportFMCLogin.get();
}
public BooleanBinding supportFMCLoginProperty() {
return supportFMCLogin;
}
}

View File

@ -0,0 +1,32 @@
package cn.forevermc.launcher.bean;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.Background;
import javafx.scene.layout.Border;
import com.imyeyu.fx.ui.components.popup.tips.AbstractPopupTips;
import com.imyeyu.fx.utils.BorderStroke;
/**
* 弹出提示组件
*
* @author 夜雨
* @since 2023-06-08 17:12
*/
public class MCPopupTips extends AbstractPopupTips<Label> {
private static final Border BORDER = new BorderStroke("#25025C").width(3).build();
private static final Insets PADDING = new Insets(4);
public MCPopupTips() {
this("");
}
public MCPopupTips(String text) {
setNode(new Label(text));
getNode().setBorder(BORDER);
getNode().setPadding(PADDING);
getNode().setTextFill(Colorful.WHITE);
getNode().setBackground(Background.EMPTY);
}
}

View File

@ -0,0 +1,44 @@
package cn.forevermc.launcher.bean;
import lombok.Data;
import com.imyeyu.utils.OS;
/**
* OpenJDK
*
* @author 夜雨
* @since 2024-06-10 10:35
*/
@Data
public class OpenJDK {
/**
* 类型
*
* @author 夜雨
* @since 2024-06-10 10:35
*/
public enum Type {
/** 集成开发工具 */
JDK,
/** 运行时 */
JRE
}
/** 类型 */
private Type type;
/** 平台 */
private OS.Platform platform;
/** 版本 */
private String version;
/** 名称 */
private String name;
/** 数据(可能是下载链接,可能是 mongoId */
private String data;
}

View File

@ -0,0 +1,85 @@
package cn.forevermc.launcher.bean;
import lombok.Data;
import java.util.List;
/**
* ForeverMC 版本包
*
* @author 夜雨
* @since 2023-06-13 17:17
*/
@Data
public class Pack {
/** 名称 */
private String name;
/** 版本 */
private String ver;
/** 标题 */
private String title;
/** 简介 */
private String description;
/** 游戏版本 */
private String gameVer;
/** 默认配置 */
private String defOption;
/** 文件大小 */
private long size;
/** true 为过时 */
private boolean isDeprecated;
/** 创建时间 */
private Long createdAt;
/** 更新时间 */
private Long updatedAt;
/** 下载源列表 */
private List<Source> sourceList;
/**
* 下载源
*
* @author 夜雨
* @since 2023-06-13 17:32
*/
@Data
public static class Source {
/**
* 数据类型
*
* @author 夜雨
* @since 2024-06-18 20:11
*/
public enum Type {
/** 附件 */
ATTACH,
/** 地址 */
URL
}
/** 名称 */
private String name;
/** 数据类型 */
private Type type;
/** 数据 */
private String data;
/** true 为默认选择此下载源 */
private boolean isDefault;
}
}

View File

@ -0,0 +1,88 @@
package cn.forevermc.launcher.bean;
import cn.forevermc.launcher.ctrl.pane.About;
import cn.forevermc.launcher.ctrl.pane.CanvasProgress;
import cn.forevermc.launcher.ctrl.pane.Dialog;
import cn.forevermc.launcher.ctrl.pane.GameOption;
import cn.forevermc.launcher.ctrl.pane.GameSelect;
import cn.forevermc.launcher.ctrl.pane.LangSelect;
import cn.forevermc.launcher.ctrl.pane.Launching;
import cn.forevermc.launcher.ctrl.pane.Menu;
import cn.forevermc.launcher.ctrl.pane.Option;
import cn.forevermc.launcher.ctrl.pane.OriginalSelect;
import cn.forevermc.launcher.ctrl.pane.PackSelect;
import cn.forevermc.launcher.ctrl.pane.PlayerSelect;
import cn.forevermc.launcher.ctrl.pane.ResourceDownload;
import cn.forevermc.launcher.view.components.AbstractPane;
import com.imyeyu.inject.TimiInject;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.StaticInject;
/**
* 页面
*
* @author 夜雨
* @since 2022-11-21 16:38
*/
@StaticInject
public enum Page {
/** 会话 */
DIALOG(Dialog.class),
/** 画板进度 */
CANVAS_PROGRESS(CanvasProgress.class),
/** 菜单 */
MENU(Menu.class),
/** 选项 */
OPTION(Option.class),
/** 原版下载 */
ORIGINAL_SELECT(OriginalSelect.class),
/** 整合版选择 */
PACK_SELECT(PackSelect.class),
/** 版本配置 */
PACK_OPTION(GameOption.class),
/** 启动版本选择 */
GAME_SELECT(GameSelect.class),
/** 玩家选择 */
PLAYER_SELECT(PlayerSelect.class),
/** 资源下载 */
RESOURCE_DOWNLOAD(ResourceDownload.class),
/** 启动过渡页 */
LAUNCHING(Launching.class),
/** 语言选择 */
LANG_SELECT(LangSelect.class),
/** 关于 */
ABOUT(About.class);
@Inject
private static TimiInject injector;
/** 页面类 */
final Class<? extends AbstractPane> page;
Page(Class<? extends AbstractPane> page) {
this.page = page;
}
/** @return 页面类 */
public Class<?> getClazz() {
return page;
}
/** @return 从 TimiInject 控制反转对象获取该页面 */
public AbstractPane getIOCPage() {
return injector.di(page);
}
}

View File

@ -0,0 +1,29 @@
package cn.forevermc.launcher.bean;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 运行时下载源
*
* @author 夜雨
* @since 2024-06-07 00:13
*/
@Getter
@AllArgsConstructor
public enum RuntimeDownloadSource {
/** ForeverMC */
FOREVER_MC("ForeverMC", "OpenJDKMirror"),
/** 清华大学 */
TUNA("Tsinghua Tuna", "OpenJDKTunaMirror"),
/** Github */
GITHUB("Github", "OpenJDKGithubMirror");
/** 名称 */
final String name;
final String mirror;
}

View File

@ -0,0 +1,165 @@
package cn.forevermc.launcher.core;
import cn.forevermc.launcher.bean.FileDownload;
import cn.forevermc.launcher.ctrl.pane.ResourceDownload;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.StaticInject;
import com.imyeyu.io.IOSpeedService;
import com.imyeyu.java.bean.Callback;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.java.bean.CallbackReturn;
import com.imyeyu.network.ProgressiveRequest;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.fluent.Request;
import org.apache.hc.core5.util.Timeout;
import java.util.List;
/**
* 下载线程,返回为当前下载文件,内部调度
*
* @author 夜雨
* @since 2022-11-21 13:27
*/
@Slf4j
@StaticInject
public class DownloadThread extends RunAsync<FileDownload> {
@Inject
private static ResourceDownload resourceDownload;
@Inject
private static IOSpeedService.Item ioSpeedItem;
/** 线程 ID */
private static int id = 0;
/** 完成事件 */
@Setter
private CallbackArg<FileDownload> onFinishEvent;
/** 错误事件 */
@Setter
private CallbackArg<FileDownload> onErrorEvent;
/** 结束事件 */
@Setter
private Callback onFinallyEvent;
private final String name;
private final CallbackReturn<FileDownload> fetchFileHandler;
/** true 为中断 */
private boolean isInterrupted = false;
/** 当前下载对象 */
private FileDownload download;
/**
* 默认构造器
*
* @param fetchFileHandler 任务获取回调,此接口不返回空时将持续进行下载文件,返回空时自然结束,非 FX 线程触发
*/
public DownloadThread(CallbackReturn<FileDownload> fetchFileHandler) {
this.fetchFileHandler = fetchFileHandler;
name = "[Download Service-%s] ".formatted(id++);
}
@Override
protected FileDownload call() throws Exception {
if (isInterrupted) {
return null;
}
progress(0);
if (download != null) {
log.info("{}{}", name, download.getUrl());
long size;
if (download.getSize() == null) {
size = ProgressiveRequest.wrap(Request.head(download.getUrl()).connectTimeout(Timeout.ofSeconds(6))).length();
} else {
size = download.getSize();
}
// 下载文件
ProgressiveRequest.get(download.getUrl(), (total, read, now) -> {
ioSpeedItem.push(now);
progress(1D * read / size);
return !isInterrupted;
}).toFile(download.getFile());
}
progress(1);
return download;
}
@Override
protected void onFinish(FileDownload file) {
if (file != null) {
onFinishEvent.handler(file);
}
if (isInterrupted) {
log.info(name + " died");
return;
}
// 继续从回调接口获取并重新开始
download = fetchFileHandler.handler();
if (download != null) {
download.progressProperty().bind(progressProperty());
restart();
} else {
log.info(name + " died");
}
}
@Override
protected void onException(Throwable e) {
if (isInterrupted) {
log.info(name + " died");
return;
}
if (download.getRetry() < 8) {
// 重试
log.warn("download error " + e.getMessage() + " and retry: " + download.getUrl());
download.setRetry(download.getRetry() + 1);
resourceDownload.putDownloadDeque(List.of(download));
} else {
log.error("download fail: " + download.getUrl(), e);
download.setError(true);
}
// TODO 最终错误常驻列表并说明下载失败
onErrorEvent.handler(download);
// 任务异常也需继续抽取队列继续下载
download = fetchFileHandler.handler();
if (download != null) {
download.progressProperty().bind(progressProperty());
restart();
} else {
log.info(name + " died");
}
}
@Override
protected void onFinally() {
onFinallyEvent.handler();
}
@Override
public void start() {
isInterrupted = false;
super.start();
}
@Override
public void restart() {
isInterrupted = false;
super.restart();
}
@Override
public boolean cancel() {
isInterrupted = true;
return super.cancel();
}
}

View File

@ -0,0 +1,750 @@
package cn.forevermc.launcher.core;
import cn.forevermc.launcher.ForeverMC;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.bean.OpenJDK;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.bean.RuntimeDownloadSource;
import cn.forevermc.launcher.ctrl.pane.CanvasProgress;
import cn.forevermc.launcher.ctrl.pane.Dialog;
import cn.forevermc.launcher.ctrl.pane.GameOption;
import cn.forevermc.launcher.ctrl.pane.PlayerSelect;
import cn.forevermc.launcher.ctrl.pane.ResourceDownload;
import cn.forevermc.launcher.service.APISettingService;
import cn.forevermc.launcher.service.FMCLoginService;
import cn.forevermc.launcher.service.GameService;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.util.Path;
import cn.forevermc.launcher.util.Resources;
import com.google.gson.Gson;
import com.imyeyu.compress.CompressType;
import com.imyeyu.compress.Decompressor;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Component;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.io.IO;
import com.imyeyu.io.IOSize;
import com.imyeyu.io.IOSpeedService;
import com.imyeyu.io.JarReader;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.java.bean.CallbackArgReturn;
import com.imyeyu.java.bean.CallbackReturn;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.FileRequest;
import com.imyeyu.network.Network;
import com.imyeyu.network.ProgressiveRequest;
import com.imyeyu.network.TimiRequest;
import com.imyeyu.utils.Digest;
import com.imyeyu.utils.OS;
import com.imyeyu.utils.StringInterpolator;
import com.imyeyu.utils.Text;
import com.imyeyu.utils.Time;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.paint.Paint;
import javafx.stage.Stage;
import lombok.Cleanup;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.fluent.Request;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* 启动器
*
* @author 夜雨
* @since 2022-11-21 13:20
*/
@Slf4j
@Component
public class Launcher implements OS.FileSystem {
/**
* 状态,目前仅内部使用
*
* @author 夜雨
* @since 2023-06-16 15:29
*/
private enum Status {
/** 空闲 */
IDLE,
/** 初始化 */
INITIALIZING,
/** 基础检查 */
BASE_CHECKING,
/** 解析版本包 */
PARSING_PACK,
/** 账号检查 */
ACCOUNT_AUTH,
/** 启动玩家选择 */
PLAYER_SELECT,
/** 资源下载 */
RESOURCE_DOWNLOAD,
/** Java 下载 */
JAVA_DOWNLOAD,
/** JNA 检查 */
JNA_CHECK,
/** 构建启动命令 */
BUILD_COMMAND,
/** 执行启动 */
LAUNCHING,
/** 等待游戏 */
WAITING_GAME,
/** 超时 */
TIMEOUT,
/** 中断 */
INTERRUPT,
/** 成功 */
SUCCEED
}
@Inject
private Config config;
@Inject
private Gson gson;
@Inject
private Stage stage;
@Inject
private Dialog dialog;
@Inject
private GameOption gameOption;
@Inject
private LaunchGame launchGame;
@Inject
private GameService gameService;
@Inject
private PageService pageService;
@Inject
private PlayerSelect playerSelect;
@Inject
private CanvasProgress canvasProgress;
@Inject
private FMCLoginService fmcLoginService;
@Inject
private ResourceDownload resourceDownload;
@Inject
private APISettingService apiSettingService;
@Inject
private IOSpeedService.Item ioSpeedItem;
private final ObjectProperty<Status> status;
/** 线程锁 */
private final Object locker;
public Launcher() {
status = new SimpleObjectProperty<>(Status.IDLE);
locker = new Object();
// 根据状态切换页面
status.addListener((obs, o, newStatus) -> {
if (newStatus != null) {
switch (newStatus) {
case IDLE -> pageService.to(Page.MENU);
case INITIALIZING -> {
dialog.setContent(TimiFXUI.MULTILINGUAL.text("loading"));
dialog.eject();
}
case ACCOUNT_AUTH -> dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.account.checking"));
case PLAYER_SELECT -> pageService.to(Page.PLAYER_SELECT);
case RESOURCE_DOWNLOAD -> pageService.to(Page.RESOURCE_DOWNLOAD);
case JAVA_DOWNLOAD -> {
RuntimeDownloadSource source = config.getMain().getRuntimeDownloadSource().get();
String requiredRTVer = gameService.getRequiredRTVer(launchGame.get());
log.info("required jre runtime ver.{} and download from {}", source, requiredRTVer);
canvasProgress.setTask(new RunAsync<String>() {
/** 镜像地址 */
static final String MIRROR_API = Resources.TIMI_SERVER_API + "/mirror/%s";
@Override
protected String call() throws Exception {
progress(0);
OpenJDK jre = getDownloadJRE(source, requiredRTVer);
String url = getDownloadURL(source, jre);
long jreLength = FileRequest.wrap(Request.head(url)).length();
String fileName = "jre" + jre.getVersion() + "." + Network.uriFileExtension(jre.getName());
File compressedFile = new File("etc" + SEP + fileName);
if (!compressedFile.exists() || compressedFile.length() != jreLength) {
log.info("downloading jre: {}", url);
update("DOWNLOAD");
ProgressiveRequest.get(url, (total, read, now) -> {
ioSpeedItem.push(now);
progress(1D * read / jreLength);
return !isInterrupted;
}).toFile(compressedFile);
}
if (!isInterrupted && compressedFile.exists()) {
update("DECOMPRESS");
log.info("decompressing jre: {}", compressedFile.getAbsolutePath());
{
File jreDir = IO.dir("jre" + jre.getVersion());
Decompressor decompressor = CompressType.fromFile(compressedFile);
decompressor.setFileCallback(file -> message(file.getName()));
decompressor.setProgressCallback(this::progress);
decompressor.run(compressedFile, jreDir.getAbsolutePath());
IO.destroy(compressedFile);
File[] jreFiles = jreDir.listFiles();
if (jreFiles != null && jreFiles.length == 1) {
// 存在根文件夹
File[] files = jreFiles[0].listFiles();
if (TimiJava.isEmpty(files)) {
throw new TimiException(TimiCode.ERROR, "TODO empty jre files");
}
for (int i = 0; i < files.length; i++) {
IO.move(files[i], jreDir.getAbsolutePath());
}
IO.destroy(jreFiles[0]);
}
}
log.info("decompress done");
}
return null;
}
@Override
protected void onUpdate(String status) {
if (TimiJava.isNotEmpty(status)) {
CallbackArg<Double> dlSpeed = d -> Platform.runLater(() -> canvasProgress.getSubLabel().setText(IOSize.format(d, IOSize.Unit.MB) + "/s"));
switch (status) {
case "DOWNLOAD" -> {
ioSpeedItem.addBufferListener(dlSpeed);
canvasProgress.setCanCancel(true);
canvasProgress.setBackgroundColor(TimiFXUI.Colorful.BLACK);
canvasProgress.setColor(Paint.valueOf("#AAA"));
canvasProgress.reset();
}
case "DECOMPRESS" -> {
ioSpeedItem.removeBufferListener(dlSpeed);
canvasProgress.setCanCancel(false);
canvasProgress.setBackgroundColor(Paint.valueOf("#AAA"));
canvasProgress.setColor(Paint.valueOf("#0A0"));
canvasProgress.reset();
}
}
}
}
@Override
protected void onException(Throwable e) {
// TODO 失败处理
}
@Override
protected void onFinally() {
// 释放线程
log.info("runtime download finished and unlocked launcher thread");
synchronized (locker) {
locker.notifyAll();
}
}
/**
* 获取下载链接
*
* @param source 下载源
* @param jreVer 需求运行时版本
* @return 下载链接
* @throws Exception 获取异常
*/
private OpenJDK getDownloadJRE(RuntimeDownloadSource source, String jreVer) throws Exception {
List<OpenJDK> jdkList = TimiRequest.<List<OpenJDK>>get(MIRROR_API.formatted(source.getMirror())).result();
for (int i = 0; i < jdkList.size(); i++) {
OpenJDK jdk = jdkList.get(i);
if (jdk.getPlatform() == OS.PLATFORM && jdk.getType() == OpenJDK.Type.JRE && jdk.getVersion().equals(jreVer)) {
return jdk;
}
}
throw new TimiException(TimiCode.ERROR).msgKey("fmc.launcher.rt.not_support");
}
private String getDownloadURL(RuntimeDownloadSource source, OpenJDK jre) {
return switch (source) {
case FOREVER_MC -> Resources.TIMI_SERVER_API + "/attachment/download/" + jre.getData();
case TUNA, GITHUB -> jre.getData();
};
}
});
canvasProgress.setCanCancel(true);
canvasProgress.setBackgroundColor(TimiFXUI.Colorful.BLACK);
canvasProgress.setColor(Paint.valueOf("#AAA"));
canvasProgress.getLabel().setText(TimiFXUI.MULTILINGUAL.text("fmc.launcher.rt.download.tips", source.getName()));
pageService.to(Page.CANVAS_PROGRESS);
}
case JNA_CHECK -> {
dialog.setTitle("");
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.launching"));
dialog.eject();
}
case WAITING_GAME -> {
if (launchGame.get().isLegacy()) {
stage.getIcons().setAll(Resources.ICON_MC_ANCIENT);
} else {
stage.getIcons().setAll(Resources.ICON_MC);
}
pageService.to(Page.LAUNCHING);
}
}
}
});
}
/** 执行启动 */
public void launch() {
if (status.get() != Status.IDLE) {
throw new TimiException(TimiCode.ERROR_SERVICE_BUSY, "launcher not in idle");
}
log.info("initializing launcher..");
if (status.isBound()) {
status.unbind();
}
status.set(Status.INITIALIZING);
RunAsync<Status> task = new RunAsync<>() {
@Override
protected Status call() throws Exception {
// 基本数据
Game game = launchGame.get();
// 基本校验
update(Status.BASE_CHECKING);
if (game == null) {
log.warn("not select launch game");
throw new LauncherException(Page.GAME_SELECT, TimiFXUI.MULTILINGUAL.text("fmc.launcher.empty_game"));
}
log.info("do launch game: {}", game.getName());
Game.Option packOption = game.getOption();
if (TimiJava.isEmpty(config.getPlayer().getName())) {
log.warn("empty player name");
throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.empty_name"));
}
if (launchGame.isOnline() && TimiJava.isEmpty(config.getPlayer().getPassword())) {
log.warn("empty player password");
throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.empty_password"));
}
// 解码版本包
log.info("parsing game");
update(Status.PARSING_PACK);
gameService.readMetaData(game);
// 验证账号
String token = null;
String playerName;
FMCLoginService.MinecraftPlayer player = null;
{
if (launchGame.isOffline()) {
// 离线
log.info("skip account check for offline mode");
playerName = config.getPlayer().getName().get();
} else {
fmcLoginService.checkLoginMod(game);
// 账号验证
log.info("{}: account checking..", packOption.getLoginType());
update(Status.ACCOUNT_AUTH);
String user = config.getPlayer().getName().get();
String password = config.getPlayer().getPassword().get();
FMCLoginService.TokenResponse resp = fmcLoginService.genLoginToken(user, password);
List<FMCLoginService.MinecraftPlayer> playerList = fmcLoginService.listBoundPlayer(resp.getToken());
if (playerList.isEmpty()) {
log.warn("not bind minecraft player");
throw new LauncherException(Page.MENU, "not bind minecraft player");
}
if (1 < playerList.size()) {
FMCLoginService.MinecraftPlayer playerByName = null;
for (int i = 0; i < playerList.size(); i++) {
if (playerList.get(i).getName().equals(user)) {
playerByName = playerList.get(i);
break;
}
}
if (playerByName != null) {
player = playerByName;
} else {
// 选择启动账号
playerSelect.getItems().setAll(playerList);
playerSelect.setOnCloseEvent(() -> {
synchronized (locker) {
log.info("selected player and unlocked launcher thread");
locker.notifyAll();
}
});
update(Status.PLAYER_SELECT);
// 锁定线程
log.info("locked launcher thread before selected player");
synchronized (locker) {
locker.wait();
}
switch (playerSelect.getAction()) {
case CONFIRM -> player = playerSelect.getSelected();
case CANCEL -> {
return Status.INTERRUPT;
}
}
}
} else {
// 仅有一个启动账号
player = playerList.getFirst();
}
// 登录
FMCLoginService.TokenResponse loginResp = fmcLoginService.doLogin(player, resp.getToken());
playerName = player.getName();
token = loginResp.getToken();
}
}
// 下载资源
{
if (TimiJava.isNotEmpty(game.getMetaData().getDownloadList())) {
log.info("starting resources download");
update(Status.RESOURCE_DOWNLOAD);
resourceDownload.setDownloadDeque(game.getMetaData().getDownloadList());
resourceDownload.setOnFinallyEvent(() -> {
log.info("resources download finished and unlocked launcher thread");
synchronized (locker) {
locker.notifyAll();
}
});
resourceDownload.start();
log.info("locked launcher thread before resources download finish");
// 锁定线程
synchronized (locker) {
locker.wait();
}
}
}
// Java 下载
File validJRE;
CallbackReturn<File> installJava = () -> {
try {
log.info("finding java..");
String jreVer = gameService.getRequiredRTVer(game);
File javaw = new File("jre" + jreVer + SEP + "bin" + SEP + (OS.IS_WINDOWS ? "javaw.exe" : "java"));
if (javaw.exists() && javaw.isFile() && 0 < javaw.length()) {
log.info("found at {}", javaw.getAbsolutePath());
return javaw;
}
log.info("fetch jre list from api..");
// 下载
update(Status.JAVA_DOWNLOAD);
// 锁定线程
log.info("locked launcher thread before runtime download finish");
synchronized (locker) {
locker.wait();
}
return javaw;
} catch (Exception e) {
log.error("download java error", e);
throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.java.error"));
}
};
if (TimiJava.isEmpty(game.getOption().getJava().get())) {
validJRE = installJava.handler();
} else {
File java = new File(game.getOption().getJava().get());
if (java.exists()) {
validJRE = java;
} else {
validJRE = installJava.handler();
}
}
// 检查 JNA
update(Status.JNA_CHECK);
log.info("checking JNA..");
{
File natives = IO.dir(Path.P_PACK + game.getMetaData().getVersion() + SEP + "natives");
final String jnaFormat = OS.IS_WINDOWS ? ".dll" : ".so";
JarReader jarReader;
Map<String, InputStream> jarFiles;
List<Game.MetaData.Libraries> jnaList = game.getMetaData().getLibrarieList().stream().filter(Game.MetaData.Libraries::isNatives).toList();
for (int i = 0; i < jnaList.size(); i++) {
// 读取 JNA jar
File file = new File(Path.P_LIB + jnaList.get(i).getPath());
jarReader = new JarReader(file);
jarFiles = jarReader.getFiles();
// 获取所有文件
for (Map.Entry<String, InputStream> jarFile : jarFiles.entrySet()) {
String path = jarFile.getKey();
if (path.endsWith(jnaFormat)) {
// 获取 JNA
String name;
if (path.contains(SEP)) {
name = path.substring(path.indexOf("/"));
} else {
name = path;
}
// 校验 SHA1
byte[] bytes = jarFile.getValue().readAllBytes();
String jarSHA1 = Digest.sha1(bytes);
File jnaFile = new File(natives.getAbsolutePath() + SEP + name);
if (!jnaFile.exists() || !Digest.sha1(IO.toBytes(jnaFile)).equals(jarSHA1)) {
// JNA 异常,重新导出
IO.toFile(jnaFile, new ByteArrayInputStream(bytes));
}
}
}
jarReader.close();
}
}
// 构建启动命令
update(Status.BUILD_COMMAND);
log.info("building launch command..");
StringBuilder command = new StringBuilder();
{
// 依赖 classpath
String classpath;
{
StringBuilder classpathBuilder = new StringBuilder();
// 一般依赖
List<Game.MetaData.Libraries> classpathList = game.getMetaData().getLibrarieList().stream().filter(Game.MetaData.Libraries::isNotNatives).distinct().toList();
for (int i = 0; i < classpathList.size(); i++) {
classpathBuilder.append(Path.ABS_LIB).append(classpathList.get(i).getPath()).append(';');
}
// 上古依赖
File libDir = new File(IO.fitPath(game.getPath().getAbsolutePath()) + "lib");
if (libDir.exists() && libDir.isDirectory()) {
File[] libs = libDir.listFiles();
if (libs != null) {
for (int i = 0; i < libs.length; i++) {
if (libs[i].getName().endsWith(".jar")) {
classpathBuilder.append(libs[i].getAbsolutePath()).append(';');
}
}
}
}
// 启动核心
classpathBuilder.append(Path.ABS_PACK).append(game.getMetaData().getVersion()).append(SEP).append(game.getMetaData().getVersion()).append(".jar");
classpath = classpathBuilder.toString();
}
CallbackArgReturn<String, String> escapePath = p -> {
if (p.endsWith("\\")) {
return Text.quote(p.substring(0, p.length() - 1)).replace("\\", "\\\\");
} else {
return Text.quote(p).replace("\\", "\\\\");
}
};
// 启动参数
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
Map<String, Object> args = new HashMap<>();
args.put("classpath", escapePath.handler(classpath));
args.put("auth_player_name", Text.quote(playerName));
args.put("version_name", Text.quote(game.getMetaData().getVersion()));
args.put("game_directory", escapePath.handler(Path.ABS_PACK + game.getName())); // 版本隔离,指向详细版本目录
args.put("assets_root", escapePath.handler(Path.ABS_ASSETS));
args.put("game_assets", escapePath.handler(Path.ABS_ASSETS)); // 1.6 以下兼容
args.put("assets_index_name", game.getMetaData().getAssetsVersion());
args.put("auth_uuid", uuid);
args.put("auth_access_token", uuid);
args.put("auth_session", uuid); // 1.6 以下兼容
args.put("clientid", config.getLauncher().getClientId().get());
args.put("auth_xuid", 0);
args.put("user_type", "Legacy");
args.put("version_type", Text.quote("Forever MC"));
args.put("natives_directory", escapePath.handler(Path.ABS_PACK + game.getMetaData().getVersion() + SEP + "natives"));
args.put("launcher_name", Text.quote("Forever MC"));
args.put("launcher_version", ForeverMC.VERSION);
args.put("quickPlayMultiplayer", Text.quote(game.getOption().getServer().get()));
// JRE
command.append('"').append(validJRE.getAbsolutePath()).append('"');
{
// 参数插值
StringInterpolator interpolator = new StringInterpolator(StringInterpolator.DOLLAR_OBJ);
// 内存参数
command.append(" -Xmn256m -Xmx").append(config.getLauncher().getMemory().intValue()).append('M');
if (TimiJava.isNotEmpty(game.getMetaData().getArgsJVM())) {
// JVM 参数
command.append(' ').append(interpolator.inject(game.getMetaData().getArgsJVM(), args));
}
// 主类
command.append(' ').append(game.getMetaData().getMainClass());
// 游戏参数
command.append(' ').append(interpolator.inject(game.getMetaData().getArgsGame(), args));
}
// 令牌
if (TimiJava.isNotEmpty(token)) {
command.append(" --token ").append(Text.quote(token));
}
// 自动进入服务器(上古版本的方式,新版本会在 quickPlayMultiplayer 参数生效)
String server = game.getOption().getServer().get();
if (game.getOption().getAutoConnect().get() && TimiJava.isNotEmpty(server)) {
command.append(" --server ");
int addressSplit = server.indexOf(":");
if (addressSplit == -1) {
command.append(Text.quote(server)).append(" --port \"25565\"");
} else {
String[] address = server.split(":");
command.append(address[0]).append(" --port ").append(Text.quote(address[1]));
}
}
}
// 执行启动命令
update(Status.LAUNCHING);
StringBuilder infoLog = new StringBuilder(); // 辅助检测启动
log.info("do launch: {}", command);
{
IO.toFile(IO.file("etc" + SEP + "latest-script.bat"), command.toString());
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec(command.toString());
// 必须把数据流读出来,否则填满缓冲区会导致阻塞,以至超长时间无启动
new Thread(() -> {
try {
@Cleanup InputStream infoIS = process.getInputStream();
@Cleanup InputStreamReader infoISR = new InputStreamReader(infoIS);
@Cleanup BufferedReader infoBR = new BufferedReader(infoISR);
String line;
infoLog.setLength(0);
while ((line = infoBR.readLine()) != null) {
infoLog.append(line).append(System.lineSeparator());
}
log.info("process info log: \n{}", infoLog);
} catch (IOException e) {
throw new RuntimeException(e);
}
}).start();
new Thread(() -> {
try {
@Cleanup InputStream errorIS = process.getErrorStream();
@Cleanup InputStreamReader errorISR = new InputStreamReader(errorIS);
@Cleanup BufferedReader errorBR = new BufferedReader(errorISR);
String line;
StringBuilder errorLog = new StringBuilder();
while ((line = errorBR.readLine()) != null) {
errorLog.append(line).append(System.lineSeparator());
}
// TODO 启动失败结束线程
log.error("process error log: \n{}", errorLog);
} catch (IOException e) {
throw new RuntimeException(e);
}
}).start();
}
// 等待游戏启动
update(Status.WAITING_GAME);
{
long startAt = Time.now();
String title = "Launching.. " + game.getMetaData().getTitle();
Platform.runLater(() -> stage.setTitle(title));
String logFlag = "Setting user: " + playerName;
if (game.isLegacy()) {
logFlag = "OpenAL initialized";
}
for (int i = 0; i < 64; i++) {
Thread.sleep(1000);
// 通过窗体检测
if (OS.IS_WINDOWS && OS.findProcess4Similarity("javaw.exe", game.getMetaData().getTitle(), title)) {
log.info("found the game window in {} ms", Time.now() - startAt);
return Status.SUCCEED;
}
// 通过日志检测
if (infoLog.indexOf(logFlag) != -1) {
log.info("\"{}\" found in the log indicates successful launch in {} ms", logFlag, Time.now() - startAt);
return Status.SUCCEED;
}
}
}
log.warn("launcher timeout, not found window of title: {}", game.getMetaData().getTitle());
return Status.TIMEOUT;
}
@Override
protected void onFinish(Status status) {
switch (status) {
case INTERRUPT -> update(Status.IDLE);
case SUCCEED -> stage.close();
case TIMEOUT -> {
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.timeout"));
dialog.setConfirmEvent(() -> pageService.to(Page.MENU));
dialog.eject();
}
}
}
@Override
protected void onException(Throwable e) {
dialog.setContent(e.getMessage());
dialog.setCloseEvent(() -> update(Status.IDLE));
if (e instanceof LauncherException le) {
log.info("launch exception: {}", e.getMessage());
dialog.setCloseEvent(() -> {
update(Status.IDLE);
pageService.to(le.toPage);
});
if (le.toPageEvent != null) {
le.toPageEvent.handler();
}
} else {
log.info("launch error", e);
}
dialog.eject();
}
@Override
protected void onFinally() {
log.info("launcher on finally");
synchronized (locker) {
locker.notifyAll();
}
}
};
status.bind(task.valueProperty());
task.messageProperty().addListener((obs, o, newMsg) -> dialog.setContent(newMsg));
task.start();
}
}

View File

@ -0,0 +1,44 @@
package cn.forevermc.launcher.core;
import cn.forevermc.launcher.bean.Page;
import com.imyeyu.java.bean.Callback;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
/**
* 启动异常
*
* @author 夜雨
* @since 2022-12-06 20:01
*/
public class LauncherException extends TimiException {
/** 异常跳转页面 */
Page toPage;
/** 跳转页面事件 */
Callback toPageEvent;
/**
* 简单构造器
*
* @param toPage 异常跳转页面
* @param message 提示消息
*/
public LauncherException(Page toPage, String message) {
this(null, toPage, message);
}
/**
* 默认构造器
*
* @param toPageEvent 跳转前回调
* @param toPage 异常跳转页面
* @param message 提示消息
*/
public LauncherException(Callback toPageEvent, Page toPage, String message) {
super(TimiCode.ERROR, message);
this.toPage = toPage;
this.toPageEvent = toPageEvent;
}
}

View File

@ -0,0 +1,94 @@
package cn.forevermc.launcher.ctrl;
import cn.forevermc.launcher.ForeverMC;
import cn.forevermc.launcher.bean.APISetting;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.service.GameService;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.util.Sound;
import cn.forevermc.launcher.view.ViewMain;
import com.google.gson.Gson;
import javafx.beans.property.ObjectProperty;
import javafx.scene.media.MediaPlayer;
import javafx.stage.Stage;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.inject.TimiInject;
import com.imyeyu.inject.annotation.IOCReturn;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.SuperInject;
import com.imyeyu.io.IOSpeedService;
import java.awt.SplashScreen;
/**
* 主控
*
* @author 夜雨
* @since 2022-11-18 15:37
*/
@Slf4j
@SuperInject
public class Main extends ViewMain {
@Inject
private Config config;
@Inject
private Gson gson;
@Inject
private APISetting apiSetting;
@Inject
private PageService pageService;
@Inject
private GameService gameService;
@Inject
private LaunchGame launchGame;
@Inject
private ObjectProperty<MediaPlayer> bgmPlayer;
private Stage stage;
@Override
public void start(Stage stage) {
this.stage = stage;
TimiInject.run(ForeverMC.getInjectApp()).ioc(this);
pageService.to(Page.MENU);
super.start(stage);
stage.show();
if (SplashScreen.getSplashScreen() != null) {
SplashScreen.getSplashScreen().close();
}
// 音效音量
Sound.CLICK.volumeProperty().bind(config.getMain().getSoundVolume().divide(100));
}
@Override
public void stop() {
log.info("doing shutdown..");
stage.close();
if (launchGame.get() != null) {
gameService.storeOption(launchGame.get());
}
ForeverMC.getConfigLoader().dump();
IOSpeedService.getInstance().shutdown();
}
/** 主窗体 */
@IOCReturn
public Stage stage() {
return stage;
}
}

View File

@ -0,0 +1,53 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.service.GameService;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.view.pages.AboutPane;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.SuperInject;
/**
* 关于
*
* @author 夜雨
* @since 2022-11-25 15:02
*/
@Controller
@SuperInject
public class About extends AboutPane {
@Inject
private PageService pageService;
@Inject
private GameService gameService;
@Inject
private LaunchGame launchGame;
public About() {
close.setOnAction(e -> pageService.to(Page.MENU));
}
@InvokeForInjected
private void injected() {
launchGame.addListener((obs, o, newLaunchGame) -> {
if (newLaunchGame == null) {
title.setImage(Resources.TITLE);
} else {
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(newLaunchGame);
if (realVersion.isModernStyle()) {
title.setImage(Resources.TITLE_NEW);
} else {
title.setImage(Resources.TITLE);
}
}
});
}
}

View File

@ -0,0 +1,219 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.ctrl.Main;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.view.pages.CanvasProgressPane;
import com.google.gson.Gson;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.input.MouseEvent;
import javafx.scene.media.MediaPlayer;
import javafx.scene.paint.Paint;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.utils.AnimationRenderer;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.SuperInject;
import com.imyeyu.utils.OS;
import java.util.ArrayList;
import java.util.List;
/**
* 画板进度
*
* @author 夜雨
* @since 2023-06-15 17:49
*/
@Slf4j
@Controller
@SuperInject
public class CanvasProgress extends CanvasProgressPane implements OS.FileSystem {
private static final String PACK_CACHE_PATH = "etc" + SEP + "pack";
@Inject
private Config config;
@Inject
private Main main;
@Inject
private Gson gson;
@Inject
private Dialog dialog;
@Inject
private PageService pageService;
@Inject
private ObjectProperty<MediaPlayer> bgmPlayer;
private final List<Pixel> pixelList;
private final DoubleProperty progress;
private final AnimationRenderer renderer;
private final ObjectProperty<Paint> color;
private final ObjectProperty<Paint> backgroundColor;
@Getter
private RunAsync<?> task;
private int i = 0;
public CanvasProgress() {
pixelList = new ArrayList<>();
progress = new SimpleDoubleProperty(0);
color = new SimpleObjectProperty<>(Colorful.PINK);
backgroundColor = new SimpleObjectProperty<>(Colorful.BLACK);
// 图形进度
canvas.visibleProperty().bind(progress.isNotEqualTo(-1));
canvas.managedProperty().bind(canvas.visibleProperty());
int unitSize = 4;
int center = (SIZE - 4) / 2;
// 层级
int layerMax = center / unitSize;
for (int layer = 0; layer <= layerMax; layer++) {
// 上
for (int i = -layer + 1; i <= layer; i++) {
int x = center + i * unitSize;
int y = center - layer * unitSize;
pixelList.add(new Pixel(x, y));
}
// 右
for (int i = -layer; i <= layer; i++) {
int x = center + layer * unitSize;
int y = center + i * unitSize;
pixelList.add(new Pixel(x, y));
}
// 下
for (int i = -layer; i <= layer; i++) {
int x = center - i * unitSize;
int y = center + layer * unitSize;
pixelList.add(new Pixel(x, y));
}
// 左
for (int i = -layer; i <= layer + 1; i++) {
int x = center - layer * unitSize;
int y = center - i * unitSize;
pixelList.add(new Pixel(x, y));
}
}
// 图形进度渲染
renderer = new AnimationRenderer();
renderer.addRenderCallback(aDouble -> {
while (1D * i / pixelList.size() < progress.get() && i < pixelList.size()) {
Pixel pixel = pixelList.get(i);
g.setFill(color.get());
g.fillRect(pixel.x, pixel.y, 4, 4);
i++;
}
});
// ---------- 事件 ----------
// 背景音乐
bgmToggle.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
if (bgmPlayer.get() == null) {
e.consume();
}
});
bgmToggle.selectedProperty().addListener((obs, o, isSelected) -> {
MediaPlayer player = bgmPlayer.get();
if (player != null) {
if (isSelected) {
player.play();
} else {
player.pause();
}
}
});
// 取消
cancel.setOnAction(e -> task.interrupt());
}
@Override
public void onShow() {
reset();
bgmToggle.visibleProperty().bind(config.getMain().getBgmVolume().greaterThan(0));
renderer.start();
if (progress.isBound()) {
progress.unbind();
}
log.info("start downloading: ");
task.messageProperty().addListener((obs, o, compressedFile) -> subLabel.setText(compressedFile));
progress.bind(task.progressProperty());
task.start();
}
@Override
public void onHide() {
renderer.stop();
progress.unbind();
label.setText("");
subLabel.setText("");
}
public void reset() {
i = 0;
{
g.setFill(backgroundColor.get());
g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
}
}
public <T> void setTask(RunAsync<T> task) {
this.task = task;
}
public void setCanCancel(boolean canCancel) {
cancel.setDisable(!canCancel);
}
public BooleanBinding canCancelProperty() {
return cancel.disableProperty().not();
}
public void setColor(Paint color) {
this.color.set(color);
}
public ObjectProperty<Paint> colorProperty() {
return color;
}
public void setBackgroundColor(Paint backgroundColor) {
this.backgroundColor.set(backgroundColor);
}
public ObjectProperty<Paint> backgroundColorProperty() {
return backgroundColor;
}
/**
*
*
* @author 夜雨
* @since 2024-07-01 23:24
*/
@AllArgsConstructor
private static class Pixel {
int x;
int y;
}
}

View File

@ -0,0 +1,139 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.pages.DialogPane;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.SuperInject;
import com.imyeyu.java.bean.Callback;
/**
* 会话,按钮显示属性绑定事件,存在则显示
*
* @author 夜雨
* @since 2022-11-28 17:34
*/
@Controller
@SuperInject
public class Dialog extends DialogPane {
@Inject
private PageService pageService;
private final ObjectProperty<Callback> confirmEvent, denyEvent, cancelEvent, closeEvent;
public Dialog() {
confirmEvent = new SimpleObjectProperty<>();
denyEvent = new SimpleObjectProperty<>();
cancelEvent = new SimpleObjectProperty<>();
closeEvent = new SimpleObjectProperty<>();
confirm.visibleProperty().bind(confirmEvent.isNotNull());
deny.visibleProperty().bind(denyEvent.isNotNull());
cancel.visibleProperty().bind(cancelEvent.isNotNull());
close.visibleProperty().bind(closeEvent.isNotNull());
confirm.setOnAction(e -> confirmEvent.get().handler());
deny.setOnAction(e -> denyEvent.get().handler());
cancel.setOnAction(e -> cancelEvent.get().handler());
close.setOnAction(e -> closeEvent.get().handler());
}
/** 显示会话 */
public void eject() {
pageService.to(Page.DIALOG);
}
/**
* 显示会话
*
* @param content 内容
*/
public void show(String content) {
show("", content);
}
/**
* 显示会话
*
* @param title 标题
* @param content 内容
*/
public void show(String title, String content) {
setTitle(title);
setContent(content);
setCloseEvent(() -> pageService.back());
eject();
}
@Override
public void onHide() {
title.setText("");
content.setText("");
confirmEvent.set(null);
denyEvent.set(null);
cancelEvent.set(null);
closeEvent.set(null);
buttonPane.getChildren().setAll(confirm, deny, cancel, close);
}
/** @param title 设置标题 */
public void setTitle(String title) {
super.title.setText(title);
}
/** @param content 设置内容 */
public void setContent(String content) {
super.content.setText(content);
}
/** @param buttons 设置按钮 */
public void setButtons(MCButton... buttons) {
buttonPane.getChildren().setAll(buttons);
}
/** 清除按钮 */
public void clearButtons() {
buttonPane.getChildren().clear();
}
/** @param confirmEvent 确认事件 */
public void setConfirmEvent(Callback confirmEvent) {
this.confirmEvent.set(confirmEvent);
}
/** @param denyEvent 拒绝事件 */
public void setDenyEvent(Callback denyEvent) {
this.denyEvent.set(denyEvent);
}
/** @param cancelEvent 取消事件 */
public void setCancelEvent(Callback cancelEvent) {
this.cancelEvent.set(cancelEvent);
}
/** @param closeEvent 关闭事件 */
public void setCloseEvent(Callback closeEvent) {
this.closeEvent.set(closeEvent);
}
/** @return 自定义组件 */
public Node getGraphic() {
return graphic.get();
}
/** @return 自定义组件属性 */
public ObjectProperty<Node> graphicProperty() {
return graphic;
}
/** @param graphic 设置自定义组件 */
public void setGraphic(Node graphic) {
this.graphic.set(graphic);
}
}

View File

@ -0,0 +1,176 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.service.FMCLoginService;
import cn.forevermc.launcher.service.GameService;
import cn.forevermc.launcher.service.MCPopupTipsService;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.util.FabricBuilder;
import cn.forevermc.launcher.view.pages.GameOptionPane;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.FileSelector;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.SuperInject;
import com.imyeyu.java.TimiJava;
import java.io.File;
/**
* 版本配置
*
* @author 夜雨
* @since 2022-12-10 00:50
*/
@Slf4j
@Controller
@SuperInject
public class GameOption extends GameOptionPane {
@Inject
private Config config;
@Inject
private Dialog dialog;
@Inject
private LaunchGame launchGame;
@Inject
private GameService gameService;
@Inject
private PageService pageService;
@Inject
private FMCLoginService fmcLoginService;
@Inject
private FabricBuilder fabricBuilder;
@Inject
private MCPopupTipsService mcPopupTipsService;
private final ObjectProperty<Game> game;
public GameOption() {
game = new SimpleObjectProperty<>();
// 选择 Java
javaSelect.setOnAction(e -> {
FileSelector selector = new FileSelector(java.getText());
selector.setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.java.select_title"));
selector.addFilter("Java", "javaw.exe;java.exe;java");
File file = selector.single(getScene().getWindow());
if (file != null) {
java.setText(file.getAbsolutePath());
}
});
// 安装 Fabric
installFabric.disableProperty().bind(Bindings.createBooleanBinding(() -> {
Game game = this.game.get();
if (game == null) {
return true;
}
return game.isFabric() || gameService.getRealVersion(game).isSupportFabric();
}, game));
installFabric.setOnAction(e -> {
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.fabric.installing"));
dialog.eject();
new FabricBuilder() {
{
setBuildGame(game.get());
}
@Override
protected void onFinish(Game builedGame) {
pageService.to(Page.GAME_SELECT);
if (fmcLoginService.isSupportFMCLogin(game.get())) {
game.get().getOption().getLoginType().set(Game.Option.LoginType.FOREVER_MC);
}
}
@Override
protected void onException(Throwable e) {
// TODO 出错应该返回整合包具体设置页
dialog.setContent(e.getMessage());
dialog.setCloseEvent(() -> pageService.to(Page.GAME_SELECT));
dialog.eject();
log.error("fabric install error", e);
}
}.start();
});
// 自动连接
autoConnect.disableProperty().bind(Bindings.createBooleanBinding(() -> TimiJava.isEmpty(server.getText()), server.textProperty()));
server.textProperty().addListener((obs, o, newServer) -> autoConnect.setValue(TimiJava.isEmpty(newServer)));
// 完成
finish.setOnAction(e -> pageService.to(Page.GAME_SELECT));
// 编辑版本包变更
game.addListener((obs, oldGame, newGame) -> {
loginType.getItems().clear();
if (oldGame != null) {
java.textProperty().unbindBidirectional(newGame.getOption().getJava());
autoConnect.valueProperty().unbindBidirectional(newGame.getOption().getAutoConnect());
server.textProperty().unbindBidirectional(newGame.getOption().getServer());
loginType.valueProperty().unbindBidirectional(newGame.getOption().getLoginType());
setTitle("");
java.clear();
autoConnect.setValue(false);
server.clear();
loginType.setDisable(true);
loginType.setValue(Game.Option.LoginType.OFFLINE);
installFabric.disableProperty().unbind();
}
if (newGame != null) {
setTitle(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.pack_option.title", newGame.getName()));
if (fmcLoginService.isSupportFMCLogin(newGame)) {
loginType.getItems().setAll(Game.Option.LoginType.OFFLINE, Game.Option.LoginType.FOREVER_MC);
} else {
loginType.setDisable(true);
}
java.textProperty().bindBidirectional(newGame.getOption().getJava());
autoConnect.valueProperty().bindBidirectional(newGame.getOption().getAutoConnect());
server.textProperty().bindBidirectional(newGame.getOption().getServer());
loginType.valueProperty().bindBidirectional(newGame.getOption().getLoginType());
}
});
}
@InvokeForInjected
private void injected() {
mcPopupTipsService.install(autoConnect, TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.auto_connect.tips"));
mcPopupTipsService.install(loginType, TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.login_type.tips"));
// TODO auto download runtime
}
@Override
protected void onHide() {
gameService.storeOption(game.get());
}
/**
* 设置编辑配置的版本包
*
* @param game 版本包
*/
public void setGame(Game game) {
this.game.set(game);
}
}

View File

@ -0,0 +1,229 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.service.GameService;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.util.Path;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCLabel;
import cn.forevermc.launcher.view.pages.GameSelectPane;
import com.google.gson.Gson;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.task.RunAsyncScheduled;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.SuperInject;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* 启动选择
*
* @author 夜雨
* @since 2022-11-28 10:22
*/
@Slf4j
@Controller
@SuperInject
public class GameSelect extends GameSelectPane {
@Inject
private Config config;
@Inject
private Gson gson;
@Inject
private Dialog dialog;
@Inject
private GameOption gameOption;
@Inject
private PageService pageService;
@Inject
private GameService gameService;
@Inject
private LaunchGame launchGame;
/** 记录从哪个页面进入的启动选择 */
private Page from;
public GameSelect() {
// 官方原版下载
originalDL.setOnAction(e -> pageService.to(Page.ORIGINAL_SELECT));
// 整合版下载
packDL.setOnAction(e -> pageService.to(Page.PACK_SELECT));
// 版本设置
option.setOnAction(e -> {
gameOption.setGame(list.getSelectionModel().getSelectedItem());
pageService.to(Page.PACK_OPTION);
});
// 删除
destory.setOnAction(e -> {
Game selectedGame = list.getSelectionModel().getSelectedItem();
dialog.setTitle(TimiFXUI.MULTILINGUAL.text("warning"));
dialog.setContent(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.launch_select.delete.tips", selectedGame.getName()));
dialog.getTitle().setTextFill(Colorful.RED);
MCButton confirm = new MCButton(ComponentSize.NORMAL, TimiFXUI.MULTILINGUAL.text("confirm"));
confirm.setDisable(true);
MCButton cancel = new MCButton(ComponentSize.NORMAL, TimiFXUI.MULTILINGUAL.text("cancel"));
// 倒数
new RunAsyncScheduled<Integer>(Duration.seconds(1)) {
int value = 5;
@Override
protected Integer call() {
return value--;
}
@Override
protected void onFinish(Integer value) {
confirm.getLabel().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.launch_select.delete.confirm", value));
if (value < 0) {
confirm.getLabel().setText(TimiFXUI.MULTILINGUAL.text("confirm"));
confirm.setDisable(false);
cancel();
}
}
}.start();
confirm.setOnAction(subE -> {
dialog.setTitle("");
dialog.setContent(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.launch_select.delete.working", selectedGame.getName()));
dialog.clearButtons();
new RunAsync<Void>() {
@Override
protected Void call() throws Exception {
Thread.sleep(500);
IO.destroy(selectedGame.getPath());
return null;
}
@Override
protected void onFinish() {
pageService.to(Page.GAME_SELECT);
}
@Override
protected void onException(Throwable e) {
dialog.setTitle(TimiFXUI.MULTILINGUAL.text("delete.error"));
dialog.setContent(e.getMessage());
dialog.setCloseEvent(() -> pageService.to(Page.GAME_SELECT));
}
}.start();
});
cancel.setOnAction(subE -> pageService.to(Page.GAME_SELECT));
dialog.setOnHideEvent(() -> {
dialog.getTitle().setTextFill(Colorful.LIGHT_GRAY);
dialog.setOnHideEvent(null);
});
dialog.setButtons(confirm, cancel);
dialog.eject();
});
// 选择
select.setOnAction(e -> {
if (launchGame.get() != null && launchGame.get().equals(list.getSelectionModel().getSelectedItem())) {
pageService.to(TimiJava.firstNotNull(from, Page.OPTION));
} else {
launchGame.set(list.getSelectionModel().getSelectedItem());
}
});
// 返回
back.setOnAction(e -> pageService.to(TimiJava.firstNotNull(from, Page.OPTION)));
}
@InvokeForInjected
private void injected() {
parseList();
}
@Override
public void onShow() {
if (pageService.getPrev() == Page.MENU || pageService.getPrev() == Page.OPTION) {
from = pageService.getPrev();
} else if (pageService.getPrev() == Page.DIALOG) {
// 启动失败的弹窗
from = Page.MENU;
}
parseList();
}
/** 解析版本列表 */
private void parseList() {
setLoading(true);
new RunAsync<List<Game>>() {
/** 启动版本 */
private Game launchGame;
@Override
protected List<Game> call() throws Exception {
List<Game> result = new ArrayList<>();
File[] games = IO.dir(Path.P_PACK).listFiles();
if (games != null) {
for (int i = 0; i < games.length; i++) {
File json = new File(IO.fitPath(games[i].getAbsolutePath()) + games[i].getName() + ".json");
if (json.exists() && json.isFile()) {
// 只有版本 JSON 存在时才识别为正常版本包
try {
Game game = gameService.buildGame(games[i]);
if (game.getName().equals(config.getLauncher().getGame().get())) {
this.launchGame = game;
}
result.add(game);
} catch (IOException e) {
log.error("parse pack data error", e);
}
}
}
}
return result;
}
@Override
protected void onFinish(List<Game> gameData) {
list.getItems().setAll(gameData);
list.getSelectionModel().select(launchGame);
GameSelect.this.launchGame.set(launchGame);
if (list.getItems().size() == 1) {
list.getSelectionModel().select(0);
GameSelect.this.launchGame.set(list.getSelectionModel().getSelectedItem());
}
}
@Override
protected void onFinally() {
setLoading(false);
if (list.getItems().isEmpty()) {
list.placeholderProperty().unbind();
list.setPlaceholder(new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.launch_select.empty")));
}
}
}.start();
}
}

View File

@ -0,0 +1,84 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.ctrl.Main;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.view.pages.LangSelectPane;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.fx.TimiFX;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.TimiInject;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.SuperInject;
import com.imyeyu.io.IO;
import com.imyeyu.java.bean.Language;
import com.imyeyu.utils.OS;
/**
* 语言选择
*
* @author 夜雨
* @since 2022-12-02 00:18
*/
@Slf4j
@Controller
@SuperInject
public class LangSelect extends LangSelectPane implements OS.FileSystem {
@Inject
private Config config;
@Inject
private Main main;
@Inject
private Dialog dialog;
@Inject
private PageService pageService;
public LangSelect() {
// 确认
confirm.setOnAction(e -> {
Language selectedLang = list.getSelectionModel().getSelectedItem();
if (selectedLang.equals(config.getMain().getLanguage().get())) {
// 没有修改
pageService.to(Page.MENU);
return;
}
dialog.setContent(TimiFXUI.MULTILINGUAL.text("tips.restart"));
dialog.setConfirmEvent(() -> {
config.getMain().getLanguage().set(selectedLang);
// JRE
String jre = System.getProperty("java.home") + SEP + "bin" + SEP + "java";
// 启动参数
String param = " -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar ";
// 启动 Jar
String jar = IO.getJarAbsolutePath(getClass());
// 重启
try {
TimiFX.doRestart(main, jre + param + jar);
} catch (Exception ex) {
dialog.setContent(TimiFXUI.MULTILINGUAL.text("tips.restart.error"));
dialog.setConfirmEvent(null);
dialog.setCancelEvent(null);
dialog.setCloseEvent(() -> pageService.to(Page.MENU));
log.error("restart error", ex);
}
});
dialog.setCancelEvent(() -> pageService.to(Page.MENU));
dialog.eject();
});
// 取消
cancel.setOnAction(e -> pageService.to(Page.MENU));
}
@Override
public void onShow() {
list.getItems().setAll(Language.values());
list.getSelectionModel().select(config.getMain().getLanguage().get());
}
}

View File

@ -0,0 +1,82 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.view.pages.LaunchingPane;
import javafx.animation.FadeTransition;
import javafx.beans.binding.Bindings;
import javafx.scene.layout.Background;
import javafx.stage.Stage;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.fx.utils.BgFill;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
/**
* 启动过渡页
*
* @author 夜雨
* @since 2022-11-23 17:32
*/
@Slf4j
@Controller
public class Launching extends LaunchingPane {
/** 正常背景 */
private static final Background BG_NORMAL = new BgFill("#EF323D").build();
/** 远古版本背景 */
private static final Background BG_ANCIENT = BG.WHITE;
@Inject
private Config config;
@Inject
private Stage stage;
@Inject
private LaunchGame launchGame;
private final FadeTransition bgTransition, mojangTransition;
public Launching() {
// 背景过渡
bgTransition = new FadeTransition(Duration.millis(500), root);
bgTransition.setDelay(Duration.millis(500));
bgTransition.setFromValue(0);
bgTransition.setToValue(1);
// LOGO 过渡
mojangTransition = new FadeTransition(Duration.millis(800), mojang);
mojangTransition.setDelay(Duration.millis(1000));
mojangTransition.setFromValue(0);
mojangTransition.setToValue(1);
}
@InvokeForInjected
private void injected() {
// Logo 图片
mojang.imageProperty().bind(Bindings.createObjectBinding(() -> {
if (launchGame.get() == null || launchGame.get().isLegacy()) {
return Resources.MOJANG_ANCIENT;
}
return Resources.MOJANG;
}, launchGame));
// 背景
root.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
if (mojang.getImage().equals(Resources.MOJANG)) {
return BG_NORMAL;
}
return BG_ANCIENT;
}, mojang.imageProperty()));
}
@Override
public void onShow() {
bgTransition.play();
mojangTransition.play();
}
}

View File

@ -0,0 +1,287 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.bean.MCPopupTips;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.core.Launcher;
import cn.forevermc.launcher.service.BGService;
import cn.forevermc.launcher.service.FMCLoginService;
import cn.forevermc.launcher.service.GameService;
import cn.forevermc.launcher.service.MCPopupTipsService;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.service.SplashService;
import cn.forevermc.launcher.util.FabricBuilder;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.view.pages.MenuPane;
import com.sun.javafx.scene.control.skin.Utils;
import javafx.animation.ScaleTransition;
import javafx.animation.Timeline;
import javafx.beans.binding.Bindings;
import javafx.geometry.VPos;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.effect.DropShadow;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Duration;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.ui.MinecraftFont;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.SuperInject;
import com.imyeyu.java.TimiJava;
import com.imyeyu.network.Network;
/**
* 菜单
*
* @author 夜雨
* @since 2022-11-22 00:27
*/
@Slf4j
@Controller
@SuperInject
public class Menu extends MenuPane {
@Inject
private Config config;
@Inject
private Stage stage;
@Inject
private Dialog dialog;
@Inject
private Launcher launcher;
@Inject
private BGService bgService;
@Inject
private GameOption gameOption;
@Inject
private GameService gameService;
@Inject
private PageService pageService;
@Inject
private LaunchGame launchGame;
@Inject
private FMCLoginService fmcLoginService;
@Inject
private SplashService splashService;
@Inject
private MCPopupTipsService mcPopupTipsService;
private final AutoLaunchService autoLaunchService;
public Menu() {
autoLaunchService = new AutoLaunchService();
// 闪烁标语
GraphicsContext g = splashCanvas.getGraphicsContext2D();
g.setFill(Colorful.YELLOW);
g.setFont(MinecraftFont.X32());
g.setEffect(new DropShadow(0, 1, 1, Color.BLACK));
g.setTextBaseline(VPos.TOP);
splashCanvas.widthProperty().bind(Bindings.createDoubleBinding(() -> {
if (splash.isEmpty().get()) {
return 1D;
}
return Utils.computeTextWidth(g.getFont(), splash.get(), -1);
}, splash));
ScaleTransition transition = new ScaleTransition(Duration.millis(300), splashCanvas);
transition.setFromX(.9);
transition.setFromY(.9);
transition.setToX(1);
transition.setToY(1);
transition.setCycleCount(Timeline.INDEFINITE);
transition.setAutoReverse(true);
transition.play();
// 账号管理
fmcAccount.visibleProperty().bind(loginTypeSelector.valueProperty().isEqualTo(Game.Option.LoginType.FOREVER_MC));
fmcAccount.setOnAction(e -> Network.openURIInBrowser("https://space.imyeyu.net"));
// 服务器状态
fmcServer.visibleProperty().bind(loginTypeSelector.valueProperty().isEqualTo(Game.Option.LoginType.FOREVER_MC));
// 启动版本管理
launchGameSelect.setOnAction(e -> pageService.to(Page.GAME_SELECT));
// 启动
launch.setOnAction(e -> {
if (autoLaunchService.isRunning()) {
autoLaunchService.isCancel = true;
return;
}
// 安装 Fabric
if (loginTypeSelector.isOnline() && launchGame.isSupportFMCLogin() && !launchGame.isFabric()) {
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.fabric.installing"));
dialog.eject();
new FabricBuilder() {
{
setBuildGame(launchGame.get());
}
@Override
protected void onFinish(Game builedGame) {
launchGame.set(builedGame);
// TODO 应该自动设置?
config.getLauncher().getGame().set(builedGame.getName());
loginTypeSelector.setValue(Game.Option.LoginType.FOREVER_MC);
launcher.launch();
}
@Override
protected void onException(Throwable e) {
dialog.setContent(e.getMessage());
dialog.setCloseEvent(() -> pageService.to(Page.MENU));
dialog.eject();
log.error("fabric install error", e);
}
}.start();
return;
}
// 启动
launcher.launch();
});
launch.getLabel().textProperty().bind(Bindings.createStringBinding(() -> {
if (!autoLaunchService.isCancel && autoLaunchService.isRunning()) {
if (launch.isHover()) {
return TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.launch.auto.cancel");
} else {
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.launch.auto", autoLaunchService.getValue());
}
} else {
return TimiFXUI.MULTILINGUAL.text("launch");
}
}, autoLaunchService.valueProperty(), autoLaunchService.stateProperty(), launch.hoverProperty()));
// 语言
langSelect.setOnAction(e -> pageService.to(Page.LANG_SELECT));
// 设置
super.option.setOnAction(e -> pageService.to(Page.OPTION));
// 退出
exit.setOnAction(e -> stage.close());
// 关于
super.about.setOnAction(e -> pageService.to(Page.ABOUT));
}
@InvokeForInjected
private void config() {
name.textProperty().bindBidirectional(config.getPlayer().getName());
password.textProperty().bindBidirectional(config.getPlayer().getPassword());
}
@InvokeForInjected
private void injected() {
// 闪烁标语
splashService.valueProperty().addListener((obs, o, newText) -> {
GraphicsContext g = splashCanvas.getGraphicsContext2D();
g.clearRect(0, 0, splashCanvas.getWidth(), splashCanvas.getHeight());
g.fillText(newText, 0, 0);
});
// 动态标题
title.imageProperty().bind(Bindings.when(launchGame.newTitleProperty()).then(Resources.TITLE_NEW).otherwise(Resources.TITLE));
// 账号管理
mcPopupTipsService.install(fmcAccount, TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.account"));
// 账号验证方式
launchGame.addListener((obs, oldGame, newGame) -> {
// 配置绑定
if (oldGame != null && oldGame.getOption().getLoginType().isBound()) {
oldGame.getOption().getLoginType().unbind();
}
loginTypeSelector.setValue(newGame.getOption().getLoginType().get());
newGame.getOption().getLoginType().bind(loginTypeSelector.valueProperty());
});
// 必须离线启动
loginTypeSelector.requiredOfflineProperty().bind(launchGame.supportFMCLoginProperty().not());
// 账号验证方式提示
MCPopupTips loginTypeTips = new MCPopupTips();
loginTypeTips.enableProperty().bind(loginTypeTips.getNode().textProperty().isNotEmpty());
loginTypeTips.getNode().setWrapText(true);
loginTypeTips.getNode().setMaxWidth(280);
loginTypeTips.getNode().textProperty().bind(Bindings.createStringBinding(() -> loginTypeSelector.getValue().getTips(), launchGame, loginTypeSelector.valueProperty()));
mcPopupTipsService.install(loginTypeSelector, loginTypeTips);
// 启动版本
launchGameLabel.textProperty().bind(Bindings.createStringBinding(() -> {
Game launchGame = this.launchGame.get();
if (launchGame == null) {
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.launch.pack", TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.launch.pack.null"));
}
// 版本名称
String gameName = launchGame.getOption().getName().get();
if (TimiJava.isEmpty(gameName)) {
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.launch.pack", launchGame.getName());
}
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.launch.pack", gameName);
}, launchGame));
// 服务器状态
mcPopupTipsService.install(fmcServer, TimiFXUI.MULTILINGUAL.text("server.status"));
// 启动版本管理
mcPopupTipsService.install(launchGameSelect, TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.launch.pack.list"));
// 获取标语
splash.bind(splashService.valueProperty());
}
@Override
public void onShow() {
super.onShow();
// 离线
if (launchGame.isNotSupportFMCLogin()) {
loginTypeSelector.setValue(Game.Option.LoginType.OFFLINE);
}
// 自动启动
if (isFirstShow && config.getLauncher().getAutoStartup().get()) {
autoLaunchService.start();
}
}
/**
* 自启服务
*
* @author 夜雨
* @since 2022-12-03 20:18
*/
private class AutoLaunchService extends RunAsync<Integer> {
/** true 为取消 */
boolean isCancel = false;
@Override
protected Integer call() throws Exception {
int i = 8;
while (0 < i) {
update(i--);
if (isCancel) {
break;
}
Thread.sleep(1000);
}
return 0;
}
@Override
protected void onFinish() {
if (!isCancel) {
launcher.launch();
}
}
}
}

View File

@ -0,0 +1,82 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.service.MCPopupTipsService;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.view.pages.OptionPane;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.SuperInject;
/**
* 选项
*
* @author 夜雨
* @since 2022-11-24 22:27
*/
@Controller
@SuperInject
public class Option extends OptionPane {
@Inject
private Config config;
@Inject
private PageService pageService;
@Inject
private MCPopupTipsService mcPopupTipsService;
public Option() {
// 内存
final double maxMem60p = (memory.getMax() / 1024 * .6) * 1024; // 总内存 60%
final double maxMem80p = (memory.getMax() / 1024 * .8) * 1024; // 总内存 80%
memory.valueProperty().addListener((obs, o, newMemory) -> {
final int mb = newMemory.intValue();
if (mb < 2048 || (maxMem60p < mb && mb <= maxMem80p)) {
memory.getLabel().setTextFill(Colorful.ORANGE);
} else {
if (maxMem80p < mb) {
memory.getLabel().setTextFill(Colorful.RED);
} else {
memory.getLabel().setTextFill(Colorful.GREEN);
}
}
memory.getLabel().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.memory", mb));
});
// 多线程下载
multiDownload.valueProperty().addListener((obs, o, size) -> {
if (size.intValue() < 4) {
multiDownload.getLabel().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.multi_download", 1));
} else {
multiDownload.getLabel().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.multi_download", size.intValue()));
}
});
// 启动版本
launchSelect.setOnAction(e -> pageService.to(Page.GAME_SELECT));
// 完成
finish.setOnAction(e -> pageService.to(Page.MENU));
}
@InvokeForInjected
private void config() {
memory.valueProperty().bindBidirectional(config.getLauncher().getMemory());
gameSource.valueProperty().bindBidirectional(config.getMain().getGameDownloadSource());
multiDownload.valueProperty().bindBidirectional(config.getLauncher().getMultiDownload());
runtimeSource.valueProperty().bindBidirectional(config.getMain().getRuntimeDownloadSource());
autoStartup.valueProperty().bindBidirectional(config.getLauncher().getAutoStartup());
bgmVolume.valueProperty().bindBidirectional(config.getMain().getBgmVolume());
soundVolume.valueProperty().bindBidirectional(config.getMain().getSoundVolume());
}
@Override
public void onShow() {
finish.requestFocus();
}
}

View File

@ -0,0 +1,137 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.util.Path;
import cn.forevermc.launcher.view.pages.OriginalSelectPane;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.SuperInject;
import com.imyeyu.io.IO;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.network.FileRequest;
import com.imyeyu.network.GsonRequest;
import com.imyeyu.utils.OS;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
/**
* 原版选择
*
* @author 夜雨
* @since 2022-11-28 15:43
*/
@Slf4j
@Controller
@SuperInject
public class OriginalSelect extends OriginalSelectPane implements OS.FileSystem {
/** 列表接口 */
private static final String API_LIST = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
@Inject
private Dialog dialog;
@Inject
private PageService pageService;
public OriginalSelect() {
// 下载
download.setOnAction(e -> {
Game game = list.getSelectionModel().getSelectedItem();
dialog.setContent(TimiFXUI.MULTILINGUAL.text("downloading"));
dialog.eject();
new RunAsync<String>() {
@Override
protected String call() throws Exception {
File dir = IO.dir(Path.P_PACK + game.getId());
Thread.sleep(500);
FileRequest.get(game.getUrl()).toFile(IO.file(dir.getAbsolutePath() + SEP + game.getId() + ".json"));
return null;
}
@Override
protected void onFinish() {
pageService.to(Page.GAME_SELECT);
}
@Override
protected void onException(Throwable e) {
dialog.setContent(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.mojang.download_error", e.getMessage()));
dialog.setCloseEvent(() -> pageService.to(Page.ORIGINAL_SELECT));
dialog.eject();
log.error("download game error", e);
}
}.start();
});
// 返回
back.setOnAction(e -> pageService.to(Page.GAME_SELECT));
}
@Override
public void onShow() {
list.getSelectionModel().clearSelection();
if (list.getItems().isEmpty()) {
setLoading(true);
new RunAsync<List<Game>>() {
@Override
protected List<Game> call() throws Exception {
List<Game> result = new ArrayList<>();
JsonObject root = GsonRequest.get(API_LIST).asJsonObject();
JsonArray array = root.get("versions").getAsJsonArray();
JsonObject jo;
Game.Type type;
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
for (int i = 0; i < array.size(); i++) {
jo = array.get(i).getAsJsonObject();
type = Ref.toType(Game.Type.class, jo.get("type").getAsString());
if (type == Game.Type.RELEASE) {
Game game = new Game();
game.setId(jo.get("id").getAsString());
game.setType(type);
game.setUrl(jo.get("url").getAsString());
game.setReleaseAt(dateFormat.parse(jo.get("releaseTime").getAsString()).getTime());
result.add(game);
}
}
return result;
}
@Override
protected void onFinish(List<Game> result) {
list.getItems().setAll(result);
}
@Override
protected void onException(Throwable e) {
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.download.mojang.api_error"));
dialog.setCloseEvent(() -> pageService.to(Page.GAME_SELECT));
dialog.eject();
}
@Override
protected void onFinally() {
setLoading(false);
}
}.start();
}
list.scrollTo(0);
}
}

View File

@ -0,0 +1,258 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Pack;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.service.GameService;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.util.Path;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.view.pages.PackSelectPane;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.imyeyu.compress.CompressType;
import com.imyeyu.compress.Decompressor;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.SuperIOC;
import com.imyeyu.inject.annotation.SuperInject;
import com.imyeyu.io.IO;
import com.imyeyu.io.IOSize;
import com.imyeyu.io.IOSpeedService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.network.Network;
import com.imyeyu.network.ProgressiveRequest;
import com.imyeyu.network.TimiRequest;
import com.imyeyu.utils.OS;
import javafx.application.Platform;
import javafx.scene.paint.Paint;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.List;
/**
* 整合版选择控制
*
* @author 夜雨
* @since 2023-06-13 23:32
*/
@Slf4j
@SuperIOC
@Controller
@SuperInject
public class PackSelect extends PackSelectPane implements OS.FileSystem {
/** 整合版列表接口 */
private static final String API = Resources.TIMI_SERVER_API + "/fmc/pack/list";
/** 附件信息接口 */
private static final String ATTACH_API = Resources.TIMI_SERVER_API + "/attachment/%s";
/** 附件下载接口 */
private static final String ATTACH_DL_API = Resources.TIMI_SERVER_API + "/attachment/download/%s";
@Inject
private Gson gson;
@Inject
private Dialog dialog;
@Inject
private PageService pageService;
@Inject
private GameService gameService;
@Inject
private CanvasProgress canvasProgress;
@Inject
private IOSpeedService.Item ioSpeedItem;
public PackSelect() {
// 安装
install.setOnAction(e -> {
String[] installedPacks = gameService.listGames();
Pack pack = packListPane.getSelectionModel().getSelectedItem();
Pack.Source source = sourceListPane.getSelectionModel().getSelectedItem();
if (installedPacks != null) {
for (int i = 0; i < installedPacks.length; i++) {
if (installedPacks[i].trim().equals(pack.getName().trim())) {
File json = new File(Path.ABS_PACK + installedPacks[i] + SEP + installedPacks[i] + ".json");
if (json.exists() && json.isFile() && 0 < json.length()) {
// 已存在安装
dialog.setTitle(TimiFXUI.MULTILINGUAL.text("install.fail"));
dialog.setContent(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.fmc.exist_warning", pack.getName()));
dialog.setConfirmEvent(() -> pageService.to(Page.GAME_SELECT));
dialog.eject();
return;
}
}
}
}
canvasProgress.setTask(new RunAsync<String>() {
final CallbackArg<Double> DL_SPEED = d -> Platform.runLater(() -> canvasProgress.getSubLabel().setText(IOSize.format(d, IOSize.Unit.MB) + "/s"));
@Override
protected String call() throws Exception {
progress(0);
String url = switch (source.getType()) {
case URL -> source.getData();
case ATTACH -> ATTACH_DL_API.formatted(source.getData());
};
String fileName = switch (source.getType()) {
case URL -> Network.uriFileName(source.getData());
case ATTACH -> TimiRequest.<JsonObject>get(ATTACH_API.formatted(source.getData())).result().get("name").getAsString();
};
File compressedFile = new File("etc" + SEP + fileName);
if (!compressedFile.exists() || compressedFile.length() != pack.getSize()) {
// 下载文件
log.info("download remote pack: {}", url);
update("DOWNLOAD");
ProgressiveRequest.get(url, (total, read, now) -> {
progress(1D * read / pack.getSize());
ioSpeedItem.push(now);
return !isInterrupted;
}).toFile(compressedFile);
}
if (!isInterrupted) {
// 解压文件
log.info("decompress pack file: {}", compressedFile.getAbsolutePath());
progress(0);
update("DECOMPRESS");
Decompressor decompressor = CompressType.fromFile(compressedFile);
decompressor.setFileCallback(file -> message(file.getName()));
decompressor.setProgressCallback(this::progress);
decompressor.run(compressedFile, Path.ABS_PACK + pack.getName());
IO.destroy(compressedFile);
// 初始配置
File optionFile = new File(Path.ABS_PACK + pack.getName() + SEP + pack.getName() + ".yaml");
IO.toFile(optionFile, pack.getDefOption());
}
return null;
}
@Override
protected void onUpdate(String status) {
if (TimiJava.isNotEmpty(status)) {
switch (status) {
case "DOWNLOAD" -> {
ioSpeedItem.addBufferListener(DL_SPEED);
canvasProgress.setCanCancel(true);
canvasProgress.setBackgroundColor(Colorful.BLACK);
canvasProgress.setColor(Paint.valueOf("#AAA"));
canvasProgress.reset();
}
case "DECOMPRESS" -> {
ioSpeedItem.removeBufferListener(DL_SPEED);
canvasProgress.setCanCancel(false);
canvasProgress.setBackgroundColor(Paint.valueOf("#AAA"));
canvasProgress.setColor(Paint.valueOf("#0A0"));
canvasProgress.reset();
}
}
}
}
@Override
protected void onFinish() {
reset();
if (isInterrupted) {
log.info("install be cancel");
pageService.to(Page.PACK_SELECT);
} else {
// 安装完成
log.info("installed pack succeed");
dialog.setContent(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.fmc.succeed", pack.getName()));
dialog.setConfirmEvent(() -> pageService.to(Page.GAME_SELECT));
dialog.eject();
}
}
@Override
protected void onException(Throwable e) {
log.error("install remote pack error", e);
dialog.setContent(TimiFXUI.MULTILINGUAL.text("error") + ": " + e.getMessage());
dialog.setConfirmEvent(() -> pageService.to(Page.PACK_SELECT));
dialog.eject();
}
});
canvasProgress.setCanCancel(true);
canvasProgress.setBackgroundColor(Colorful.BLACK);
canvasProgress.setColor(Paint.valueOf("#AAA"));
canvasProgress.getLabel().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.fmc.downloading", pack.getName()));
pageService.to(Page.CANVAS_PROGRESS);
});
// 返回
back.setOnAction(e -> pageService.to(Page.GAME_SELECT));
}
@InvokeForInjected
private void injected() {
// 下载源列表更新
packListPane.getSelectionModel().selectedItemProperty().addListener((obs, o, newSelectedItem) -> {
if (newSelectedItem == null) {
sourceListPane.getItems().clear();
} else {
List<Pack.Source> sources = newSelectedItem.getSourceList();
sourceListPane.getItems().setAll(sources);
for (int i = 0; i < sources.size(); i++) {
if (sources.get(i).isDefault()) {
sourceListPane.getSelectionModel().select(sources.get(i));
break;
}
}
}
});
}
@Override
public void onShow() {
packListPane.setLoading(true);
sourceListPane.setLoading(true);
new RunAsync<List<Pack>>() {
@Override
protected List<Pack> call() throws Exception {
// 获取整合版列表
return TimiRequest.<List<Pack>>post(API).result();
}
@Override
protected void onFinish(List<Pack> packs) {
packListPane.getItems().setAll(packs);
}
@Override
protected void onException(Throwable e) {
log.error("list remote pack error", e);
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.download.fmc.error"));
dialog.setConfirmEvent(() -> pageService.to(Page.GAME_SELECT));
dialog.eject();
}
@Override
protected void onFinally() {
packListPane.setLoading(false);
sourceListPane.setLoading(false);
}
}.start();
}
@Override
public void onHide() {
packListPane.getItems().clear();
sourceListPane.getItems().clear();
}
}

View File

@ -0,0 +1,75 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.service.FMCLoginService;
import cn.forevermc.launcher.view.pages.PlayerSelectPane;
import com.google.gson.Gson;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.Setter;
import com.imyeyu.inject.annotation.Component;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.java.bean.Callback;
/**
* @author 夜雨
* @since 2024-05-21 19:40
*/
@Component
public class PlayerSelect extends PlayerSelectPane {
/**
*
*
* @author 夜雨
* @since 2024-05-21 23:14
*/
public enum Action {
CONFIRM,
CANCEL
}
@Inject
private Gson gson;
@Getter
private Action action;
@Setter
private Callback onCloseEvent;
public PlayerSelect() {
// 确认
confirm.setOnAction(e -> {
action = Action.CONFIRM;
onCloseEvent.handler();
});
// 取消
cancel.setOnAction(e -> {
action = Action.CANCEL;
onCloseEvent.handler();
});
}
@Override
protected void onShow() {
list.getItems().sort((o1, o2) -> {
if (o1.getLastLoginAt() == null || o2.getLastLoginAt() == null) {
return -1;
}
return o2.getLastLoginAt().compareTo(o1.getLastLoginAt());
});
list.getSelectionModel().select(0);
}
/** @return 列表数据 */
public ObservableList<FMCLoginService.MinecraftPlayer> getItems() {
return list.getItems();
}
/** @return 选中的玩家 */
public FMCLoginService.MinecraftPlayer getSelected() {
return list.getSelectionModel().getSelectedItem();
}
}

View File

@ -0,0 +1,217 @@
package cn.forevermc.launcher.ctrl.pane;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.FileDownload;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.core.DownloadThread;
import cn.forevermc.launcher.ctrl.Main;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.view.pages.ResourceDownloadPane;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.concurrent.Worker;
import javafx.scene.input.MouseEvent;
import javafx.scene.media.MediaPlayer;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.inject.annotation.Controller;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.SuperInject;
import com.imyeyu.io.IOSize;
import com.imyeyu.io.IOSpeedService;
import com.imyeyu.java.bean.Callback;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
/**
* 资源下载
*
* @author 夜雨
* @since 2022-11-28 19:30
*/
@Slf4j
@Controller
@SuperInject
public class ResourceDownload extends ResourceDownloadPane {
@Inject
private Config config;
@Inject
private Main main;
@Inject
private PageService pageService;
@Inject
private IOSpeedService.Item ioSpeedItem;
@Inject
private ObjectProperty<MediaPlayer> bgmPlayer;
/** 完成事件(无论如何都要触发,否则锁死启动线程) */
@Setter
private Callback onFinallyEvent;
/** 队列 */
private final Queue queue;
private long downloadSize;
private DownloadThread[] threads;
public ResourceDownload() {
queue = new Queue();
// 取消
cancel.setOnAction(e -> {
if (threads != null) {
for (int i = 0; i < threads.length; i++) {
threads[i].cancel();
}
}
queue.clear();
list.getItems().clear();
pageService.to(Page.MENU);
});
// 背景音乐
bgmToggle.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
if (bgmPlayer.get() == null) {
e.consume();
}
});
bgmToggle.selectedProperty().addListener((obs, o, isSelected) -> {
MediaPlayer player = bgmPlayer.get();
if (player != null) {
if (isSelected) {
player.play();
} else {
player.pause();
}
}
});
}
@InvokeForInjected
private void injected() {
ioSpeedItem.addBufferListener(d -> Platform.runLater(() -> speed.setText(IOSize.format(d, IOSize.Unit.MB) + "/s")));
}
@Override
public void onShow() {
bgmToggle.visibleProperty().bind(config.getMain().getBgmVolume().greaterThan(0));
}
/** 开始下载 */
public void start() {
if (queue.isNotEmpty()) {
pb.setProgress(-1);
// 下载线程
int threadSize = config.getLauncher().getMultiDownload().get();
threads = new DownloadThread[Math.min(threadSize, queue.size())];
for (int i = 0; i < threads.length; i++) {
threads[i] = new DownloadThread(queue::safePull);
threads[i].setOnFinallyEvent(() -> {
for (int j = 0; j < threads.length; j++) {
if (threads[j].getState() != Worker.State.SUCCEEDED) {
return;
}
}
// 所有任务结束
onFinallyEvent.handler();
});
threads[i].setOnFinishEvent(file -> {
// 完成
file.progressProperty().unbind();
list.getItems().remove(file);
pb.setProgress(1 - 1D * list.getItems().size() / downloadSize);
});
threads[i].setOnErrorEvent(file -> {
file.progressProperty().unbind();
list.getItems().remove(file);
});
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
}
}
/**
* 设置下载队列,此方法会重置总进度
*
* @param list 列表
*/
public void setDownloadDeque(List<FileDownload> list) {
downloadSize = list.size();
this.list.getItems().addAll(list);
queue.putDownloadDeque(list);
}
/**
* 追加下载队列,此方法不会重置总进度
*
* @param list 列表
*/
public void putDownloadDeque(List<FileDownload> list) {
this.list.getItems().addAll(this.list.getItems().size(), list);
queue.putDownloadDeque(list);
}
/**
* 下载队列
*
* @author 夜雨
* @since 2022-11-29 00:57
*/
private static class Queue {
private final Deque<FileDownload> deque;
public Queue() {
deque = new ArrayDeque<>();
}
/**
* 追加队列
*
* @param list 列表
*/
void putDownloadDeque(List<FileDownload> list) {
deque.addAll(list);
}
/** @return true 为队列已空 */
boolean isEmpty() {
return deque.isEmpty();
}
/** @return true 为队列非空 */
boolean isNotEmpty() {
return !isEmpty();
}
/** @return 队列大小 */
int size() {
return deque.size();
}
/** 清除队列 */
void clear() {
deque.clear();
}
/**
* 同步获取队列文件,可多线程操作
*
* @return 下载文件
*/
synchronized FileDownload safePull() {
return deque.pollFirst();
}
}
}

View File

@ -0,0 +1,79 @@
package cn.forevermc.launcher.service;
import cn.forevermc.launcher.bean.APISetting;
import cn.forevermc.launcher.util.Resources;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.Service;
import com.imyeyu.network.TimiRequest;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.fluent.Request;
import org.apache.hc.core5.http.ContentType;
import java.util.HashMap;
import java.util.Map;
/**
* @author 夜雨
* @since 2024-04-29 20:41
*/
@Slf4j
@Service
public class APISettingService {
private static final String API = Resources.TIMI_SERVER_API + "/setting/map";
@Inject
private Gson gson;
private final ObjectProperty<APISetting> value;
public APISettingService() {
value = new SimpleObjectProperty<>();
}
@InvokeForInjected
private void injected() {
new RunAsync<APISetting>() {
@Override
protected APISetting call() throws Exception {
APISetting setting = new APISetting();
Map<APISetting.Key, Map<String, Object>> reqArgs = new HashMap<>();
for (APISetting.Key key : APISetting.Key.values()) {
reqArgs.put(key, null);
}
Map<APISetting.Key, String> data = TimiRequest.<Map<APISetting.Key, String>>wrap(Request.post(API).bodyString(gson.toJson(reqArgs), ContentType.APPLICATION_JSON)).result();
setting.setBg(gson.fromJson(data.get(APISetting.Key.FMC_BG), APISetting.DynamicList.class));
setting.setBgm(gson.fromJson(data.get(APISetting.Key.FMC_BGM), APISetting.DynamicList.class));
setting.setSplashes(gson.fromJson(data.get(APISetting.Key.FMC_SPLASHES), APISetting.DynamicList.class));
return setting;
}
@Override
protected void onFinish(APISetting setting) {
APISettingService.this.value.set(setting);
}
@Override
protected void onException(Throwable e) {
super.onException(e);
}
}.start();
}
public APISetting getValue() {
return value.get();
}
public ReadOnlyObjectProperty<APISetting> valueProperty() {
return value;
}
}

View File

@ -0,0 +1,153 @@
package cn.forevermc.launcher.service;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.util.Resources;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.Service;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.network.FileRequest;
import com.imyeyu.utils.OS;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import javafx.scene.media.Media;
import javafx.scene.media.MediaPlayer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author 夜雨
* @since 2024-04-29 20:46
*/
@Slf4j
@Service
public class BGMService implements OS.FileSystem {
private static final String CACHE_PATH = "etc" + SEP + "bgm";
@Inject
private Config config;
@Inject
private APISettingService apiSettingService;
private final ObservableMap<String, Media> map;
private final Map<String, File> cacheList = new HashMap<>();
@Getter
private MediaPlayer player;
private Media bgmOld;
private Runnable bgmRun;
public BGMService() {
map = FXCollections.observableHashMap();
bgmRun = () -> {
if (player != null) {
player.dispose();
player = null;
}
List<Media> list = new ArrayList<>(map.values());
Collections.shuffle(list);
Media bgmNew = list.getFirst();
if (bgmOld != null) {
while (bgmOld.equals(bgmNew)) {
Collections.shuffle(list);
bgmNew = list.getFirst();
}
}
bgmOld = bgmNew;
MediaPlayer player = new MediaPlayer(bgmNew);
player.volumeProperty().bind(config.getMain().getBgmVolume().divide(100));
player.setOnEndOfMedia(bgmRun);
player.setOnReady(player::play);
};
map.addListener((MapChangeListener<String, Media>) change -> {
if (player == null && !map.isEmpty()) {
// bgmRun.run();
}
});
}
@InvokeForInjected
private void injected() {
try {
{
File[] files = IO.dir(CACHE_PATH).listFiles();
if (files != null) {
for (int i = 0; i < files.length; i++) {
cacheList.put(files[i].getName(), files[i]);
}
}
}
if (TimiJava.isNotEmpty(cacheList)) {
List<File> files = new ArrayList<>(cacheList.values());
Collections.shuffle(files);
map.put(files.getFirst().getName(), new Media(files.getFirst().toURI().toURL().toExternalForm()));
}
} catch (Exception e) {
e.printStackTrace();
}
apiSettingService.valueProperty().addListener((obs, o, setting) -> {
if (setting == null) {
return;
}
try {
String active = setting.getBgm().getActive();
if (TimiJava.isNotEmpty(active)) {
if (cacheList.containsKey(active)) {
map.put(active, new Media(cacheList.get(active).toURI().toURL().toExternalForm()));
} else {
fetch(active, active, value -> map.put(active, value));
}
} else {
List<String> list = setting.getBgm().getList();
if (TimiJava.isNotEmpty(list)){
Collections.shuffle(list);
String shuffleMongoId = list.getFirst();
if (cacheList.containsKey(shuffleMongoId)) {
map.put(shuffleMongoId, new Media(cacheList.get(shuffleMongoId).toURI().toURL().toExternalForm()));
} else {
fetch(shuffleMongoId, shuffleMongoId, value -> map.put(shuffleMongoId, value));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
private void fetch(String mongoId, String fileName, CallbackArg<Media> callback) {
new RunAsync<Media>() {
@Override
protected Media call() throws Exception {
File file = IO.file(CACHE_PATH + SEP + fileName + ".mp3");
FileRequest.get(Resources.TIMI_SERVER_API + "/attachment/read/" + mongoId).toFile(file);
cacheList.put(mongoId, file);
return new Media(file.toURI().toURL().toExternalForm());
}
@Override
protected void onFinish(Media media) {
callback.handler(media);
}
}.start();
}
}

View File

@ -0,0 +1,137 @@
package cn.forevermc.launcher.service;
import cn.forevermc.launcher.util.Resources;
import com.google.gson.Gson;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.utils.BgImage;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.Service;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.network.FileRequest;
import com.imyeyu.utils.OS;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image;
import javafx.scene.layout.Background;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.fluent.Request;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 动态背景地址获取服务
*
* @author 夜雨
* @since 2022-11-28 00:12
*/
@Slf4j
@Service
public class BGService implements OS.FileSystem {
private static final String CACHE_PATH = "etc" + SEP + "bg";
@Inject
private APISettingService apiSettingService;
@Inject
private Gson gson;
private final ObjectProperty<Background> value;
private final Map<String, File> cacheList = new HashMap<>();
public BGService() {
value = new SimpleObjectProperty<>();
setValue(IO.resourceToInputStream(getClass(), "assets/img/bg.png"));
}
@InvokeForInjected
private void injected() {
try {
{
File[] files = IO.dir(CACHE_PATH).listFiles();
if (files != null) {
for (int i = 0; i < files.length; i++) {
cacheList.put(files[i].getName(), files[i]);
}
}
}
if (TimiJava.isNotEmpty(cacheList)) {
List<File> files = new ArrayList<>(cacheList.values());
Collections.shuffle(files);
setValue(IO.getInputStream(files.get(0)));
}
} catch (Exception e) {
e.printStackTrace();
}
// 从缓存随机抽取
apiSettingService.valueProperty().addListener((obs, o, setting) -> {
if (setting == null) {
return;
}
try {
String active = setting.getBg().getActive();
if (TimiJava.isNotEmpty(active)) {
if (cacheList.containsKey(active)) {
setValue(IO.getInputStream(cacheList.get(active)));
} else {
fetch(active, active, this::setValue);
}
} else {
List<String> list = setting.getBg().getList();
if (TimiJava.isNotEmpty(list)){
Collections.shuffle(list);
String shuffleMongoId = list.get(0);
if (cacheList.containsKey(shuffleMongoId)) {
setValue(IO.getInputStream(cacheList.get(shuffleMongoId)));
} else {
fetch(shuffleMongoId, shuffleMongoId, this::setValue);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
private void setValue(InputStream stream) {
value.set(new BgImage(new Image(stream, -1, -1, false, false)).cover().build());
}
private void fetch(String mongoId, String fileName, CallbackArg<InputStream> callback) {
new RunAsync<InputStream>() {
@Override
protected InputStream call() throws Exception {
File file = IO.file(CACHE_PATH + SEP + fileName);
FileRequest.get(Resources.TIMI_SERVER_API + "/attachment/read/" + mongoId).toFile(file);
cacheList.put(mongoId, file);
return IO.getInputStream(file);
}
@Override
protected void onFinish(InputStream stream) {
callback.handler(stream);
}
}.start();
}
public ReadOnlyObjectProperty<Background> valueProperty() {
return value;
}
public Background getValue() {
return value.get();
}
}

View File

@ -0,0 +1,265 @@
package cn.forevermc.launcher.service;
import cn.forevermc.launcher.bean.FabricAPI;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.core.LauncherException;
import cn.forevermc.launcher.util.Resources;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.Service;
import com.imyeyu.io.IO;
import com.imyeyu.io.JarReader;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.FileRequest;
import com.imyeyu.network.TimiRequest;
import com.imyeyu.utils.OS;
import javafx.beans.property.MapProperty;
import javafx.beans.property.ReadOnlyMapProperty;
import javafx.beans.property.SimpleMapProperty;
import javafx.collections.FXCollections;
import lombok.Cleanup;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.fluent.Request;
import org.apache.hc.core5.http.ContentType;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* ForeverMC 账号验证服务
*
* @author 夜雨
* @since 2022-11-30 00:31
*/
@Slf4j
@Service
public class FMCLoginService implements OS.FileSystem {
/**
* 登录玩家
*
* @author 夜雨
* @since 2024-05-21 18:11
*/
@Data
public static class MinecraftPlayer {
/** 账号 ID */
Long id;
/** 玩家名 */
String name;
/** 上次登录时间 */
Long lastLoginAt;
}
/**
* 令牌请求
*
* @author 夜雨
* @since 2024-05-21 18:20
*/
@Data
public static class TokenRequest {
/** 账号ID、邮箱或玩家名 */
String user;
/** 密码 */
String password;
}
/**
* 令牌返回
*
* @author 夜雨
* @since 2024-05-21 18:19
*/
@Data
public static class TokenResponse {
/** 令牌 */
String token;
/** 过期于 */
long expiredAt;
}
private static final String SETTING_API = Resources.TIMI_SERVER_API + "/setting";
private static final String MIRROR_API = Resources.TIMI_SERVER_API + "/mirror";
private static final String ATTACH_DL_API = Resources.TIMI_SERVER_API + "/attachment/download";
/** Timi 数据中心验证接口 */
private static final String LOGIN_API = Resources.TIMI_SERVER_API + "/fmc/player/login";
/** Timi 数据中心验证接口 */
private static final String LIST_PLAYER_API = Resources.TIMI_SERVER_API + "/fmc/player/list";
/** Timi 数据中心验证接口 */
private static final String LOGIN_TOKEN_API = Resources.TIMI_SERVER_API + "/fmc/player/login/token";
@Inject
private Gson gson;
@Inject
private GameService gameService;
/** 辅助登录模组映射 Map&lt;版本, mongoId&gt; */
private static final MapProperty<String, String> loginModMap = new SimpleMapProperty<>(FXCollections.observableHashMap());
/** FabricAPI 镜像映射 Map&lt;版本, FabricAPI&gt; */
private static final MapProperty<String, FabricAPI> fabricApiMap = new SimpleMapProperty<>(FXCollections.observableHashMap());
@InvokeForInjected
private void injected() {
new RunAsync<Void>() {
Map<String, String> loginModMapTemp = null;
Map<String, FabricAPI> fabricAPIMapTemp = null;
@Override
protected Void call() throws Exception {
{
String data = TimiRequest.<String>get(SETTING_API + "/FMC_LOGIN_FABRIC").result();
loginModMapTemp = gson.fromJson(data, new TypeToken<Map<String, String>>() {}.getType());
}
{
List<FabricAPI> data = TimiRequest.<List<FabricAPI>>get(MIRROR_API + "/FabricAPI").result();
fabricAPIMapTemp = data.stream().collect(Collectors.toMap(FabricAPI::getMinecraftVer, item -> item));
}
return null;
}
@Override
protected void onFinish(Void map) {
fabricApiMap.clear();
fabricApiMap.putAll(fabricAPIMapTemp);
loginModMap.clear();
loginModMap.putAll(loginModMapTemp);
}
@Override
protected void onException(Throwable e) {
log.error("list support login mod map error", e);
}
}.start();
}
public boolean isSupportFMCLogin(Game game) {
if (game == null || game.isLegacy()) {
return false;
}
if (loginModMap.isEmpty()) {
return false;
}
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(game);
boolean supportLoginMod = loginModMap.containsKey(realVersion.getValue());
try {
if (!supportLoginMod) {
return false;
}
if (hasMod(game, "fabric-api")) {
return true;
}
for (String key : fabricApiMap.keySet()) {
if (key.contains(realVersion.getValue())) {
return true;
}
}
return false;
} catch (Exception e) {
log.error("check token login support error", e);
return false;
}
}
public void checkLoginMod(Game game) {
try {
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(game);
if (!loginModMap.containsKey(realVersion.getValue())) {
// 不支持的版本
throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.account.not_support"));
}
File modPath = IO.dir(IO.fitPath(game.getPath().getAbsolutePath()) + "mods" + SEP);
if (!hasMod(game, "fmc-login-fabric")) {
log.info("downloading fmc-login-fabric mod for {}", loginModMap.get(realVersion.getValue()));
String dlURL = ATTACH_DL_API + "/%s".formatted(loginModMap.get(realVersion.getValue()));
FileRequest.get(dlURL).toFile(modPath.getAbsolutePath(), "fmc-login-fabric.jar");
}
} catch (Exception e) {
log.error("check mod error", e);
throw new TimiException(TimiCode.RESULT_BAD, "check mod fail");
}
}
public TokenResponse genLoginToken(String user, String password) throws Exception {
TokenRequest request = new TokenRequest();
request.setUser(user);
request.setPassword(password);
return TimiRequest.<TokenResponse>wrap(Request.post(LOGIN_TOKEN_API).bodyString(gson.toJson(request), ContentType.APPLICATION_JSON)).result();
}
public List<MinecraftPlayer> listBoundPlayer(String token) throws Exception {
return TimiRequest.<List<MinecraftPlayer>>wrap(Request.get(LIST_PLAYER_API).addHeader("Token", token)).result();
}
public TokenResponse doLogin(MinecraftPlayer player, String token) throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("playerId", player.id);
return TimiRequest.<TokenResponse>wrap(Request.get(LIST_PLAYER_API)
.addHeader("Token", token)
.bodyString(gson.toJson(map), ContentType.APPLICATION_JSON)).result();
}
private boolean hasMod(Game game, String name) {
try {
File modPath = IO.dir(IO.fitPath(game.getPath().getAbsolutePath()) + "mods" + SEP);
File[] mods = modPath.listFiles();
if (TimiJava.isNotEmpty(mods)) {
for (int i = 0; i < mods.length; i++) {
if (mods[i].getName().endsWith(".jar")) {
@Cleanup
JarReader jar = new JarReader(mods[i]);
if (jar.has("fabric.mod.json")) {
String fabricJson = new String(jar.getBytes("fabric.mod.json"));
JsonObject root = JsonParser.parseString(fabricJson).getAsJsonObject();
if (root.get("id").getAsString().equals(name)) {
return true;
}
}
}
}
}
return false;
} catch (Exception e) {
log.error("check mod error", e);
throw new TimiException(TimiCode.ERROR, "check mod fail");
}
}
public static ReadOnlyMapProperty<String, String> loginModMapProperty() {
return loginModMap;
}
public static ReadOnlyMapProperty<String, FabricAPI> fabricAPIMapProperty() {
return fabricApiMap;
}
}

View File

@ -0,0 +1,625 @@
package cn.forevermc.launcher.service;
import cn.forevermc.launcher.bean.Config;
import cn.forevermc.launcher.bean.FileDownload;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.GameDownloadSource;
import cn.forevermc.launcher.util.Path;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.imyeyu.config.ConfigLoader;
import com.imyeyu.fx.config.BindingsConfig;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.Service;
import com.imyeyu.io.IO;
import com.imyeyu.java.bean.CallbackArgReturn;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.CommonRequest;
import com.imyeyu.network.FileRequest;
import com.imyeyu.network.GsonRequest;
import com.imyeyu.utils.Digest;
import com.imyeyu.utils.OS;
import com.imyeyu.utils.Text;
import com.imyeyu.utils.Time;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.NoSuchAlgorithmException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* 游戏服务
*
* @author 夜雨
* @since 2022-12-11 19:14
*/
@Slf4j
@Service
public class GameService implements OS.FileSystem {
/**
* JNA 类型
*
* @author 夜雨
* @since 2022-11-29 13:19
*/
@AllArgsConstructor
enum NativeType {
/** Windows */
WINDOWS("natives-windows"),
/** X86 架构 Windows */
WINDOWS_X86("natives-windows-x86"),
/** Linux */
LINUX("natives-linux"),
/** MacOS */
MACOS("natives-macos"),
/** ARM 架构 MacOS */
MACOS_ARM("natives-macos-arm64");
/** 位于游戏配置的属性名称 */
final String name;
}
/** 原版启动类 */
private static final String MAIN_CLASS_MOJANG = "net.minecraft.client.main.Main";
/** Fabric Loader 启动类 */
private static final String MAIN_CLASS_FABRIC = "net.fabricmc.loader.impl.launch.knot.KnotClient";
/** 远古版本资源数据 */
private static final String ASSETS_ANCIENT = "pre-1.6";
/** Key: 必须要指定运行时版本的 Minecraft 版本的发布开始时间,此时间点之后的版本需要 Value: Java 版本 */
private static final Map<Long, String> REQ_RT_MAP = Collections.unmodifiableMap(new LinkedHashMap<>() {{
try {
put(Time.dateTimeT.parse("2024-04-03T11:49:39").getTime(), "21"); // 24w14a
put(Time.dateTimeT.parse("2021-11-16T17:04:48").getTime(), "17"); // 1.18-pre2
put(0L, "8");
} catch (ParseException e) {
throw new RuntimeException(e);
}
}});
/** 配置加载器缓存 */
private final Map<Game, ConfigLoader<?>> configLoaderMap;
@Inject
private Config config;
@Inject
private Gson gson;
public GameService() {
configLoaderMap = new HashMap<>();
}
/** @return 当前版本包列表 */
public String[] listGames() {
return new File(Path.P_PACK).list();
}
/**
* 读取游戏配置
*
* @param game 版本
* @return 游戏配置
*/
public Game.Option getOption(Game game) {
ConfigLoader<Game.Option> loader = BindingsConfig.build("GameOption.yaml", IO.fitPath(game.getPath().getAbsolutePath()) + game.getName() + ".yaml", Game.Option.class);
configLoaderMap.put(game, loader);
return loader.load();
}
/**
* 保存游戏配置
*
* @param game 游戏
*/
public void storeOption(Game game) {
configLoaderMap.get(game).dump();
}
/**
* 获取游戏需要运行时最低版本。映射表静态储存 {@link #REQ_RT_MAP}
*
* @param game 游戏
* @return 8、17、21 等 Java 运行时版本
*/
public String getRequiredRTVer(Game game) {
for (Map.Entry<Long, String> item : REQ_RT_MAP.entrySet()) {
if (item.getKey() < game.getReleaseAt()) {
return item.getValue();
}
}
throw new TimiException(TimiCode.ERROR).msgKey("TODO get req java version error");
}
/**
* 构建游戏,解析整合包基本数据,此方法不会深度解析
*
* @param gamePath 整合包路径文件,通常是 .minecraft/versions 路径的文件夹
* @return 版本包数据
* @throws IOException 读取异常
*/
public Game buildGame(File gamePath) throws Exception {
File jar = new File(IO.fitPath(gamePath.getAbsolutePath()) + gamePath.getName() + ".jar");
File json = new File(IO.fitPath(gamePath.getAbsolutePath()) + gamePath.getName() + ".json");
File yaml = new File(IO.fitPath(gamePath.getAbsolutePath()) + gamePath.getName() + ".yaml");
Game game = new Game();
game.setName(gamePath.getName());
game.setPath(gamePath);
game.setJar(jar);
game.setJson(json);
game.setYaml(yaml);
JsonObject root = JsonParser.parseString(IO.toString(json)).getAsJsonObject();
String mainClass = root.get("mainClass").getAsString();
game.setJsonRoot(root);
game.setOriginal(MAIN_CLASS_MOJANG.equals(mainClass));
game.setFabric(MAIN_CLASS_FABRIC.equals(mainClass));
game.setLegacy(ASSETS_ANCIENT.equals(root.get("assets").getAsString()));
game.setOption(getOption(game));
game.setReleaseAt(Time.dateTimeT.parse(root.get("releaseTime").getAsString()).getTime());
return game;
}
/**
* 读取版本包元数据,深度解析所有属性,并确定缺少的依赖和资源,比较耗时
*
* @param game 版本包
* @throws Exception 读取异常
*/
public void readMetaData(Game game) throws Exception {
Game.MetaData metaData = new Game.MetaData();
JsonObject root = game.getJsonRoot();
// 主版本
metaData.setVersion(root.get("id").getAsString());
// 真实版本
metaData.setRealVersion(getRealVersion(game));
// 启动核心
JsonObject core = root.get("downloads").getAsJsonObject().get("client").getAsJsonObject();
metaData.setCoreSHA1(core.get("sha1").getAsString());
metaData.setCoreURL(core.get("url").getAsString());
metaData.setCoreSize(core.get("size").getAsLong());
// 资源文件
log.info("parsing assets list..");
metaData.setAssetsVersion(root.get("assets").getAsString());
metaData.setAssetList(parseAssetsList(root.get("assetIndex").getAsJsonObject().get("url").getAsString(), metaData));
// 依赖文件
// TODO Create Astral 长时间卡这里
log.info("parsing libraries list..");
metaData.setLibrarieList(parseLibrariesList(root.get("libraries").getAsJsonArray()));
// 启动类
metaData.setMainClass(root.get("mainClass").getAsString());
// Java 版本
metaData.setJavaVersion(root.get("javaVersion").getAsJsonObject().get("majorVersion").getAsString());
{
// 启动参数模板
log.info("generate arguments template for launch");
if (root.has("arguments")) {
JsonObject arguments = root.get("arguments").getAsJsonObject();
if (arguments.has("game")) {
metaData.setArgsGame(parseArgsArray(game, arguments.get("game").getAsJsonArray()));
}
if (arguments.has("jvm")) {
metaData.setArgsJVM(parseArgsArray(game, arguments.get("jvm").getAsJsonArray()));
}
} else if (root.has("minecraftArguments")) {
// 远古版本
metaData.setArgsGame(root.get("minecraftArguments").getAsString());
metaData.setArgsJVM(" -Dminecraft.applet.TargetDirectory=${game_directory} -Djava.library.path=${natives_directory} -cp ${classpath}");
}
}
// 标题
if (root.has("title")) {
metaData.setTitle(root.get("title").getAsString());
} else if (root.has("id")) {
metaData.setTitle("Minecraft " + root.get("id").getAsString());
}
if (game.isLegacy()) {
metaData.setTitle("Minecraft");
}
log.info("parsing required download file list");
// 解析需要下载的资源
metaData.setDownloadList(toFileDL(metaData));
if (metaData.getDownloadList().isEmpty()) {
log.info("nothing required download");
} else {
log.info("required download size {}", metaData.getDownloadList().size());
}
// 设置元数据
game.setMetaData(metaData);
// JSON 资源可能被补充 size 或 sha1写入磁盘
IO.toFile(game.getJson(), gson.toJson(root));
}
/**
* 获取版本包的真实游戏版本
*
* @param game 版本包
* @return 真实游戏版本
*/
public Game.MetaData.RealVersion getRealVersion(Game game) {
JsonObject root = game.getJsonRoot();
// 真实版本
Game.MetaData.RealVersion realVersion = new Game.MetaData.RealVersion();
String value = root.get("id").getAsString();
if (root.has("source-id")) {
value = root.get("source-id").getAsString();
}
// HMCL 的原始版本
if (root.has("patches")) {
JsonArray patches = root.get("patches").getAsJsonArray();
for (int j = 0; j < patches.size(); j++) {
JsonObject object = patches.get(j).getAsJsonObject();
if (object.has("id") && object.get("id").getAsString().equals("game")) {
value = object.get("version").getAsString();
break;
}
}
}
realVersion.setValue(value);
return realVersion;
}
/**
* 解析版本需要下载的依赖和资源
*
* @param metadata 版本数据
* @return 依赖和资源文件
*/
private List<FileDownload> toFileDL(Game.MetaData metadata) throws IOException, NoSuchAlgorithmException {
List<FileDownload> dlList = new ArrayList<>();
File file;
FileDownload fileDL;
String version = metadata.getVersion();
// TODO 告知 UI 正在检查项
// 核心
{
file = new File(Path.P_PACK + version + SEP + version + ".jar");
if (!file.exists() || file.length() == 0 || file.length() != metadata.getCoreSize() || !Digest.sha1(IO.toBytes(file)).equals(metadata.getCoreSHA1())) {
fileDL = new FileDownload(file);
fileDL.setHash(metadata.getCoreSHA1());
fileDL.setUrl(metadata.getCoreURL());
fileDL.setSize(metadata.getCoreSize());
dlList.add(fileDL);
}
}
// 资源
String assetsPath = Path.P_ASSETS + "objects" + SEP;
List<Game.MetaData.Assets> assetList = metadata.getAssetList();
for (int i = 0; i < assetList.size(); i++) {
Game.MetaData.Assets asset = assetList.get(i);
file = new File(assetsPath + asset.getHash().substring(0, 2) + SEP + asset.getHash());
if (!file.exists() || file.length() == 0 || file.length() != asset.getSize() || !Digest.sha1(IO.toBytes(file)).equals(asset.getHash())) {
fileDL = new FileDownload(file);
fileDL.setHash(asset.getHash());
fileDL.setUrl(asset.getUrl());
fileDL.setSize(asset.getSize());
dlList.add(fileDL);
}
}
// 依赖
List<Game.MetaData.Libraries> librarieList = metadata.getLibrarieList();
for (int i = 0; i < librarieList.size(); i++) {
Game.MetaData.Libraries library = librarieList.get(i);
String path = library.getPath();
file = new File(Path.P_LIB + path.substring(0, path.lastIndexOf("/")) + path.substring(path.lastIndexOf("/")));
if (!file.exists() || file.length() == 0 || file.length() != library.getSize() || !Digest.sha1(IO.toBytes(file)).equals(library.getSha1())) {
fileDL = new FileDownload(file);
fileDL.setHash(library.getSha1());
fileDL.setUrl(library.getUrl());
fileDL.setSize(library.getSize());
dlList.add(fileDL);
}
}
return dlList;
}
/**
* 解析 JSON 配置启动参数
*
* @param array 启动参数 JSON
* @return 启动参数命令
*/
private String parseArgsArray(Game game, JsonArray array) {
StringBuilder sb = new StringBuilder();
String osVersion = System.getProperty("os.version");
CallbackArgReturn<String, String> qmArgs = p -> {
int i = p.indexOf("=");
if (i == -1) {
return p;
} else {
if (p.charAt(i + 1) == '$') {
return p;
} else {
return p.substring(0, i + 1) + Text.quote(p.substring(i + 1));
}
}
};
JsonObject arg, rule, os;
JsonArray rules, values;
args: for (int i = 0; i < array.size(); i++) {
if (array.get(i).isJsonObject()) {
arg = array.get(i).getAsJsonObject();
rules = arg.get("rules").getAsJsonArray();
if (arg.get("value").isJsonArray()) {
// 数组参数
for (int j = 0; j < rules.size(); j++) {
// 约束规则
rule = rules.get(j).getAsJsonObject();
if (rule.get("action").getAsString().equals("allow")) {
// 允许的规则
if (rule.has("os")) {
// 系统约束
os = rule.get("os").getAsJsonObject();
if (os.has("name")) {
if (OS.IS_WINDOWS && os.get("name").getAsString().equals("windows")) {
// windows 系统
if (os.has("version")) {
// 版本约束
if (!Pattern.matches(os.get("version").getAsString(), osVersion.substring(0, osVersion.length() - 1))) {
// 版本不通过
continue args;
}
}
if (arg.get("value").isJsonArray()) {
// 多值
values = arg.get("value").getAsJsonArray();
for (int k = 0; k < values.size(); k++) {
sb.append(qmArgs.handler(values.get(k).getAsString())).append(' ');
}
} else {
// 单值
sb.append(qmArgs.handler(arg.get("value").getAsString())).append(' ');
}
continue args;
} // else Linux 系统
} // else 非系统名称约束
} else if (rule.has("features")) {
// 可选规则(暂不支持自定义规则)
// for (int k = 0; k < optionRules.size(); k++) {
// if (rule.get("features").getAsJsonObject().has(optionRules.get(k))) {
// if (arg.get("value").isJsonArray()) {
// // 多值
// values = arg.get("value").getAsJsonArray();
// for (int m = 0; m < values.size(); m++) {
// sb.append(qmArgs.handler(values.get(m).getAsString())).append(' ');
// }
// } else {
// // 单值
// sb.append(qmArgs.handler(arg.get("value").getAsString())).append(' ');
// }
// break;
// }
// }
}
} // else 禁用的规则
}
} // 非数组参数
} else {
// 通用字符串参数
sb.append(array.get(i).getAsString()).append(' ');
}
}
return sb.toString();
}
/**
* 解析资源列表
*
* @param assetsURL 资源数据 JSON URL
* @return 资源数据
* @throws Exception 异常
*/
private List<Game.MetaData.Assets> parseAssetsList(String assetsURL, Game.MetaData metaData) throws Exception {
List<Game.MetaData.Assets> list = new ArrayList<>();
final String urlDomain = config.getMain().getGameDownloadSource().get().getResources();
String indexJson = null;
int retry = 0;
while (retry < 10) {
try {
indexJson = CommonRequest.get(assetsURL).asString();
break;
} catch (SocketTimeoutException e) {
retry++;
log.error("official resources api error", e);
}
}
if (indexJson == null) {
throw new TimiException(TimiCode.ERROR, TimiFXUI.MULTILINGUAL.text("fmc.launcher.resource_error"));
}
IO.toFile(new File(Path.P_ASSETS + "indexes" + SEP + metaData.getAssetsVersion() + ".json"), indexJson);
JsonObject objects = GsonRequest.get(assetsURL).asJsonObject().get("objects").getAsJsonObject();
Game.MetaData.Assets assets;
for (Map.Entry<String, JsonElement> object : objects.entrySet()) {
if (object.getValue() instanceof JsonObject jo) {
assets = new Game.MetaData.Assets();
assets.setHash(jo.get("hash").getAsString());
assets.setUrl(urlDomain + assets.getHash().substring(0, 2) + '/' + assets.getHash());
assets.setSize(jo.get("size").getAsLong());
list.add(assets);
}
}
return list;
}
/**
* 解析依赖
*
* @param libs 依赖列表
* @return 依赖
*/
private List<Game.MetaData.Libraries> parseLibrariesList(JsonArray libs) throws Exception {
List<Game.MetaData.Libraries> list = new ArrayList<>();
Game.MetaData.Libraries libraries;
JsonObject lib, rule, os, downloads, artifact, natives, classifiers, classifier;
JsonArray rules;
libs:
for (int i = 0; i < libs.size(); i++) {
lib = libs.get(i).getAsJsonObject();
if (lib.has("name") && lib.has("url")) {
// 兼容 HMCL 的 JSON 结构仅有 name 和 url临时转为本启动器结构
String name = lib.get("name").getAsString();
String[] names = name.split(":");
// 合成路径
StringBuilder pathSB = new StringBuilder();
String[] paths = names[0].split("\\.");
for (int j = 0; j < paths.length; j++) {
pathSB.append(paths[j]).append('/');
}
pathSB.append(names[1]).append('/').append(names[2]).append('/').append(names[1]).append('-').append(names[2]).append(".jar");
downloads = new JsonObject();
{
String path = pathSB.toString();
String url = lib.get("url").getAsString() + path;
artifact = new JsonObject();
artifact.addProperty("path", path);
artifact.addProperty("url", url);
artifact.addProperty("size", FileRequest.get(url).length());
artifact.addProperty("sha1", Digest.sha1(FileRequest.get(url).asBytes()));
downloads.add("artifact", artifact);
lib.add("downloads", downloads);
}
}
if (lib.has("downloads")) {
downloads = lib.get("downloads").getAsJsonObject();
if (lib.has("rules")) {
// 约束规则
rules = lib.get("rules").getAsJsonArray();
for (int j = 0; j < rules.size(); j++) {
rule = rules.get(j).getAsJsonObject();
switch (rule.get("action").getAsString()) {
case "allow" -> {
if (rule.has("os")) {
os = rule.get("os").getAsJsonObject();
if (os.has("name") && os.get("name").getAsString().equals("osx") && !OS.IS_OSX) {
// OSX 专用
continue libs;
}
} // else 通用
}
case "disallow" -> {
if (rule.has("os")) {
os = rule.get("os").getAsJsonObject();
if (os.has("name") && os.get("name").getAsString().equals("osx") && OS.IS_OSX) {
// OSX 禁用
continue libs;
}
} // else 通用
}
}
}
}
// JNA
if (lib.has("natives")) {
// 1.18 及以下
natives = lib.get("natives").getAsJsonObject();
// Windows JNA
if (natives.has("windows") && OS.IS_WINDOWS) {
classifiers = downloads.get("classifiers").getAsJsonObject();
classifier = classifiers.get(natives.get("windows").getAsString()).getAsJsonObject();
libraries = new Game.MetaData.Libraries();
libraries.setNatives(true);
libraries.setPath(classifier.get("path").getAsString());
libraries.setSha1(classifier.get("sha1").getAsString());
libraries.setSize(classifier.get("size").getAsLong());
libraries.setUrl(classifier.get("url").getAsString());
list.add(libraries);
}
// 其他系统 JNA
}
if (downloads.has("artifact")) {
artifact = downloads.get("artifact").getAsJsonObject();
libraries = new Game.MetaData.Libraries();
String name = lib.get("name").getAsString();
if (OS.IS_WINDOWS) {
libraries.setNatives(name.endsWith("natives-windows"));
}
if (OS.IS_UNIX) {
libraries.setNatives(name.endsWith("natives-linux"));
}
if (OS.IS_OSX) {
libraries.setNatives(name.endsWith("natives-macos"));
}
libraries.setPath(artifact.get("path").getAsString());
// TODO 告知 UI 耗时操作
String url = artifact.get("url").getAsString();
if (artifact.has("sha1")) {
libraries.setSha1(artifact.get("sha1").getAsString());
} else {
String sha1 = Digest.sha1(FileRequest.get(url).asBytes());
artifact.addProperty("sha1", sha1);
libraries.setSha1(sha1);
}
if (artifact.has("size")) {
libraries.setSize(artifact.get("size").getAsLong());
} else {
long size = FileRequest.get(url).length();
artifact.addProperty("size", size);
libraries.setSize(size);
}
libraries.setUrl(url);
list.add(libraries);
}
}
}
// 替换下载源
GameDownloadSource source = config.getMain().getGameDownloadSource().get();
for (int i = 0; i < list.size(); i++) {
String fromURL = list.get(i).getUrl();
String toURL = fromURL.replaceAll(GameDownloadSource.MOJANG.getLibraries(), source.getLibraries());
list.get(i).setUrl(toURL);
}
return list;
}
}

View File

@ -0,0 +1,76 @@
package cn.forevermc.launcher.service;
import cn.forevermc.launcher.bean.MCPopupTips;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.layout.Background;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import com.imyeyu.fx.ui.components.popup.AbstractPopupTipsService;
import com.imyeyu.fx.utils.BgFill;
import com.imyeyu.inject.annotation.Service;
/**
* 弹出提示服务
*
* @author 夜雨
* @since 2021-12-04 11:03
*/
@Service
public class MCPopupTipsService extends AbstractPopupTipsService<BorderPane> {
private static final Background BG = new BgFill("#101D").build();
private BorderPane main;
public MCPopupTipsService() {
showOnRoot = node -> main.setCenter(node);
}
@Override
protected BorderPane createRoot() {
main = new BorderPane();
main.setBackground(BG);
BorderPane root = new BorderPane(main);
Pane t = new Pane();
Pane r = new Pane();
Pane l = new Pane();
Pane b = new Pane();
t.setPrefHeight(3);
r.setPrefWidth(3);
b.setPrefHeight(3);
l.setPrefWidth(3);
t.setBackground(BG);
r.setBackground(BG);
b.setBackground(BG);
l.setBackground(BG);
final Insets MARGIN = new Insets(0, 3, 0, 3);
BorderPane.setMargin(t, MARGIN);
BorderPane.setMargin(b, MARGIN);
root.setTop(t);
root.setRight(r);
root.setBottom(b);
root.setLeft(l);
return root;
}
/**
* 安装提示
*
* @param node 被安装节点
* @param text 文本
* @return 弹出提示
*/
public MCPopupTips install(Node node, String text) {
MCPopupTips tips = new MCPopupTips(text);
install(node, tips);
return tips;
}
}

View File

@ -0,0 +1,11 @@
package cn.forevermc.launcher.service;
/**
* 正版登录验证
*
* @author 夜雨
* @since 2024-08-03 12:06
*/
public class MojangLoginService {
// TODO 待实现
}

View File

@ -0,0 +1,75 @@
package cn.forevermc.launcher.service;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.view.components.RootLayout;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import lombok.Getter;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.Service;
/**
* 页面服务
*
* @author 夜雨
* @since 2022-11-21 16:21
*/
@Service
public class PageService {
@Inject
private RootLayout root;
/** 上一页 */
@Getter
private Page prev;
private final ObjectProperty<Page> activatedPage;
private boolean isSneakyBack;
public PageService() {
activatedPage = new SimpleObjectProperty<>();
activatedPage.addListener((obs, prev, now) -> {
this.prev = prev;
if (!isSneakyBack && prev != null) {
prev.getIOCPage().hide();
}
if (now != null) {
now.getIOCPage().show();
root.setPage(now.getIOCPage());
}
isSneakyBack = false;
});
}
/**
* 跳转页面
*
* @param page 页面
*/
public void to(Page page) {
activatedPage.set(page);
}
/** 返回上一个页面 */
public void back() {
to(prev);
}
/** 悄悄地返回,此动作不会调度页面的 onHide 事件 */
public void back4Sneaky() {
isSneakyBack = true;
back();
}
public Page getActivatedPage() {
return activatedPage.get();
}
/** @return 当前页面监听 */
public ReadOnlyObjectProperty<Page> activatedPageProperty() {
return activatedPage;
}
}

View File

@ -0,0 +1,54 @@
package cn.forevermc.launcher.service;
import cn.forevermc.launcher.bean.APISetting;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.inject.annotation.Service;
import com.imyeyu.java.TimiJava;
import java.util.Collections;
/**
* 动态闪烁标语服务
*
* @author 夜雨
* @since 2022-11-22 10:45
*/
@Slf4j
@Service
public class SplashService {
@Inject
private APISettingService apiSettingService;
private final StringProperty value;
public SplashService() {
value = new SimpleStringProperty();
value.set("Forever MC!");
}
@InvokeForInjected
private void injected() {
apiSettingService.valueProperty().addListener((obs, o, setting) -> {
if (setting == null) {
return;
}
APISetting.DynamicList splashes = setting.getSplashes();
if (TimiJava.isNotEmpty(splashes.getActive())) {
value.set(splashes.getActive());
return;
}
Collections.shuffle(splashes.getList());
value.set(splashes.getList().get(0));
});
}
public ReadOnlyStringProperty valueProperty() {
return value;
}
}

View File

@ -0,0 +1,230 @@
package cn.forevermc.launcher.util;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.service.GameService;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.StaticInject;
import com.imyeyu.io.IO;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.FileRequest;
import com.imyeyu.network.GsonRequest;
import com.imyeyu.network.TimiRequest;
import com.imyeyu.utils.Digest;
import com.imyeyu.utils.OS;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.fluent.Request;
import java.io.File;
import java.io.IOException;
import java.util.List;
/**
* Fabric API 解析器
*
* @author 夜雨
* @since 2022-11-21 10:33
*/
@Slf4j
@StaticInject
public class FabricBuilder extends RunAsync<Game> implements OS.FileSystem {
public static final String API = "https://maven.fabricmc.net/";
public static final String LOADER_URL = "https://meta.fabricmc.net/v2/versions/loader";
private static final String API_MIRROR_URL = Resources.TIMI_SERVER_API + "/mirror/%s";
private static final String ATTACH_DL_URL = Resources.TIMI_SERVER_API + "/attachment/download/%s";
@Inject
private static Gson gson;
@Inject
private static GameService gameService;
@Setter
private Game buildGame;
@Override
protected final Game call() throws Exception {
if (buildGame == null) {
throw new TimiException(TimiCode.ARG_MISS);
}
String oldName = buildGame.getName();
String newName = oldName + "-Fabric";
log.info("installing fabric: {}", newName);
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(buildGame);
// 原版数据
File jsonFile = buildGame.getJson();
JsonObject jsonRoot = buildGame.getJsonRoot();
// 获取加载器
log.info("fetching fabric loader..");
JsonArray loaderList = GsonRequest.get(LOADER_URL).asJsonArray();
JsonObject loader = null;
for (int i = 0; i < loaderList.size(); i++) {
loader = loaderList.get(i).getAsJsonObject();
if (loader.get("stable").getAsBoolean()) {
break;
}
}
if (loader == null) {
log.info("not found support fabric loader, game: {}", oldName);
throw new TimiException(TimiCode.RESULT_NULL, TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.fabric.not_support_api"));
}
// 获取 Fabric Loader 加载器的游戏版本版本依赖
JsonObject fabricData, fabricMeta;
{
String loaderVersion = loader.get("version").getAsString();
// Fabric Loader 数据
String fabricMetaResp;
try {
fabricMetaResp = Request.get(LOADER_URL + "/" + oldName + "/" + loaderVersion).execute().returnContent().asString();
} catch (IOException e) {
log.error("not found support fabric api, game: {}", oldName);
throw new TimiException(TimiCode.RESULT_BAD, TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.fabric.not_support"));
}
log.info("injecting fabric lib to original json");
fabricData = JsonParser.parseString(fabricMetaResp).getAsJsonObject();
fabricMeta = fabricData.get("launcherMeta").getAsJsonObject();
}
// 改名
jsonRoot.addProperty("id", newName);
// 启动类
jsonRoot.addProperty("mainClass", fabricMeta.get("mainClass").getAsJsonObject().get("client").getAsString());
{
// 向原版注入 Fabric Loader 依赖
JsonArray minecraftLibs = jsonRoot.get("libraries").getAsJsonArray();
{
// Fabric Loader 加载器
JsonObject fabricLoader = fabricData.get("loader").getAsJsonObject();
fabricLoader.addProperty("url", API);
fabricLoader.addProperty("name", fabricLoader.get("maven").getAsString());
minecraftLibs.add(injectToMinecraftLib(fabricLoader));
}
{
// Fabric Loader 中间件
JsonObject fabricIntermediary = fabricData.get("intermediary").getAsJsonObject();
fabricIntermediary.addProperty("url", API);
fabricIntermediary.addProperty("name", fabricIntermediary.get("maven").getAsString());
minecraftLibs.add(injectToMinecraftLib(fabricIntermediary));
}
{
// Fabric API mod
List<FabricAPIItem> apiItemList = TimiRequest.<List<FabricAPIItem>>get(API_MIRROR_URL.formatted("FabricAPI")).result();
FabricAPIItem item = null;
for (FabricAPIItem fabricAPI : apiItemList) {
if (fabricAPI.minecraftVer.equals(realVersion.getValue())) {
item = fabricAPI;
break;
}
}
if (item == null) {
throw new TimiException(TimiCode.RESULT_NULL, "not found fabric-api");
}
File modFile = IO.file(IO.fitPath(buildGame.getPath().getAbsolutePath()) + "mods" + SEP + item.name);
FileRequest.get(ATTACH_DL_URL.formatted(item.mongoId)).toFile(modFile);
}
{
// 其他依赖
JsonArray fabricLibsClient = fabricMeta.get("libraries").getAsJsonObject().get("client").getAsJsonArray();
JsonArray fabricLibsCommon = fabricMeta.get("libraries").getAsJsonObject().get("common").getAsJsonArray();
for (int i = 0; i < fabricLibsClient.size(); i++) {
minecraftLibs.add(injectToMinecraftLib(fabricLibsClient.get(i).getAsJsonObject()));
}
for (int i = 0; i < fabricLibsCommon.size(); i++) {
minecraftLibs.add(injectToMinecraftLib(fabricLibsCommon.get(i).getAsJsonObject()));
}
}
}
// 原版版本
jsonRoot.addProperty("source-id", oldName);
// 加载器会修改标题,此标题用于判定是否启动成功
jsonRoot.addProperty("title", "Minecraft* " + oldName);
log.info("rename game file");
// 写入文件
IO.toFile(jsonFile, gson.toJson(jsonRoot));
// 重命名版本
File minecraftJarFile = buildGame.getJar();
if (minecraftJarFile != null && minecraftJarFile.exists()) {
IO.rename(minecraftJarFile, newName + ".jar");
}
// 重命名配置
buildGame.setYaml(IO.rename(buildGame.getYaml(), newName + ".yaml"));
buildGame.setJson(IO.rename(jsonFile, newName + ".json"));
IO.rename(jsonFile.getParentFile(), newName);
// 重载
return gameService.buildGame(new File(Path.ABS_PACK + newName));
}
/**
* 将 Fabric 依赖转为原版结构 JSON
*
* @param fabricLib Fabric 依赖
* @return 原版结构 JSON
*/
private JsonObject injectToMinecraftLib(JsonObject fabricLib) throws Exception {
JsonObject minecraftLib = new JsonObject();
// net.fabricmc:tiny-mappings-parser:0.3.0+build.17
String name = fabricLib.get("name").getAsString();
String[] names = name.split(":");
StringBuilder pathSB = new StringBuilder();
String[] paths = names[0].split("\\.");
for (int i = 0; i < paths.length; i++) {
pathSB.append(paths[i]).append('/');
}
pathSB.append(names[1]).append('/').append(names[2]).append('/').append(names[1]).append('-').append(names[2]).append(".jar");
minecraftLib.addProperty("name", name);
JsonObject downloads = new JsonObject();
{
String url = fabricLib.get("url").getAsString() + pathSB;
JsonObject artifact = new JsonObject();
artifact.addProperty("path", pathSB.toString());
artifact.addProperty("url", url);
// TODO 耗时操作,告知 UI
if (fabricLib.has("size")) {
artifact.addProperty("size", fabricLib.get("size").getAsLong());
} else {
artifact.addProperty("size", FileRequest.get(url).length());
}
if (fabricLib.has("sha1")) {
artifact.addProperty("sha1", fabricLib.get("sha1").getAsString());
} else {
artifact.addProperty("sha1", Digest.sha1(FileRequest.get(url).asBytes()));
}
downloads.add("artifact", artifact);
minecraftLib.add("downloads", downloads);
}
return minecraftLib;
}
/**
* Fabric API mod 对象
*
* @author 夜雨
* @since 2024-07-04 19:48
*/
private static class FabricAPIItem {
/** 文件名 */
String name;
/** Fabric 版本 */
String fabricVer;
/** Minecraft 版本 */
String minecraftVer;
/** 下载 MongoId */
String mongoId;
}
}

View File

@ -0,0 +1,45 @@
package cn.forevermc.launcher.util;
import com.imyeyu.utils.OS;
import java.io.File;
/**
* 路径
*
* @author 夜雨
* @since 2023-06-09 15:17
*/
public class Path implements OS.FileSystem {
// ---------- 相对路径 ----------
/** 根目录 */
public static final String P_ROOT = ".minecraft" + SEP;
/** 资源 */
public static final String P_ASSETS = P_ROOT + "assets" + SEP;
/** 依赖 */
public static final String P_LIB = P_ROOT + "libraries" + SEP;
/** 版本 */
public static final String P_PACK = P_ROOT + "versions" + SEP;
// ---------- 绝对路径 ----------
/** 绝对客户端所在目录 */
public static final String ABS = new File("./").getAbsoluteFile().getParent() + SEP;
/** 绝对根目录 */
public static final String ABS_ROOT = ABS + P_ROOT;
/** 绝对资源 */
public static final String ABS_ASSETS = ABS + P_ASSETS;
/** 绝对依赖 */
public static final String ABS_LIB = ABS + P_LIB;
/** 绝对版本 */
public static final String ABS_PACK = ABS + P_PACK;
}

View File

@ -0,0 +1,122 @@
package cn.forevermc.launcher.util;
import cn.forevermc.launcher.ForeverMC;
import cn.forevermc.launcher.bean.APISetting;
import cn.forevermc.launcher.bean.Config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.imyeyu.inject.annotation.IOCReturn;
import com.imyeyu.io.IOSize;
import com.imyeyu.io.IOSpeedService;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.media.MediaPlayer;
import javafx.scene.paint.Color;
/**
* 静态资源
*
* @author 夜雨
* @since 2022-11-18 15:41
*/
@com.imyeyu.inject.annotation.Resources
public class Resources {
/** TimiServer 接口 */
public static final String TIMI_SERVER_API = "http://localhost:8091";
/** TimiServer 接口 */
// public static final String TIMI_SERVER_API = "https://api.imyeyu.com";
// ---------- 图标 ----------
/** 启动器图标 */
public static final Image ICON = new Image("icon.png");
/** 窗体标题图标 */
public static final Image ICON_MC = new Image("assets/img/icon-mc.png");
/** 远古版本窗体标题图标 */
public static final Image ICON_MC_ANCIENT = new Image("assets/img/icon-mc-ancient.png");
/** 按钮 */
public static final Image BTN = new Image("assets/img/btn.png", 396, 36, true, false, true);
/** 选择钩子 X36 */
public static final Image CHECK = new Image("assets/img/check.png", 36, 36, false, false);
/** 选择钩子 X18 */
public static final Image CHECK_SMALL = new Image("assets/img/check.png", 18, 18, false, false);
/** 标题 */
public static final Image TITLE = new Image("assets/img/title.png");
/** 新标题1.20 版本开始) */
public static final Image TITLE_NEW = new Image("assets/img/title-modern.png", 680, 100, true, false);
/** 语言 */
public static final Image LANG = new Image("assets/img/lang.png", 26, 26, true, true);
/** 启动过渡页 */
public static final Image SPLASH_SCREEN = new Image("splash-screen.png");
/** Mojang Logo */
public static final Image MOJANG = new Image("assets/img/mojang.png", 480, -1, true, true);
/** Mojang Logo 远古版本 */
public static final Image MOJANG_ANCIENT = new Image("assets/img/mojang_ancient.png", 480, -1, true, true);
/** 泥土(平铺背景) */
public static final Image DIRT = new Image("assets/img/dirt.png");
/** 信息 */
public static final Image INFO = new Image("assets/img/info.png");
/** FForeverMC 登录方式图标) */
public static final Image F = new Image("assets/img/f.png");
/** MMojang 登录方式图标) */
public static final Image M = new Image("assets/img/m.png");
/** 不死图腾 */
public static final Image TOTEM = new Image("assets/img/totem.png", 32, -1, true, false);
/** 命令方块 */
public static final Image COMMAND_BLOCK = new Image("assets/img/command_block.png", 32, -1, true, false);
/** 文本投影 */
public static final DropShadow TEXT_SHADOW = new DropShadow(0, 1, 1, Color.valueOf("#0009"));
/** @return Gson */
@IOCReturn
public Gson gson() {
return new GsonBuilder().serializeNulls().create();
}
/** @return 主配置 */
@IOCReturn
public Config config() {
return ForeverMC.getConfig();
}
@IOCReturn
public APISetting apiSetting() {
return new APISetting();
}
@IOCReturn
public ObjectProperty<MediaPlayer> bgmPlayer() {
return new SimpleObjectProperty<>();
}
/** @return 字节速度计算主项 */
@IOCReturn
public IOSpeedService.Item ioSpeedItem() {
IOSpeedService service = IOSpeedService.getInstance();
service.setSalt(IOSize.KB * 4);
service.start();
return service.createItem();
}
}

View File

@ -0,0 +1,15 @@
package cn.forevermc.launcher.util;
import javafx.scene.media.AudioClip;
/**
* 音频
*
* @author 夜雨
* @since 2023-06-09 15:15
*/
public class Sound {
/** 点击音效 */
public static final AudioClip CLICK = new AudioClip(ClassLoader.getSystemResource("assets/sound/click.wav").toExternalForm());
}

View File

@ -0,0 +1,34 @@
package cn.forevermc.launcher.view;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.view.components.RootLayout;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Inject;
/**
* 主视图
*
* @author 夜雨
* @since 2022-11-18 15:36
*/
public abstract class ViewMain extends Application implements TimiFXUI {
@Inject
private RootLayout root;
@Override
public void start(Stage stage) {
Scene scene = new Scene(root);
scene.getStylesheets().addAll(CSS_FONT, CSS_STYLE, "style.css");
stage.setScene(scene);
stage.getIcons().add(Resources.ICON);
stage.setTitle("Forever MC");
stage.setMinWidth(870);
stage.setMinHeight(519);
stage.setWidth(870);
stage.setHeight(519);
}
}

View File

@ -0,0 +1,120 @@
package cn.forevermc.launcher.view.components;
import cn.forevermc.launcher.ForeverMC;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.view.components.control.MCLabel;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import com.imyeyu.fx.utils.BgFill;
import com.imyeyu.fx.utils.BorderStroke;
import com.imyeyu.inject.TimiInject;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.StaticInject;
/**
* 内容面板,具有标题和底部操作面板
*
* @author 夜雨
* @since 2024-07-14 17:38
*/
@StaticInject
public abstract class AbstractContentPane extends AbstractPane {
@Inject
private static LaunchGame launchGame;
protected final GridPane bottom;
protected final StackPane content;
private final Label title;
private final Region shadowTop, shadowBottom;
private final StackPane strokeWhitePane;
public AbstractContentPane() {
title = new MCLabel();
title.setPadding(new Insets(24, 0, 16, 0));
// 黑边
StackPane strokeBlackPane = new StackPane();
strokeBlackPane.setBorder(new BorderStroke(Color.valueOf("#000A")).width(2, 0, 2, 0).build());
{
// 白边
strokeWhitePane = new StackPane();
strokeWhitePane.setBackground(new BgFill("#0007").build());
{
// 旧 UI 阴影
shadowTop = new Region();
shadowBottom = new Region();
{
shadowTop.setMaxHeight(8);
shadowTop.setBackground(new BgFill("#0006", "#0000").toBottom().build());
shadowTop.setPickOnBounds(false);
shadowTop.setFocusTraversable(true);
shadowTop.setMouseTransparent(true);
shadowBottom.setMaxHeight(8);
shadowBottom.setBackground(new BgFill("#0000", "#0006").toBottom().build());
shadowBottom.setPickOnBounds(false);
shadowBottom.setFocusTraversable(true);
shadowBottom.setMouseTransparent(true);
}
// 内容面板
content = new StackPane();
StackPane.setAlignment(shadowTop, Pos.TOP_CENTER);
StackPane.setAlignment(shadowBottom, Pos.BOTTOM_CENTER);
strokeWhitePane.getChildren().addAll(content, shadowTop, shadowBottom);
}
strokeBlackPane.getChildren().addAll(strokeWhitePane);
}
bottom = new GridPane();
bottom.setHgap(8);
bottom.setVgap(8);
bottom.setAlignment(Pos.TOP_CENTER);
bottom.setPadding(new Insets(12));
setAlignment(title, Pos.CENTER);
setTop(title);
setCenter(strokeBlackPane);
setBottom(bottom);
ForeverMC.getInjectApp().addAfterInjectListener(timiInject -> {
strokeWhitePane.borderProperty().bind(Bindings.createObjectBinding(() -> {
if (launchGame.isNewUI()) {
return new BorderStroke(Color.valueOf("#FFF4")).width(2, 0, 2, 0).build();
}
return new BorderStroke(Color.valueOf("#000C")).width(2, 0, 2, 0).build();
}, launchGame));
shadowTop.visibleProperty().bind(launchGame.newUIProperty().not());
shadowBottom.visibleProperty().bind(launchGame.newUIProperty().not());
});
}
protected void setTitle(String title) {
this.title.setText(title);
}
protected void setContentPane(Node node) {
content.getChildren().setAll(node);
}
protected void addBottomPaneValue(Node child, int columnIndex, int rowIndex, int colSpan, int rowSpan) {
bottom.add(child, columnIndex, rowIndex, colSpan, rowSpan);
}
protected void addBottomPaneValue(int rowIndex, Node... children) {
bottom.addRow(rowIndex, children);
}
protected void setBottomPaneValue(Node... children) {
bottom.getChildren().clear();
bottom.addRow(0, children);
}
}

View File

@ -0,0 +1,48 @@
package cn.forevermc.launcher.view.components;
import cn.forevermc.launcher.view.components.control.MCList;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.geometry.Insets;
/**
* 列表面板,基于内容面板的列表视图
*
* @author 夜雨
* @since 2022-11-23 17:41
*/
public abstract class AbstractListPane<T> extends AbstractContentPane {
private static final Insets PADDING_SCROLL = new Insets(0, 0, 0, 32);
private static final Insets PADDING_SCROLL_R = new Insets(0, 0, 0, 0);
protected final MCList<T> list;
public AbstractListPane() {
list = new MCList<>();
content.paddingProperty().bind(Bindings.when(list.showingVScrollBarProperty()).then(PADDING_SCROLL).otherwise(PADDING_SCROLL_R));
content.maxWidthProperty().bind(Bindings.when(list.showingVScrollBarProperty()).then(690D).otherwise(640));
setContentPane(list);
}
@Override
protected void onHide() {
list.getItems().clear();
}
/** @return true 为正在加载 */
public boolean isLoading() {
return list.isLoading();
}
/** @return 正在加载属性 */
public BooleanProperty loadingProperty() {
return list.loadingProperty();
}
/** @param isLoading true 为正在加载 */
public void setLoading(boolean isLoading) {
list.setLoading(isLoading);
}
}

View File

@ -0,0 +1,52 @@
package cn.forevermc.launcher.view.components;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.java.bean.Callback;
import javafx.scene.layout.BorderPane;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 抽象面板
*
* @author 夜雨
* @since 2022-11-21 14:23
*/
@Data
@EqualsAndHashCode(callSuper = true)
public abstract class AbstractPane extends BorderPane implements TimiFXUI {
/** 显示事件,触发慢于 {@link #onShow()},一次性对象,触发即删除 */
private Callback onShowEvent;
/** 隐藏事件,触发慢于 {@link #onHide()},一次性对象,触发即删除 */
private Callback onHideEvent;
/** 触发显示(页面服务调度) */
public final void show() {
onShow();
if (onShowEvent != null) {
onShowEvent.handler();
onShowEvent = null;
}
}
/** 触发隐藏(页面服务调度) */
public final void hide() {
onHide();
if (onHideEvent != null) {
onHideEvent.handler();
onHideEvent = null;
}
}
/** 显示时触发UI 线程 */
protected void onShow() {
// 子类实现
}
/** 隐藏时触发UI 线程 */
protected void onHide() {
// 子类实现
}
}

View File

@ -0,0 +1,72 @@
package cn.forevermc.launcher.view.components;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.skin.ScrollPaneSkin;
import javafx.scene.layout.StackPane;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.fx.utils.BorderStroke;
import com.imyeyu.fx.utils.SmoothScroll;
import com.imyeyu.java.ref.Ref;
/**
* 滚动面板,支持滚动的内容面板
*
* @author 夜雨
* @since 2024-07-14 17:43
*/
@Slf4j
public abstract class AbstractScrollPane extends AbstractContentPane {
private static final Insets PADDING_SCROLL = new Insets(0, 0, 0, 32);
private static final Insets PADDING_SCROLL_R = new Insets(0, 0, 0, 0);
private final StackPane content;
protected final BooleanBinding showingVScrollBar;
private ScrollBar vsb;
public AbstractScrollPane() {
content = new StackPane();
content.setAlignment(Pos.CENTER);
content.setPadding(new Insets(16, 0, 12, 0));
ScrollPane scrollPane = new ScrollPane() {{
setAlignment(content, Pos.TOP_CENTER);
setFitToWidth(true);
getStyleClass().add(CSS.BG_TP);
setContent(content);
SmoothScroll.scrollPane(this);
}};
setContentPane(scrollPane);
showingVScrollBar = Bindings.createBooleanBinding(() -> {
// TODO 最大化或最小化时无效
try {
if (vsb == null && scrollPane.getSkin() != null && scrollPane.getSkin() instanceof ScrollPaneSkin skin) {
vsb = Ref.getFieldValue(skin, "vsb", ScrollBar.class);
}
if (vsb != null) {
return vsb.isVisible();
}
} catch (Exception e) {
log.error("ref MCList virtual scroll bar error", e);
}
return false;
}, scrollPane.skinProperty(), scrollPane.layoutBoundsProperty());
scrollPane.paddingProperty().bind(Bindings.when(showingVScrollBar).then(PADDING_SCROLL).otherwise(PADDING_SCROLL_R));
scrollPane.maxWidthProperty().bind(Bindings.when(showingVScrollBar).then(690D).otherwise(640));
}
protected void setScrollContent(Node node) {
content.getChildren().setAll(node);
}
}

View File

@ -0,0 +1,112 @@
package cn.forevermc.launcher.view.components;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.util.Resources;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import com.imyeyu.fx.icon.TimiFXIcon;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BorderStroke;
import javax.swing.border.StrokeBorder;
/**
* 登录方式选择器
*
* @author 夜雨
* @since 2024-08-03 11:41
*/
public class LoginTypeSelector extends StackPane {
private final ObservableList<Game.Option.LoginType> items;
private final ObjectProperty<Game.Option.LoginType> value;
private final BooleanProperty requiredOffline;
public LoginTypeSelector() {
items = FXCollections.observableArrayList();
items.setAll(Game.Option.LoginType.OFFLINE, Game.Option.LoginType.FOREVER_MC); // 暂不支持 MOJANG
value = new SimpleObjectProperty<>();
requiredOffline = new SimpleBooleanProperty(false);
Text offline = TimiFXIcon.fromName("UNLINK", TimiFXUI.Colorful.WHITE);
ImageView foreverMC = new ImageView(Resources.F);
ImageView mojang = new ImageView(Resources.M);
setMaxSize(16, 16);
// ---------- 事件 ----------
addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
if (!isRequiredOffline()) {
int nextIndex = items.indexOf(value.get()) + 1;
if (items.size() <= nextIndex) {
nextIndex = 0;
}
value.set(items.get(nextIndex));
}
});
value.addListener((obs, o, n) -> getChildren().setAll(switch (value.get()) {
case OFFLINE -> offline;
case FOREVER_MC -> foreverMC;
case MOJANG -> mojang;
}));
requiredOffline.addListener((obs, o, n) -> {
if (isRequiredOffline()) {
value.set(Game.Option.LoginType.OFFLINE);
}
});
}
public boolean isOffline() {
return value.get() == Game.Option.LoginType.OFFLINE;
}
public boolean isOnline() {
return !isOffline();
}
public boolean isForeverMC() {
return value.get() == Game.Option.LoginType.FOREVER_MC;
}
public boolean isMojang() {
return value.get() == Game.Option.LoginType.MOJANG;
}
public void setValue(Game.Option.LoginType value) {
this.value.set(value);
}
public Game.Option.LoginType getValue() {
return value.get();
}
public ObjectProperty<Game.Option.LoginType> valueProperty() {
return value;
}
public void setRequiredOffline(boolean requiredOffline) {
this.requiredOffline.set(requiredOffline);
}
public boolean isRequiredOffline() {
return requiredOffline.get();
}
public BooleanProperty requiredOfflineProperty() {
return requiredOffline;
}
}

View File

@ -0,0 +1,103 @@
package cn.forevermc.launcher.view.components;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.bean.Page;
import cn.forevermc.launcher.service.BGService;
import cn.forevermc.launcher.service.PageService;
import cn.forevermc.launcher.util.Resources;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.event.EventHandler;
import javafx.scene.effect.GaussianBlur;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import com.imyeyu.fx.utils.BgImage;
import com.imyeyu.inject.annotation.Component;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
/**
* 根布局
*
* @author 夜雨
* @since 2022-11-21 16:39
*/
@Component
public class RootLayout extends StackPane {
@Inject
private BGService bgService;
@Inject
private PageService pageService;
@Inject
private LaunchGame launchGame;
private final StackPane bg;
private final BorderPane main;
public RootLayout() {
// 背景面板
bg = new StackPane();
bg.setScaleX(1.1);
bg.setScaleY(1.1);
main = new BorderPane();
getChildren().addAll(bg, main);
// ---------- 事件 ----------
// 动态背景
final float bgStep = .1F;
addEventFilter(MouseEvent.MOUSE_MOVED, new EventHandler<>() {
// 面板 xy 坐标,旧的鼠标 xy 坐标
private double pX = 0, pY = 0, moX, moY;
@Override
public void handle(MouseEvent event) {
if (event.getX() < moX && pX < 0) {
pX += bgStep;
} else {
if (moX < event.getX() && -20 < pX) {
pX -= bgStep;
}
}
if (event.getY() < moY && pY < 0) {
pY += bgStep;
} else {
if (moY < event.getY() && -20 < pY) {
pY -= bgStep;
}
}
bg.setTranslateX(pX);
bg.setTranslateY(pY);
moX = event.getX();
moY = event.getY();
}
});
}
@InvokeForInjected
private void injected() {
Background legacyBG = new BgImage(Resources.DIRT).build();
bg.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
if (launchGame.isNewUI()) {
return bgService.getValue();
}
if (pageService.getActivatedPage() == Page.MENU) {
return bgService.getValue();
}
return legacyBG;
}, launchGame.newUIProperty(), pageService.activatedPageProperty()));
BooleanBinding withOutBlur = pageService.activatedPageProperty().isEqualTo(Page.MENU).or(launchGame.newUIProperty().not());
bg.effectProperty().bind(Bindings.when(withOutBlur).then((GaussianBlur) null).otherwise(new GaussianBlur(16)));
}
public void setPage(AbstractPane page) {
main.setCenter(page);
}
}

View File

@ -0,0 +1,75 @@
package cn.forevermc.launcher.view.components;
import cn.forevermc.launcher.util.Resources;
import com.google.gson.JsonObject;
import com.imyeyu.fx.task.RunAsync;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.ui.components.VersionLabel;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.GsonRequest;
import lombok.extern.slf4j.Slf4j;
/**
* Timi 通用版本标签
*
* @author 夜雨
* @since 2022-05-31 23:24
*/
@Slf4j
public class TimiVersionLabel extends VersionLabel<JsonObject> {
private final String nowVersion, appName;
public TimiVersionLabel(String nowVersion, String appName) {
super(TimiFXUI.MULTILINGUAL.textArgs("version.checking", nowVersion));
this.appName = appName;
this.nowVersion = nowVersion;
Status.NORMAL.setTextColor(WHITE);
version.setEffect(Resources.TEXT_SHADOW);
RunAsync.later(() -> checkVersion(nowVersion), 2000);
}
@Override
protected JsonObject run() {
try {
Thread.sleep(2000);
log.info("checking version..");
String api = Resources.TIMI_SERVER_API + "/versions/" + appName;
return GsonRequest.get(api).asJsonObject();
} catch (Exception e) {
throw new TimiException(TimiCode.ERROR, e.getMessage());
}
}
@Override
protected String onReturn(JsonObject resp) {
int code = resp.get("code").getAsInt();
if (code == 20000) {
JsonObject data = resp.get("data").getAsJsonObject();
setUpdateURL(data.get("url").getAsString());
if (data.has("content")) {
String content = data.get("content").getAsString();
if (TimiJava.isNotEmpty(content)) {
this.content.setText(content);
}
}
return data.get("version").getAsString();
} else {
throw new TimiException(TimiCode.fromCode(code), resp.get("msg").getAsString());
}
}
@Override
protected String updateText(String newVersion) {
return TimiFXUI.MULTILINGUAL.textArgs("version.new", nowVersion, newVersion);
}
@Override
protected String failText(Throwable e) {
return TimiFXUI.MULTILINGUAL.textArgs("version.fail", nowVersion);
}
}

View File

@ -0,0 +1,98 @@
package cn.forevermc.launcher.view.components.control;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.util.Sound;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Border;
import javafx.scene.layout.StackPane;
import lombok.Getter;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BgImage;
import com.imyeyu.fx.utils.BorderStroke;
/**
* MC 按钮
*
* @author 夜雨
* @since 2022-11-22 23:05
*/
public class MCButton extends Button implements TimiFXUI {
private static final Border BORDER_WHITE = new BorderStroke(Colorful.WHITE).width(2).build();
private static final Border BORDER_BLACK = new BorderStroke(Colorful.BLACK).width(2).build();
@Getter
protected final Label label;
protected final StackPane root;
public MCButton() {
this("");
}
public MCButton(String text) {
this(null, text);
}
/**
* 默认构造
*
* @param size 尺寸,为 null 时根据文本自适应
* @param text 按钮文本
*/
public MCButton(Double size, String text) {
// 高光
StackPane light = new StackPane();
light.setBorder(new BorderStroke(Colorful.WHITE).width(2, 0, 0, 2).build());
light.setOpacity(.4);
light.visibleProperty().bind(disabledProperty().not());
// 阴影
StackPane dark = new StackPane();
dark.setBorder(new BorderStroke(Colorful.BLACK).width(0, 2, 3, 0).build());
dark.setOpacity(.3);
dark.visibleProperty().bind(disabledProperty().not());
// 禁用蒙版
StackPane mark = new StackPane();
mark.setOpacity(.7);
mark.visibleProperty().bind(disabledProperty());
mark.setBackground(BG.BLACK);
label = new Label(text);
label.setEffect(Resources.TEXT_SHADOW);
label.setTextFill(Colorful.WHITE);
label.setPadding(new Insets(8, 16, 8, 16));
getStyleClass().clear();
if (size != null) {
setPrefWidth(size);
}
borderProperty().bind(Bindings.createObjectBinding(() -> {
if (!isDisabled() && isHover()) {
return BORDER_WHITE;
}
return BORDER_BLACK;
}, hoverProperty(), disabledProperty()));
setGraphic(root = new StackPane() {{
setBackground(new BgImage(Resources.BTN).build());
getChildren().addAll(light, dark, mark, label);
}});
// ---------- 事件 ----------
textProperty().addListener((obs, o, newText) -> {
label.setText(newText);
setText("");
});
addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
if (!isDisabled()) {
Sound.CLICK.play();
}
});
}
}

View File

@ -0,0 +1,98 @@
package cn.forevermc.launcher.view.components.control;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.util.Sound;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventHandler;
import javafx.scene.control.Label;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Border;
import javafx.scene.layout.StackPane;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BgImage;
import com.imyeyu.fx.utils.BorderStroke;
/**
* MC 复选框
*
* @author 夜雨
* @since 2022-11-24 01:14
*/
public class MCCheckBox extends StackPane implements TimiFXUI {
private static final Border BORDER_WHITE = new BorderStroke(Colorful.WHITE).width(2).build();
private static final Border BORDER_BLACK = new BorderStroke(Colorful.BLACK).width(2).build();
private final BooleanProperty selected;
private final ObjectProperty<Label> label;
public MCCheckBox() {
selected = new SimpleBooleanProperty(false);
label = new SimpleObjectProperty<>();
ImageView check = new ImageView(Resources.CHECK);
check.visibleProperty().bind(selected);
setMaxSize(34, 34);
setPrefSize(34, 34);
borderProperty().bind(Bindings.when(hoverProperty()).then(BORDER_WHITE).otherwise(BORDER_BLACK));
getChildren().addAll(new StackPane() {{
setBackground(new BgImage(Resources.BTN).build());
getChildren().add(new StackPane() {{
setOpacity(.6);
setBackground(BG.BLACK);
}});
}}, check);
// ---------- 事件 ----------
EventHandler<MouseEvent> clickEvent = e -> {
selected.set(!selected.get());
Sound.CLICK.play();
};
addEventFilter(MouseEvent.MOUSE_PRESSED, clickEvent);
label.addListener((obs, oldLabel, newLabel) -> {
if (newLabel != null) {
newLabel.addEventFilter(MouseEvent.MOUSE_PRESSED, clickEvent);
}
if (oldLabel != null) {
oldLabel.removeEventFilter(MouseEvent.MOUSE_PRESSED, clickEvent);
}
});
}
/** @return true 为已选中 */
public boolean isSelected() {
return selected.get();
}
/** @param selected true 为已选中 */
public void setSelected(boolean selected) {
this.selected.set(selected);
}
/** @return 选中监听 */
public BooleanProperty selectedProperty() {
return selected;
}
/** @return 标签,此标签点击触发复选 */
public Label getLabel() {
return label.get();
}
/** @param label 设置标签,此标签点击触发复选 */
public void setLabel(Label label) {
this.label.set(label);
}
/** @return 标签监听,此标签点击触发复选 */
public ObjectProperty<Label> labelProperty() {
return label;
}
}

View File

@ -0,0 +1,25 @@
package cn.forevermc.launcher.view.components.control;
import cn.forevermc.launcher.util.Resources;
import javafx.scene.control.Label;
import com.imyeyu.fx.ui.TimiFXUI;
/**
* MC 标签
*
* @author 夜雨
* @since 2022-11-24 21:44
*/
public class MCLabel extends Label implements TimiFXUI {
public MCLabel() {
this("");
}
public MCLabel(String text) {
super(text);
setEffect(Resources.TEXT_SHADOW);
setTextFill(Colorful.WHITE);
}
}

View File

@ -0,0 +1,60 @@
package cn.forevermc.launcher.view.components.control;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.util.Sound;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Border;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BorderStroke;
import com.imyeyu.network.Network;
/**
* MC 链接
*
* @author 夜雨
* @since 2022-11-24 17:46
*/
public class MCLink extends Label implements TimiFXUI {
private static final Border BORDER_DEFAULT = new BorderStroke(Colorful.TRANSPARENT).width(0, 0, 2, 0).build();
private static final Border BORDER_HOVER = new BorderStroke(Colorful.LIGHT_GRAY).width(0, 0, 2, 0).build();
private final StringProperty url;
public MCLink(String text) {
this(text, text);
}
public MCLink(String text, String url) {
super(text);
this.url = new SimpleStringProperty(url);
setEffect(Resources.TEXT_SHADOW);
setTextFill(Colorful.WHITE);
borderProperty().bind(Bindings.when(hoverProperty()).then(BORDER_HOVER).otherwise(BORDER_DEFAULT));
addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
Network.openURIInBrowser(this.url.get());
Sound.CLICK.play();
});
}
/** @return 获取 URL */
public String getUrl() {
return url.get();
}
/** @param url 设置 URL */
public void setUrl(String url) {
this.url.set(url);
}
/** @return URL 监听 */
public StringProperty urlProperty() {
return url;
}
}

View File

@ -0,0 +1,77 @@
package cn.forevermc.launcher.view.components.control;
import com.sun.javafx.scene.control.VirtualScrollBar;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.scene.control.ListView;
import javafx.scene.control.skin.ListViewSkin;
import javafx.scene.control.skin.VirtualFlow;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.SmoothScroll;
import com.imyeyu.java.ref.Ref;
/**
* @author 夜雨
* @since 2024-07-14 20:38
*/
@Slf4j
public class MCList<T> extends ListView<T> {
protected static final String STYLE_CLASS_LIST = "mc-list";
protected final BooleanProperty isLoading;
protected final BooleanBinding showingVScrollBar;
private VirtualScrollBar vBar;
public MCList() {
isLoading = new SimpleBooleanProperty(true);
MCLabel emptyList = new MCLabel(TimiFXUI.MULTILINGUAL.text("select.empty"));
MCLabel loading = new MCLabel(TimiFXUI.MULTILINGUAL.text("loading"));
placeholderProperty().bind(Bindings.when(isLoading).then(loading).otherwise(emptyList));
getStyleClass().addAll(STYLE_CLASS_LIST, TimiFXUI.CSS.BORDER_N);
SmoothScroll.virtual(this);
showingVScrollBar = Bindings.createBooleanBinding(() -> {
// TODO 最大化最小化无效
try {
if (vBar == null && getSkin() != null && getSkin() instanceof ListViewSkin<?> skin) {
VirtualFlow<?> flow = Ref.getFieldValue(skin, "flow", VirtualFlow.class);
vBar = Ref.getFieldValue(flow, "vbar", VirtualScrollBar.class);
}
if (vBar != null) {
return vBar.isVisible();
}
} catch (Exception e) {
log.error("ref MCList virtual scroll bar error", e);
}
return false;
}, skinProperty(), layoutBoundsProperty());
}
public BooleanBinding showingVScrollBarProperty() {
return showingVScrollBar;
}
/** @return true 为正在加载 */
public boolean isLoading() {
return isLoading.get();
}
/** @return 正在加载属性 */
public BooleanProperty loadingProperty() {
return isLoading;
}
/** @param isLoading true 为正在加载 */
public void setLoading(boolean isLoading) {
this.isLoading.set(isLoading);
}
}

View File

@ -0,0 +1,84 @@
package cn.forevermc.launcher.view.components.control;
import cn.forevermc.launcher.util.Sound;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.Skin;
import javafx.scene.control.skin.TextFieldSkin;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.Border;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import com.imyeyu.fx.ui.MinecraftFont;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BgFill;
import com.imyeyu.fx.utils.BorderStroke;
import com.imyeyu.java.ref.Ref;
/**
* MC 密码输入框
*
* @author 夜雨
* @since 2022-11-23 00:27
*/
public class MCPasswordField extends PasswordField implements TimiFXUI {
private static final Border BORDER_FOCUSED = new BorderStroke(Colorful.WHITE).width(2).build();
private static final Border BORDER_DEFAULT = new BorderStroke("#A0A0A0").width(2).build();
private static final Background BG = new BgFill("#000D").build();
private static final Background BG_DISABLED = new BgFill("#444C").build();
private final Label label;
public MCPasswordField() {
label = new Label();
label.setTextFill(Colorful.LIGHT_GRAY);
label.translateXProperty().bind(label.widthProperty().negate().subtract(4));
getStyleClass().setAll("mc-text-field");
setFont(MinecraftFont.X16());
borderProperty().bind(Bindings.when(focusedProperty()).then(BORDER_FOCUSED).otherwise(BORDER_DEFAULT));
paddingProperty().bind(Bindings.createObjectBinding(() -> new Insets(8, 0, 8, label.getWidth() + 8), label.widthProperty()));
backgroundProperty().bind(Bindings.when(disabledProperty()).then(BG_DISABLED).otherwise(BG));
addEventFilter(MouseEvent.MOUSE_PRESSED, e -> Sound.CLICK.play());
}
@Override
protected Skin<?> createDefaultSkin() {
Skin<?> skin = super.createDefaultSkin();
if (skin instanceof TextFieldSkin textFieldSkin) {
try {
Pane textGroup = Ref.getFieldValue(textFieldSkin, "textGroup", Pane.class);
StackPane root = new StackPane();
StackPane.setAlignment(label, Pos.CENTER_LEFT);
root.getChildren().addAll(label, textGroup);
textFieldSkin.getChildren().setAll(root);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return skin;
}
/** @return 标签 */
public String getLabel() {
return label.getText();
}
/** @param label 设置标签 */
public void setLabel(String label) {
this.label.setText(label);
}
/** @return 标签监听 */
public StringProperty labelProperty() {
return label.textProperty();
}
}

View File

@ -0,0 +1,74 @@
package cn.forevermc.launcher.view.components.control;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.Setter;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArgReturn;
/**
* MC 选择器
*
* @author 夜雨
* @since 2022-11-23 17:04
*/
public class MCSelector<T> extends MCButton {
/** 数据列表 */
@Getter
private final ObservableList<T> items;
/** 字符串转换器 */
@Setter
private CallbackArgReturn<T, String> stringConverter;
private final ObjectProperty<T> value;
public MCSelector() {
items = FXCollections.observableArrayList();
value = new SimpleObjectProperty<>();
// 监听值
value.addListener((obs, o, newValue) -> {
if (stringConverter == null) {
if (newValue == null) {
label.setText("");
} else {
label.setText(newValue.toString());
}
} else {
label.setText(stringConverter.handler(newValue));
}
});
// 点击切换
setOnAction(e -> {
if (TimiJava.isNotEmpty(items)) {
int i = items.indexOf(value.get()) + 1;
value.set(items.get(i == items.size() ? 0 : i));
}
});
}
/**
* 选择数据
*
* @param value 目标数据
*/
public void setValue(T value) {
this.value.set(value);
}
/** @return 当前选择 */
public T getValue() {
return value.get();
}
/** @return 值监听 */
public ObjectProperty<T> valueProperty() {
return value;
}
}

View File

@ -0,0 +1,228 @@
package cn.forevermc.launcher.view.components.control;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.util.Sound;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Border;
import javafx.scene.layout.StackPane;
import lombok.Getter;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BgFill;
import com.imyeyu.fx.utils.BgImage;
import com.imyeyu.fx.utils.BorderStroke;
import com.imyeyu.utils.Calc;
import java.util.ArrayList;
import java.util.List;
/**
* MC 滑动选择
*
* @author 夜雨
* @since 2022-11-23 15:31
*/
public class MCSlider extends StackPane implements TimiFXUI {
private static final Border BORDER_WHITE = new BorderStroke(Colorful.WHITE).width(2).build();
private static final Border BORDER_BLACK = new BorderStroke(Colorful.BLACK).width(2).build();
/** 标签 */
@Getter
private final Label label;
private final MCButton thumb;
private final DoubleProperty min, max, step, value;
private final ObjectProperty<Double[]> steps;
public MCSlider() {
min = new SimpleDoubleProperty();
max = new SimpleDoubleProperty();
step = new SimpleDoubleProperty();
value = new SimpleDoubleProperty();
steps = new SimpleObjectProperty<>();
thumb = new MCButton() {{
label.setVisible(false);
label.setManaged(false);
}};
thumb.setPrefWidth(16);
thumb.prefHeightProperty().bind(heightProperty());
thumb.borderProperty().unbind();
thumb.borderProperty().bind(Bindings.when(hoverProperty()).then(BORDER_WHITE).otherwise(BORDER_BLACK));
thumb.setMouseTransparent(true);
label = new Label();
label.setPadding(new Insets(8));
label.setTextFill(Colorful.WHITE);
label.setEffect(Resources.TEXT_SHADOW);
label.setMouseTransparent(true);
setAlignment(thumb, Pos.CENTER_LEFT);
setBackground(new BgFill(Colorful.BLACK).build());
getChildren().addAll(new StackPane() {{
setMargin(this, new Insets(2));
setBackground(new BgImage(Resources.BTN).build());
getChildren().add(new StackPane() {{
setOpacity(.6);
setBackground(BG.BLACK);
}});
}}, thumb, label);
// ---------- 事件 ----------
// 计算步进
steps.bind(Bindings.createObjectBinding(() -> {
if (step.get() <= 0) {
return null;
}
List<Double> result = new ArrayList<>();
// 去头尾所有步进值
for (double i = min.get() + step.get(); i <= max.get() - step.get(); i += step.get()) {
result.add(i);
}
return result.toArray(new Double[0]);
}, min, max, step));
min.set(0);
max.set(100);
step.set(-1);
// 点下
addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
Sound.CLICK.play();
calcValue(e);
e.consume();
});
// 拖拽
addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
calcValue(e);
e.consume();
});
widthProperty().addListener((obs, o, n) -> calcNearValue(value.get()));
}
/**
* 计算滑块位置
*
* @param event 鼠标事件
*/
private void calcValue(MouseEvent event) {
if (event.getX() < thumb.getWidth() * .5) {
thumb.setTranslateX(0);
value.set(min.get());
return;
}
if (getWidth() - thumb.getWidth() * .5 < event.getX()) {
thumb.setTranslateX(getWidth() - thumb.getWidth());
value.set(max.get());
return;
}
double percent = event.getX() / getWidth();
if (step.get() == -1) {
thumb.setTranslateX((int) getPrefWidth() * percent - thumb.getPrefWidth() * .5);
value.set(max.get() * percent);
} else {
// 坐标对应值
calcNearValue(event.getX() / getWidth() * max.get());
}
}
/**
* 计算最近位置
*
* @param value 当前值
*/
private void calcNearValue(double value) {
if (steps.get() == null) {
thumb.setTranslateX((getPrefWidth() - thumb.getPrefWidth()) * (value / (max.get() - min.get())));
} else {
// 最近步进位置
double min = steps.get()[0];
double max = steps.get()[steps.get().length - 1];
double nearIndex = (steps.get().length - 1) * ((value - this.min.get()) / (max - min)) - 1;
// 最近值
int index = Calc.round(nearIndex);
if (0 <= index) {
if (index < this.steps.get().length) {
// 反向计算滑块位置
this.value.set(steps.get()[index]);
thumb.setTranslateX((int) (this.value.get() / this.max.get() * getWidth() - thumb.getWidth() * .5));
} else {
// 达到最大值
this.value.set(this.max.get());
thumb.setTranslateX((int) (getWidth() - thumb.getPrefWidth()));
}
}
}
}
/** @return 最小值 */
public double getMin() {
return min.get();
}
/** @param min 最小值 */
public void setMin(double min) {
this.min.set(min);
}
/** @return 最小值监听 */
public DoubleProperty minProperty() {
return min;
}
/** @return 最大值 */
public double getMax() {
return max.get();
}
/** @param max 最大值 */
public void setMax(double max) {
this.max.set(max);
}
/** @return 最大值监听 */
public DoubleProperty maxProperty() {
return max;
}
/** @return 步进 */
public double getStep() {
return step.get();
}
/** @param step 步进 */
public void setStep(double step) {
this.step.set(step);
}
/** @return 步进监听 */
public DoubleProperty stepProperty() {
return step;
}
/** @return 当前值 */
public double getValue() {
return value.get();
}
/** @param value 当前值 */
public void setValue(double value) {
this.value.set(value);
}
/** @return 当前值监听 */
public DoubleProperty valueProperty() {
return value;
}
}

View File

@ -0,0 +1,94 @@
package cn.forevermc.launcher.view.components.control;
import cn.forevermc.launcher.util.Sound;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.TextField;
import javafx.scene.control.skin.TextFieldSkin;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.Border;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import com.imyeyu.fx.ui.MinecraftFont;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BgFill;
import com.imyeyu.fx.utils.BorderStroke;
import com.imyeyu.java.ref.Ref;
/**
* MC 输入框
*
* @author 夜雨
* @since 2022-11-23 00:27
*/
public class MCTextField extends TextField implements TimiFXUI {
private static final Border BORDER_FOCUSED = new BorderStroke(Colorful.WHITE).width(2).build();
private static final Border BORDER_DEFAULT = new BorderStroke("#A0A0A0").width(2).build();
private static final Background BG = new BgFill("#000A").build();
private static final Background BG_DISABLED = new BgFill("#444C").build();
protected final Label label;
public MCTextField() {
this("");
}
public MCTextField(String label) {
this.label = new Label(label);
this.label.setTextFill(Colorful.LIGHT_GRAY);
this.label.setMouseTransparent(true);
this.label.translateXProperty().bind(this.label.widthProperty().negate());
getStyleClass().setAll("mc-text-field");
setFont(MinecraftFont.X16());
borderProperty().bind(Bindings.when(focusedProperty()).then(BORDER_FOCUSED).otherwise(BORDER_DEFAULT));
paddingProperty().bind(Bindings.createObjectBinding(() -> new Insets(8, 0, 8, this.label.getWidth() + 8), this.label.widthProperty()));
backgroundProperty().bind(Bindings.when(disabledProperty()).then(BG_DISABLED).otherwise(BG));
addEventFilter(MouseEvent.MOUSE_PRESSED, e -> Sound.CLICK.play());
addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume);
}
@Override
protected Skin<?> createDefaultSkin() {
Skin<?> skin = super.createDefaultSkin();
if (skin instanceof TextFieldSkin textFieldSkin) {
try {
Pane textGroup = Ref.getFieldValue(textFieldSkin, "textGroup", Pane.class);
StackPane root = new StackPane();
StackPane.setAlignment(label, Pos.CENTER_LEFT);
StackPane.setMargin(textGroup, new Insets(0, 0, 0, 4));
root.getChildren().addAll(label, textGroup);
textFieldSkin.getChildren().setAll(root);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
return skin;
}
/** @return 标签 */
public String getLabel() {
return label.getText();
}
/** @param label 标签 */
public void setLabel(String label) {
this.label.setText(label);
}
/** @return 标签监听 */
public StringProperty labelProperty() {
return label.textProperty();
}
}

View File

@ -0,0 +1,78 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.ForeverMC;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.view.components.AbstractPane;
import cn.forevermc.launcher.view.components.TimiVersionLabel;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCLabel;
import cn.forevermc.launcher.view.components.control.MCLink;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BgImage;
import com.imyeyu.fx.utils.Column;
/**
* 关于面板
*
* @author 夜雨
* @since 2022-11-24 17:44
*/
public abstract class AboutPane extends AbstractPane {
protected final MCButton close;
protected final ImageView title;
public AboutPane() {
// 标题
title = new ImageView(Resources.TITLE);
// 说明
MCLabel description = new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.about.description"));
description.setPadding(new Insets(0, 140, 16, 140));
description.setWrapText(true);
// 关闭
close = new MCButton(TimiFXUI.MULTILINGUAL.text("close"));
close.setPrefWidth(ComponentSize.SMALL);
setMargin(close, new Insets(0, 0, 30, 0));
setAlignment(close, Pos.CENTER);
setAlignment(title, Pos.CENTER);
setPadding(new Insets(64, 0, 0, 0));
setTop(title);
setCenter(new VBox() {{
setSpacing(12);
setPadding(new Insets(16));
getChildren().addAll(new TimiVersionLabel(ForeverMC.VERSION, ForeverMC.class.getSimpleName()), description, new GridPane() {{
getColumnConstraints().add(Column.build(HPos.RIGHT));
setHgap(6);
setVgap(6);
setAlignment(Pos.CENTER);
int row = 0;
addRow(row++, new MCLabel() {{
setText(TimiFXUI.MULTILINGUAL.text("source"));
setTextFill(Colorful.LIGHT_GRAY);
}}, new MCLink("https://git.imyeyu.net/Timi/ForeverMC"));
addRow(row++, new MCLabel() {{
setText(TimiFXUI.MULTILINGUAL.text("developer"));
setTextFill(Colorful.LIGHT_GRAY);
}}, new MCLabel("夜雨"));
addRow(row, new MCLabel() {{
setText(TimiFXUI.MULTILINGUAL.text("blog"));
setTextFill(Colorful.LIGHT_GRAY);
}}, new MCLink("https://www.imyeyu.net"));
}});
}});
setBottom(close);
}
}

View File

@ -0,0 +1,87 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.view.components.AbstractPane;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCCheckBox;
import cn.forevermc.launcher.view.components.control.MCLabel;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import lombok.Getter;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BgImage;
/**
* 画板进度页面
*
* @author 夜雨
* @since 2023-06-15 17:49
*/
public abstract class CanvasProgressPane extends AbstractPane {
protected static final int SIZE = 128;
@Getter
protected final MCLabel label, subLabel;
protected final Canvas canvas;
protected final MCButton cancel;
protected final MCCheckBox bgmToggle;
protected final GraphicsContext g;
public CanvasProgressPane() {
// 主提示
label = new MCLabel();
// 副提示
subLabel = new MCLabel();
// 图形进度
canvas = new Canvas();
canvas.setWidth(SIZE);
canvas.setHeight(SIZE);
g = canvas.getGraphicsContext2D();
// 取消
cancel = new MCButton(ComponentSize.LARGE, TimiFXUI.MULTILINGUAL.text("cancel"));
// 背景音乐
bgmToggle = new MCCheckBox();
bgmToggle.setSelected(true);
setCenter(new BorderPane() {{
setAlignment(canvas, Pos.CENTER);
setMargin(canvas, new Insets(8, 0, 12, 0));
setCenter(new VBox() {{
setSpacing(8);
setAlignment(Pos.CENTER);
getChildren().addAll(label, subLabel);
}});
setBottom(new BorderPane() {{
setCenter(canvas);
setBottom(new HBox() {{
setSpacing(4);
setAlignment(Pos.CENTER);
visibleProperty().bind(bgmToggle.visibleProperty());
managedProperty().bind(visibleProperty());
getChildren().addAll(bgmToggle, new MCLabel() {{
bgmToggle.setLabel(this);
setText(TimiFXUI.MULTILINGUAL.text("bgm"));
}});
}});
}});
}});
setBottom(new HBox() {{
setSpacing(16);
setPadding(new Insets(16, 16, 64, 16));
setAlignment(Pos.CENTER);
getChildren().setAll(cancel);
}});
}
}

View File

@ -0,0 +1,93 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.view.components.AbstractPane;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCLabel;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import lombok.Getter;
import com.imyeyu.fx.ui.TimiFXUI;
/**
* 会话面板
*
* @author 夜雨
* @since 2022-11-28 17:32
*/
public abstract class DialogPane extends AbstractPane {
/** 标题 */
@Getter
protected final MCLabel title;
/** 内容 */
@Getter
protected final MCLabel content;
protected HBox buttonPane;
protected MCButton confirm, deny, cancel, close;
protected ObjectProperty<Node> graphic;
private final StackPane graphicPane;
public DialogPane() {
graphic = new SimpleObjectProperty<>();
// 标题
title = new MCLabel();
title.setTextFill(Colorful.LIGHT_GRAY);
// 内容
content = new MCLabel("content");
content.setPadding(new Insets(0, 8, 0, 8));
// 自定义组件
graphicPane = new StackPane();
graphic.addListener((obs, o, newGraphic) -> {
if (newGraphic == null) {
graphicPane.getChildren().clear();
} else {
graphicPane.getChildren().setAll(newGraphic);
}
});
// 确认
confirm = new MCButton(TimiFXUI.MULTILINGUAL.text("confirm"));
confirm.setPrefWidth(ComponentSize.SMALL);
confirm.managedProperty().bind(confirm.visibleProperty());
// 拒绝
deny = new MCButton(TimiFXUI.MULTILINGUAL.text("no"));
deny.setPrefWidth(ComponentSize.SMALL);
deny.managedProperty().bind(deny.visibleProperty());
// 取消
cancel = new MCButton(TimiFXUI.MULTILINGUAL.text("cancel"));
cancel.setPrefWidth(ComponentSize.SMALL);
cancel.managedProperty().bind(cancel.visibleProperty());
// 关闭
close = new MCButton(TimiFXUI.MULTILINGUAL.text("close"));
close.setPrefWidth(ComponentSize.SMALL);
close.managedProperty().bind(close.visibleProperty());
setCenter(new VBox() {{
setSpacing(8);
setAlignment(Pos.CENTER);
getChildren().addAll(title, content, graphicPane);
}});
setBottom(buttonPane = new HBox() {{
setSpacing(16);
setPadding(new Insets(16, 16, 64, 16));
setAlignment(Pos.CENTER);
getChildren().setAll(confirm, deny, cancel, close);
}});
}
}

View File

@ -0,0 +1,86 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.view.components.AbstractContentPane;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCSelector;
import cn.forevermc.launcher.view.components.control.MCTextField;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import com.imyeyu.fx.ui.TimiFXUI;
/**
* 版本配置面板
*
* @author 夜雨
* @since 2022-12-10 00:49
*/
public abstract class GameOptionPane extends AbstractContentPane {
protected final MCButton installFabric, javaSelect, finish;
protected final MCTextField java, server;
protected final MCSelector<Boolean> autoConnect;
protected final MCSelector<Game.Option.LoginType> loginType;
public GameOptionPane() {
// Java
java = new MCTextField();
java.setLabel("Java: ");
java.setFocusTraversable(false);
javaSelect = new MCButton(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.java.select"));
// 安装 Fabric
installFabric = new MCButton(ComponentSize.MEDIUM, TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.fabric.install"));
// 自动连接
autoConnect = new MCSelector<>();
autoConnect.setPrefWidth(ComponentSize.MEDIUM);
autoConnect.getItems().addAll(true, false);
autoConnect.setStringConverter(isSelected -> {
if (isSelected) {
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.auto_connect", TimiFXUI.MULTILINGUAL.text("turn.on"));
}
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.auto_connect", TimiFXUI.MULTILINGUAL.text("turn.off"));
});
// 服务器地址
server = new MCTextField();
server.setPrefWidth(ComponentSize.MEDIUM);
server.setLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.server"));
server.disableProperty().bind(autoConnect.valueProperty().isEqualTo(false));
// 登录方式
loginType = new MCSelector<>();
loginType.setPrefWidth(ComponentSize.MEDIUM);
loginType.setStringConverter(type -> {
if (type == null) {
return "";
}
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.pack_option.login_type", type.getName());
});
loginType.getItems().setAll(Game.Option.LoginType.values());
// 完成
finish = new MCButton(TimiFXUI.MULTILINGUAL.text("finish"));
finish.setPrefWidth(ComponentSize.LARGE);
setContentPane(new GridPane() {{
setHgap(24);
setVgap(12);
setAlignment(Pos.TOP_CENTER);
int row = 0;
add(new BorderPane() {{
setMargin(java, new Insets(0, 10, 0, 0));
setCenter(java);
setRight(javaSelect);
}}, 0, row++, 2, 1);
addRow(row++, installFabric, loginType);
addRow(row, autoConnect, server);
}});
setBottomPaneValue(finish);
}
}

View File

@ -0,0 +1,104 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.bean.LaunchGame;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.view.components.AbstractListPane;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCLabel;
import javafx.geometry.Insets;
import javafx.scene.control.ListCell;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.java.TimiJava;
/**
* 启动版本选择面板
*
* @author 夜雨
* @since 2022-11-28 10:09
*/
public abstract class GameSelectPane extends AbstractListPane<Game> {
@Inject
protected LaunchGame launchGame;
protected final MCButton originalDL, packDL, select, destory, option, back;
public GameSelectPane() {
// 列表
list.setCellFactory(cell -> new ListCell<>() {
final MCLabel name;
final MCLabel title;
final ImageView activatedIcon;
final BorderPane root;
{
activatedIcon = new ImageView(Resources.CHECK_SMALL);
name = new MCLabel();
title = new MCLabel();
title.setTextFill(Colorful.LIGHT_GRAY);
title.getStyleClass().clear();
title.visibleProperty().bind(title.textProperty().isNotEmpty());
title.managedProperty().bind(title.visibleProperty());
root = new BorderPane();
root.setLeft(activatedIcon);
root.setCenter(new HBox() {{
setPadding(new Insets(0, 0, 0, 4));
getChildren().addAll(name, title);
}});
}
@Override
protected void updateItem(Game item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
if (activatedIcon.visibleProperty().isBound()) {
activatedIcon.visibleProperty().unbind();
}
activatedIcon.visibleProperty().bind(itemProperty().isEqualTo(launchGame));
name.setText(item.getName());
if (TimiJava.isEmpty(item.getOption().getName().get())) {
title.setText("");
} else {
title.setText(" - " + item.getOption().getName().get());
}
setGraphic(root);
}
}
});
// 原版下载
originalDL = new MCButton(ComponentSize.MEDIUM + 10, TimiFXUI.MULTILINGUAL.text("fmc.launcher.launch_select.download.mojang"));
// 整合下载
packDL = new MCButton(ComponentSize.MEDIUM + 10, TimiFXUI.MULTILINGUAL.text("fmc.launcher.launch_select.download.fmc"));
// 选择
select = new MCButton(ComponentSize.SMALL, TimiFXUI.MULTILINGUAL.text("select"));
select.disableProperty().bind(loadingProperty().or(list.getSelectionModel().selectedItemProperty().isNull()));
// 销毁
destory = new MCButton(ComponentSize.SMALL, TimiFXUI.MULTILINGUAL.text("delete"));
destory.disableProperty().bind(loadingProperty().or(list.getSelectionModel().selectedItemProperty().isNull()));
// 设置
option = new MCButton(ComponentSize.SMALL, TimiFXUI.MULTILINGUAL.text("fmc.launcher.launch_select.option"));
option.disableProperty().bind(loadingProperty().or(list.getSelectionModel().selectedItemProperty().isNull()));
// 返回
back = new MCButton(ComponentSize.SMALL, TimiFXUI.MULTILINGUAL.text("back"));
setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.launch_select"));
addBottomPaneValue(originalDL, 0, 0, 2, 1);
addBottomPaneValue(packDL, 2, 0, 2, 1);
addBottomPaneValue(1, destory, option, select, back);
}
}

View File

@ -0,0 +1,47 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.view.components.AbstractListPane;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCLabel;
import javafx.geometry.HPos;
import javafx.scene.layout.GridPane;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.ui.components.EnumListCell;
import com.imyeyu.java.bean.Language;
/**
* 语言选择面板
*
*
* @author 夜雨
* @since 2022-12-02 00:09
*/
public abstract class LangSelectPane extends AbstractListPane<Language> {
protected final MCButton confirm, cancel;
public LangSelectPane() {
setLoading(false);
// 列表
list.setCellFactory(cell -> new EnumListCell<>());
// 提示
MCLabel tips = new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.lang_Select.tips"));
tips.setTextFill(Colorful.LIGHT_GRAY);
// 确认
confirm = new MCButton(TimiFXUI.MULTILINGUAL.text("confirm"));
confirm.setPrefWidth(ComponentSize.MEDIUM);
// 取消
cancel = new MCButton(TimiFXUI.MULTILINGUAL.text("cancel"));
cancel.setPrefWidth(ComponentSize.MEDIUM);
GridPane.setHalignment(tips, HPos.CENTER);
setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.lang_select"));
addBottomPaneValue(tips, 0, 0, 2, 1);
addBottomPaneValue(1, confirm, cancel);
}
}

View File

@ -0,0 +1,27 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.view.components.AbstractPane;
import javafx.scene.image.ImageView;
import javafx.scene.layout.StackPane;
/**
* 启动中过渡面板
*
* @author 夜雨
* @since 2022-11-28 21:19
*/
public abstract class LaunchingPane extends AbstractPane {
protected StackPane root;
protected ImageView mojang;
public LaunchingPane() {
mojang = new ImageView();
mojang.setOpacity(0);
root = new StackPane(mojang);
root.setOpacity(0);
setCenter(root);
}
}

View File

@ -0,0 +1,192 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.ForeverMC;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.view.components.AbstractPane;
import cn.forevermc.launcher.view.components.LoginTypeSelector;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCLabel;
import cn.forevermc.launcher.view.components.control.MCPasswordField;
import cn.forevermc.launcher.view.components.control.MCTextField;
import javafx.animation.FadeTransition;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import javafx.scene.canvas.Canvas;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.util.Duration;
import com.imyeyu.fx.icon.TimiFXIcon;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BgFill;
import com.imyeyu.fx.utils.Row;
/**
* 菜单面板
*
* @author 夜雨
* @since 2022-11-21 19:33
*/
public abstract class MenuPane extends AbstractPane {
protected final Canvas splashCanvas;
protected final MCLabel launchGameLabel;
protected final MCButton fmcAccount, fmcServer, launchGameSelect, launch, option, exit, langSelect, about;
protected final ImageView title;
protected final MCTextField name;
protected final StringProperty splash;
protected final MCPasswordField password;
protected final LoginTypeSelector loginTypeSelector;
private final StackPane root, transitionPane;
protected boolean isFirstShow = true;
public MenuPane() {
splash = new SimpleStringProperty();
// 过渡面板
transitionPane = new StackPane(new ImageView(Resources.SPLASH_SCREEN));
transitionPane.setBackground(new BgFill("#EF323D").build());
// 标题
title = new ImageView(Resources.TITLE);
// 闪烁标语,使用 Canvas 减少动画导致抖动
splashCanvas = new Canvas(1, 32);
splashCanvas.setTranslateX(180);
splashCanvas.setTranslateY(20);
splashCanvas.setRotate(-20);
// 账号管理
fmcAccount = new MCButton();
fmcAccount.getLabel().setEffect(null);
fmcAccount.getLabel().setGraphic(new ImageView(Resources.TOTEM));
fmcAccount.getLabel().setPadding(new Insets(1));
// 游戏名
name = new MCTextField() {{
paddingProperty().unbind();
paddingProperty().bind(Bindings.createObjectBinding(() -> new Insets(8, 28, 8, label.getWidth() + 8), label.widthProperty()));
}};
name.setLabel(TimiFXUI.MULTILINGUAL.text("name"));
// 登录方式
loginTypeSelector = new LoginTypeSelector();
// 启动版本
launchGameLabel = new MCLabel();
launchGameLabel.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
launchGameLabel.setPadding(new Insets(4));
// 密码
password = new MCPasswordField();
password.setLabel(TimiFXUI.MULTILINGUAL.text("password"));
password.disableProperty().bind(loginTypeSelector.valueProperty().isEqualTo(Game.Option.LoginType.OFFLINE));
// 启动版本
launchGameSelect = new MCButton();
launchGameSelect.getLabel().setEffect(null);
launchGameSelect.getLabel().setGraphic(TimiFXIcon.fromName("LIST", 32, Colorful.WHITE));
launchGameSelect.getLabel().setPadding(Insets.EMPTY);
launchGameSelect.getLabel().setPadding(new Insets(1, 2, 1, 2));
// 服务器
fmcServer = new MCButton();
fmcServer.getLabel().setEffect(null);
fmcServer.getLabel().setGraphic(new ImageView(Resources.COMMAND_BLOCK));
fmcServer.getLabel().setPadding(new Insets(1));
// 启动
launch = new MCButton(TimiFXUI.MULTILINGUAL.text("launch"));
launch.prefWidthProperty().bind(new SimpleDoubleProperty(ComponentSize.NORMAL).subtract(launchGameSelect.widthProperty()));
// 语言
langSelect = new MCButton();
langSelect.getLabel().setGraphic(new ImageView(Resources.LANG));
langSelect.getLabel().setPadding(new Insets(4));
// 关于
about = new MCButton();
about.getLabel().setGraphic(new ImageView(Resources.INFO));
about.getLabel().setPadding(new Insets(9, 15, 9, 15));
// 设置
option = new MCButton(TimiFXUI.MULTILINGUAL.text("option"));
option.setPrefWidth(ComponentSize.NORMAL);
// 退出
exit = new MCButton(TimiFXUI.MULTILINGUAL.text("exit"));
exit.setPrefWidth(ComponentSize.NORMAL);
setCenter(root = new StackPane() {{
getChildren().addAll(new BorderPane() {{
setTop(new StackPane() {{
setPadding(new Insets(64, 0, 0, 0));
getChildren().addAll(title, splashCanvas);
}});
setCenter(new GridPane() {{
// 纵向排版
getRowConstraints().addAll(Row.build(), Row.build().top().height(48), Row.build());
setHgap(7);
setVgap(7);
setAlignment(Pos.BOTTOM_CENTER);
int col = 0;
add(fmcAccount, col, 0);
add(langSelect, col, 2);
col++;
addColumn(col, new StackPane() {{
setMargin(loginTypeSelector, new Insets(0, 8, 0, 0));
setAlignment(loginTypeSelector, Pos.CENTER_RIGHT);
getChildren().addAll(name, loginTypeSelector);
}}, password, option);
col++;
addColumn(col, new StackPane() {{
setBackground(new BgFill("#000A").build());
getChildren().setAll(launchGameLabel);
}}, new BorderPane() {{
setAlignment(launch, Pos.TOP_CENTER);
setLeft(launchGameSelect);
setCenter(launch);
}}, exit);
col++;
add(fmcServer, col, 0);
add(about, col, 2);
}});
setBottom(new MCLabel() {{
setMargin(this, new Insets(16, 4, 2, 4));
setText("ForeverMC " + ForeverMC.VERSION);
}});
}}, transitionPane);
}});
}
@Override
public void onShow() {
if (isFirstShow) {
FadeTransition welcomeFT = new FadeTransition(Duration.millis(650));
welcomeFT.setNode(transitionPane);
welcomeFT.setFromValue(1);
welcomeFT.setToValue(0);
welcomeFT.setDelay(Duration.millis(2000));
welcomeFT.setOnFinished(e -> {
root.getChildren().remove(transitionPane);
isFirstShow = false;
});
welcomeFT.play();
}
}
}

View File

@ -0,0 +1,128 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.bean.GameDownloadSource;
import cn.forevermc.launcher.bean.RuntimeDownloadSource;
import cn.forevermc.launcher.view.components.AbstractScrollPane;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCLabel;
import cn.forevermc.launcher.view.components.control.MCSelector;
import cn.forevermc.launcher.view.components.control.MCSlider;
import cn.forevermc.launcher.view.components.control.MCTextField;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.GridPane;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.Column;
import com.imyeyu.utils.OS;
/**
* 选项面板
*
* @author 夜雨
* @since 2022-11-24 22:27
*/
public abstract class OptionPane extends AbstractScrollPane {
protected final MCButton launchSelect, finish;
protected final MCSlider memory, bgmVolume, soundVolume, multiDownload;
protected final MCTextField proxyHost, proxyPort;
protected final MCSelector<Boolean> autoStartup;
protected final MCSelector<GameDownloadSource> gameSource;
protected final MCSelector<RuntimeDownloadSource> runtimeSource;
public OptionPane() {
// 内存
memory = new MCSlider();
memory.setMin(1024);
memory.setMax(OS.getSystemMemorySize() / Math.pow(1024, 2));
memory.setStep(512);
memory.setPrefWidth(ComponentSize.MEDIUM);
// 版本
launchSelect = new MCButton(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.launch_pack"));
launchSelect.setPrefWidth(ComponentSize.MEDIUM);
// 自动启动
autoStartup = new MCSelector<>();
autoStartup.setPrefWidth(ComponentSize.MEDIUM);
autoStartup.getItems().addAll(true, false);
autoStartup.setStringConverter(isSelected -> {
if (isSelected) {
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.auto_launch", TimiFXUI.MULTILINGUAL.text("turn.on"));
}
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.auto_launch", TimiFXUI.MULTILINGUAL.text("turn.off"));
});
// 背景音量
bgmVolume = new MCSlider();
bgmVolume.setPrefWidth(ComponentSize.MEDIUM);
bgmVolume.getLabel().textProperty().bind(bgmVolume.valueProperty().asString(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.bgm")));
// 音效音量
soundVolume = new MCSlider();
soundVolume.setPrefWidth(ComponentSize.MEDIUM);
soundVolume.getLabel().textProperty().bind(soundVolume.valueProperty().asString(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.sound")));
MCLabel network = new MCLabel(TimiFXUI.MULTILINGUAL.text("network"));
// 游戏下载源
gameSource = new MCSelector<>();
gameSource.setPrefWidth(ComponentSize.MEDIUM);
gameSource.setStringConverter(type -> {
if (type == null) {
return "";
}
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.game_source", type.toString());
});
gameSource.getItems().setAll(GameDownloadSource.values());
// 运行时下载源
runtimeSource = new MCSelector<>();
runtimeSource.setPrefWidth(ComponentSize.MEDIUM);
runtimeSource.setStringConverter(type -> {
if (type == null) {
return "";
}
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.runtime_source", type.getName());
});
runtimeSource.getItems().setAll(RuntimeDownloadSource.values());
// 多线程下载
multiDownload = new MCSlider();
multiDownload.setMin(0);
multiDownload.setMax(64);
multiDownload.setStep(4);
multiDownload.setPrefWidth(ComponentSize.MEDIUM);
proxyHost = new MCTextField();
proxyHost.setLabel(TimiFXUI.MULTILINGUAL.text("proxy.host"));
proxyPort = new MCTextField();
proxyPort.setLabel(TimiFXUI.MULTILINGUAL.text("proxy.port"));
// 完成
finish = new MCButton(TimiFXUI.MULTILINGUAL.text("finish"));
finish.setPrefWidth(ComponentSize.LARGE);
setTitle(TimiFXUI.MULTILINGUAL.text("option"));
setScrollContent(new GridPane() {{
setMargin(network, new Insets(16, 0, 0, 0));
getColumnConstraints().addAll(Column.build(HPos.CENTER), Column.build(HPos.CENTER));
setHgap(20);
setVgap(12);
setAlignment(Pos.TOP_CENTER);
int row = 0;
add(memory, 0, row++, 2, 1);
addRow(row++, autoStartup, launchSelect);
addRow(row++, soundVolume, bgmVolume);
add(network, 0, row++, 2, 1);
addRow(row++, runtimeSource, gameSource);
addRow(row++, proxyHost, proxyPort);
addRow(row, multiDownload);
}});
setBottomPaneValue(finish);
}
}

View File

@ -0,0 +1,65 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.bean.Game;
import cn.forevermc.launcher.view.components.AbstractListPane;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCLabel;
import javafx.geometry.Pos;
import javafx.scene.control.ListCell;
import javafx.scene.layout.BorderPane;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.utils.Time;
/**
* 原版选择面板
*
* @author 夜雨
* @since 2022-11-28 10:09
*/
public abstract class OriginalSelectPane extends AbstractListPane<Game> {
protected final MCButton download, back;
public OriginalSelectPane() {
// 列表
list.setCellFactory(cell -> new ListCell<>() {
private final MCLabel version, date;
private final BorderPane root;
{
version = new MCLabel();
date = new MCLabel();
root = new BorderPane();
BorderPane.setAlignment(version, Pos.CENTER_LEFT);
root.setCenter(version);
root.setRight(date);
}
@Override
protected void updateItem(Game item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
version.setText(item.getId());
date.setText(Time.toDateTime(item.getReleaseAt()));
setGraphic(root);
}
}
});
// 下载
download = new MCButton(TimiFXUI.MULTILINGUAL.text("download"));
download.setPrefWidth(ComponentSize.MEDIUM);
download.disableProperty().bind(list.getSelectionModel().selectedItemProperty().isNull());
// 返回
back = new MCButton(TimiFXUI.MULTILINGUAL.text("back"));
back.setPrefWidth(ComponentSize.MEDIUM);
setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.download.mojang.title"));
setBottomPaneValue(download, back);
}
}

View File

@ -0,0 +1,224 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.bean.MCPopupTips;
import cn.forevermc.launcher.bean.Pack;
import cn.forevermc.launcher.service.MCPopupTipsService;
import cn.forevermc.launcher.util.Resources;
import cn.forevermc.launcher.view.components.AbstractContentPane;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCLabel;
import cn.forevermc.launcher.view.components.control.MCList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ListCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BorderStroke;
import com.imyeyu.inject.annotation.Component;
import com.imyeyu.inject.annotation.Inject;
import com.imyeyu.inject.annotation.InvokeForInjected;
import com.imyeyu.io.IOSize;
import com.imyeyu.utils.Time;
/**
* 整合版下载页面
*
* @author 夜雨
* @since 2023-06-13 23:30
*/
public abstract class PackSelectPane extends AbstractContentPane {
@Inject
protected PackListPane packListPane;
@Inject
protected SourceListPane sourceListPane;
protected MCButton install, back;
public PackSelectPane() {
MCLabel titleList = new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.download.fmc.list"));
titleList.setPadding(new Insets(24, 0, 16, 8));
titleList.prefWidthProperty().bind(widthProperty().multiply(.618));
MCLabel titleUrl = new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.download.fmc.url"));
titleUrl.setPadding(new Insets(24, 0, 16, 0));
titleUrl.prefWidthProperty().bind(widthProperty().multiply(1 - .618).subtract(8));
// 安装
install = new MCButton(ComponentSize.MEDIUM, TimiFXUI.MULTILINGUAL.text("install"));
// 返回
back = new MCButton(ComponentSize.MEDIUM, TimiFXUI.MULTILINGUAL.text("back"));
content.setPadding(Insets.EMPTY);
setTop(new HBox(titleList, titleUrl));
setBottomPaneValue(install, back);
}
@InvokeForInjected
private void injected() {
install.disableProperty().bind(sourceListPane.getSelectionModel().selectedItemProperty().isNull());
packListPane.prefWidthProperty().bind(widthProperty().multiply(.618));
sourceListPane.prefWidthProperty().bind(widthProperty().multiply(1 - .618));
setContentPane(new BorderPane() {{
setCenter(new StackPane() {{
setBorder(new BorderStroke(Color.valueOf("#FFF4")).right(2).build());
getChildren().setAll(packListPane);
}});
setRight(sourceListPane);
}});
}
/**
* 整合版列表面板
*
* @author 夜雨
* @since 2023-06-15 10:17
*/
@Component
protected static class PackListPane extends MCList<Pack> {
@Inject
private MCPopupTipsService mcPopupTipsService;
public PackListPane() {
setCellFactory(cell -> new ListCell<>() {
final MCLabel gameVer, name, title, deprecated, time;
final BorderPane root;
final MCPopupTips tips;
{
// 提示
tips = new MCPopupTips();
tips.getNode().setMaxWidth(440);
tips.getNode().setWrapText(true);
tips.enableProperty().bind(tips.getNode().textProperty().isNotEmpty());
// 名称
name = new MCLabel();
// 标题
title = new MCLabel();
title.setTextFill(Colorful.LIGHT_GRAY);
title.getStyleClass().clear();
// 游戏版本
gameVer = new MCLabel();
// 过时提示
deprecated = new MCLabel(TimiFXUI.MULTILINGUAL.text("deprecated"));
deprecated.setTextFill(Colorful.LIGHT_GRAY);
// 更新时间
time = new MCLabel();
root = new BorderPane();
BorderPane.setAlignment(name, Pos.CENTER_LEFT);
root.setLeft(new BorderPane() {{
setPadding(new Insets(0, 4, 0, 0));
setLeft(new MCLabel("["));
setCenter(new StackPane() {{
setAlignment(Pos.CENTER_LEFT);
setPrefWidth(50);
getChildren().add(gameVer);
}});
setRight(new MCLabel("]"));
}});
root.setCenter(new HBox(4, name, title));
root.setRight(new HBox(8, deprecated, time));
mcPopupTipsService.install(this, tips);
}
@Override
protected void updateItem(Pack item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
tips.getNode().setText("");
deprecated.setVisible(false);
} else {
name.setText(item.getName());
title.setText(" - " + item.getTitle());
gameVer.setText(item.getGameVer());
time.setText(Time.toDate(item.getCreatedAt()));
// 提示内容
String tipsContent = item.getDescription();
if (item.isDeprecated()) {
setOpacity(.6);
deprecated.setVisible(true);
}
Object[] tipsParams = {
item.getName(),
item.getTitle(),
item.getGameVer(),
item.getVer(),
IOSize.format(item.getSize()),
Time.toDateTime(item.getCreatedAt()),
tipsContent
};
tips.getNode().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.fmc.tips", tipsParams));
setGraphic(root);
}
}
});
}
}
/**
* 下载源列表面板
*
* @author 夜雨
* @since 2023-06-15 10:17
*/
@Component
protected static class SourceListPane extends MCList<Pack.Source> {
private static final String ATTACH_DL_API = Resources.TIMI_SERVER_API + "/attachment/download";
@Inject
private MCPopupTipsService mcPopupTipsService;
public SourceListPane() {
setCellFactory(cell -> new ListCell<>() {
final MCPopupTips tips;
{
// 提示
tips = new MCPopupTips();
tips.enableProperty().bind(tips.getNode().textProperty().isNotEmpty());
mcPopupTipsService.install(this, tips);
}
@Override
protected void updateItem(Pack.Source item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText("");
setGraphic(null);
tips.getNode().setText("");
} else {
setText(item.getName());
String url = switch (item.getType()) {
case URL -> item.getData();
case ATTACH -> ATTACH_DL_API + "/%s".formatted(item.getData());
};
String domain = url.substring(0, url.indexOf("/", url.indexOf("//") + 2));
tips.getNode().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.fmc.url.tips", domain));
}
}
});
}
}
}

View File

@ -0,0 +1,64 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.bean.ComponentSize;
import cn.forevermc.launcher.service.FMCLoginService;
import cn.forevermc.launcher.view.components.AbstractListPane;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCLabel;
import javafx.scene.control.ListCell;
import javafx.scene.layout.VBox;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.utils.Time;
/**
* @author 夜雨
* @since 2024-05-21 19:40
*/
public abstract class PlayerSelectPane extends AbstractListPane<FMCLoginService.MinecraftPlayer> {
protected final MCButton confirm, cancel;
public PlayerSelectPane() {
// 列表
list.setCellFactory(cell -> new ListCell<>() {
private final MCLabel name, lastLoginAt;
private final VBox root;
{
name = new MCLabel();
lastLoginAt = new MCLabel();
lastLoginAt.getStyleClass().add("last-login-at");
root = new VBox(4);
root.getChildren().addAll(name, lastLoginAt);
}
@Override
protected void updateItem(FMCLoginService.MinecraftPlayer item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
name.setText(item.getName());
if (item.getLastLoginAt() != null) {
lastLoginAt.setText("最近登录: " + Time.toDateTime(item.getLastLoginAt()));
}
setGraphic(root);
}
}
});
list.getStyleClass().add("player");
// 下载
confirm = new MCButton(TimiFXUI.MULTILINGUAL.text("confirm"));
confirm.setPrefWidth(ComponentSize.MEDIUM);
confirm.disableProperty().bind(list.getSelectionModel().selectedItemProperty().isNull());
// 返回
cancel = new MCButton(TimiFXUI.MULTILINGUAL.text("cancel"));
cancel.setPrefWidth(ComponentSize.MEDIUM);
setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.player.select.title"));
setBottomPaneValue(confirm, cancel);
}
}

View File

@ -0,0 +1,125 @@
package cn.forevermc.launcher.view.pages;
import cn.forevermc.launcher.bean.FileDownload;
import cn.forevermc.launcher.view.components.AbstractListPane;
import cn.forevermc.launcher.view.components.control.MCButton;
import cn.forevermc.launcher.view.components.control.MCCheckBox;
import cn.forevermc.launcher.view.components.control.MCLabel;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ListCell;
import javafx.scene.control.OverrunStyle;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.text.TextAlignment;
import com.imyeyu.fx.ui.TimiFXUI;
import com.imyeyu.fx.utils.BorderStroke;
import com.imyeyu.fx.utils.Column;
import com.imyeyu.fx.utils.NoSelectionModel;
/**
* 资源下载面板
*
* @author 夜雨
* @since 2022-11-28 19:18
*/
public abstract class ResourceDownloadPane extends AbstractListPane<FileDownload> {
protected final MCLabel speed;
protected final MCButton cancel;
protected final MCCheckBox bgmToggle;
protected final ProgressBar pb;
public ResourceDownloadPane() {
// 列表
list.setSelectionModel(new NoSelectionModel<>());
list.setCellFactory(cell -> new ListCell<>() {
private final MCLabel text;
private final BorderPane root;
private final ProgressBar pb;
{
text = new MCLabel();
text.setAlignment(Pos.CENTER_RIGHT);
text.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
text.setTextAlignment(TextAlignment.RIGHT);
pb = new ProgressBar();
pb.setPrefHeight(20);
root = new BorderPane();
root.setCenter(text);
root.setRight(pb);
text.maxWidthProperty().bind(root.widthProperty().subtract(pb.widthProperty()).subtract(40));
}
@Override
protected void updateItem(FileDownload item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
text.setText(item.getDisplayName());
if (text.textFillProperty().isBound()) {
text.textFillProperty().unbind();
}
text.textFillProperty().bind(Bindings.when(item.errorProperty()).then(Colorful.RED).otherwise(Colorful.WHITE));
if (pb.progressProperty().isBound()) {
pb.progressProperty().unbind();
}
pb.progressProperty().bind(item.progressProperty());
setGraphic(root);
}
}
});
// 总进度
MCLabel labelPB = new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.resource_download.total"));
labelPB.setTextFill(Colorful.WHITE);
speed = new MCLabel();
pb = new ProgressBar();
pb.setMaxWidth(Double.MAX_VALUE);
pb.setPrefHeight(28);
// 取消
cancel = new MCButton(TimiFXUI.MULTILINGUAL.text("cancel"));
cancel.getLabel().setPadding(new Insets(8, 24, 8, 24));
// 背景音乐
bgmToggle = new MCCheckBox();
bgmToggle.setSelected(true);
bgmToggle.managedProperty().bind(bgmToggle.visibleProperty());
setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.resource_download"));
setBottomPaneValue(new BorderPane() {{
setMargin(cancel, new Insets(0, 0, 0, 16));
setAlignment(labelPB, Pos.CENTER_RIGHT);
prefWidthProperty().bind(bottom.widthProperty());
setLeft(labelPB);
setCenter(new StackPane(pb, speed));
setRight(new HBox() {{
setMargin(bgmToggle, new Insets(0, 0, 0, 16));
setSpacing(4);
setPadding(new Insets(0, 0, 0, 12));
setAlignment(Pos.CENTER);
getChildren().addAll(cancel, new HBox() {{
setSpacing(4);
setAlignment(Pos.CENTER);
visibleProperty().bind(bgmToggle.visibleProperty());
managedProperty().bind(visibleProperty());
getChildren().addAll(bgmToggle, new MCLabel() {{
bgmToggle.setLabel(this);
setText(TimiFXUI.MULTILINGUAL.text("bgm"));
}});
}});
}});
}});
}
}

View File

@ -0,0 +1,17 @@
main:
language: zh_CN
bgmVolume: 32
soundVolume: 100
gameDownloadSource: MOJANG
runtimeDownloadSource: TUNA
player:
name:
password:
launcher:
memory: 2048
game:
clientId:
autoStartup: false
multiDownload: 32

View File

@ -0,0 +1,5 @@
name:
loginType: OFFLINE
java:
server:
autoConnect:

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
src/main/resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

View File

@ -0,0 +1,93 @@
back=zurück
bgm=Hintergrundmusik
blog=Persönlicher Blog
delete.error=Fehler löschen
deprecated=veraltet
developer=Entwickler
download=Download
downloading=Herunterladen
exit=Beenden
fmc.launcher.about.description=\tForever MC ist ein völlig kostenloser Open-Source-Minecraft-Launcher, und alle verwendeten Ressourcen sind urheberrechtlich geschützt von Mojang AB. Das Programm ist nur für Entwicklung, Lernen oder Referenz und ist von kommerziellen Aktivitäten verboten.
fmc.launcher.account.checking=Konto verifizieren
fmc.launcher.account.not_support=Anmeldemethode wird in dieser Version nicht unterstützt: {0}
fmc.launcher.download.fmc.downloading=Versionspaket herunterladen: {0}
fmc.launcher.download.fmc.error=Erfassungsfehler der integrierten Versionsliste
fmc.launcher.download.fmc.exist_warning=Die integrierte Version {0} existiert bereits. Bitte starten Sie sie direkt oder entfernen Sie sie vor der Installation.
fmc.launcher.download.fmc.list=Integrierte Versionsliste:
fmc.launcher.download.fmc.succeed=Installation von {0} erfolgreich
fmc.launcher.download.fmc.tips=ID: {0} \ n Name: {1} \ n Spielversion: {2} \ n Integrierte Version: {3} \ n Dateigröße: {4} \ n Veröffentlichungszeit: {5} \ n \ n {6}
fmc.launcher.download.fmc.url=Download Quelle:
fmc.launcher.download.fmc.url.tips=Quelle: {0}
fmc.launcher.download.mojang.api_error=Laden der Liste fehlgeschlagen, offizielle Schnittstelle Ausnahme
fmc.launcher.download.mojang.download_error=Download fehlgeschlagen: {0}
fmc.launcher.download.mojang.title=Offizielle Originalliste
fmc.launcher.empty_game=Bitte wählen Sie die Startversion aus
fmc.launcher.empty_name=Bitte geben Sie den Spielnamen ein
fmc.launcher.empty_password=Bitte Passwort eingeben
fmc.launcher.java.error=Java-Download fehlgeschlagen, bitte versuchen Sie es erneut oder wählen Sie manuell verfügbare Java
fmc.launcher.lang_Select.tips=(Sprachübersetzung kann nicht 100% genau sein)
fmc.launcher.lang_select=Sprache
fmc.launcher.launch_select=Versionsauswahl starten
fmc.launcher.launch_select.delete.confirm=Bestätigen ({0})
fmc.launcher.launch_select.delete.tips=Möchtest du {0} wirklich löschen? Dies wird dauerhaft alle Archive und Konfigurationen dieser Version verlieren, und dieser Vorgang wird irreparabel sein!
fmc.launcher.launch_select.delete.working=Löschen {0}
fmc.launcher.launch_select.download.fmc=Integrierte Version herunterladen
fmc.launcher.launch_select.download.mojang=Offizieller Originaldownload
fmc.launcher.launch_select.empty=Keine verfügbare Version, bitte wählen Sie offiziellen Download oder integrierte Version Download
fmc.launcher.launch_select.option=Versionseinstellungen
fmc.launcher.launching=Start
fmc.launcher.menu.account=Gehen Sie zur Registrierung oder Verwaltung von Spielkonten: \nForeverMC verwendet das einheitliche Kontoverwaltungssystem von imyeyu.net
fmc.launcher.menu.account.fmc=ForeverMC Unified Account Authentication
fmc.launcher.menu.account.mojang=Echte Mojang-Zertifizierung
fmc.launcher.menu.account.type=Prüfmethode: {0}
fmc.launcher.menu.launch.auto=Automatischer Start nach {0} Sekunden
fmc.launcher.menu.launch.auto.cancel=Automatischer Start abbrechen
fmc.launcher.menu.launch.pack=Version: {0}
fmc.launcher.menu.launch.pack.list=Versionsverwaltung starten
fmc.launcher.menu.launch.pack.null=Keine Startversion ausgewählt
fmc.launcher.option.auto_connect=Automatische Verbindung zum Server herstellen: {0}
fmc.launcher.option.auto_launch=Autostart: {0}
fmc.launcher.option.bgm=Lautstärke der Hintergrundmusik des Launchers: %.0f%%
fmc.launcher.option.game_source=Download source: {0}
fmc.launcher.option.java.select=Wählen
fmc.launcher.option.java.select_title=Wählen Sie Java
fmc.launcher.option.launch_pack=Startversion
fmc.launcher.option.memory=Maximaler Arbeitsspeicher: {0} MB
fmc.launcher.option.multi_download=Download-Ressource mit mehreren Threads: {0}
fmc.launcher.option.runtime_source=Quelle zur Laufzeit herunterladen: {0}
fmc.launcher.option.server=Serveradresse:
fmc.launcher.option.sound=Lautstärke des Launcher-Soundeffekts: %.0f%%
fmc.launcher.pack_option.auto_connect.tips=Wenn das Spiel nach dem Start abstürzt, kannst du versuchen, diese Option auszuschalten
fmc.launcher.pack_option.fabric.install=Fabric installieren
fmc.launcher.pack_option.fabric.installing=Fabric installieren
fmc.launcher.pack_option.fabric.not_support=Installation fehlgeschlagen, es gibt keine geeignete Version von Fabric für diese Version
fmc.launcher.pack_option.fabric.not_support_api=Installation fehlgeschlagen, kein passender Fabric API Loader verfügbar
fmc.launcher.pack_option.login_type=Anmeldeüberprüfungsmethode: {0}
fmc.launcher.pack_option.login_type.tips=Wenn Sie sich nicht sicher sind, welchen Zweck diese Einstellung hat, ändern Sie sie bitte nicht.
fmc.launcher.pack_option.title={0} Versionskonfiguration
fmc.launcher.player.select.title=Wählen Sie, um ein Konto zu eröffnen
fmc.launcher.resource_download=Herunterladen notwendiger Dateien, bitte haben Sie Geduld
fmc.launcher.resource_download.total=Gesamte Fortschritte:
fmc.launcher.resource_error=Verbindung zur offiziellen Minecraft-Ressourcenstation kann nicht hergestellt werden
fmc.launcher.rt.download.tips=Herunterladen von Java von {0}
fmc.launcher.timeout=Startzeitüberschreitung oder Unfähigkeit, gestartete Spiele zu erkennen
install=installieren
install.fail=Installation fehlgeschlagen
launch=@startup
network=Netzwerk
offline=offline
option=Option
password=Passwort
proxy.host=Proxy-Adresse
proxy.port=Proxy-Port
select=Wahl
select.empty=Keine Optionen verfügbar
server.status=Serverstatus
source=Quellcode
startup=Inbetriebnahme
tips.restart=Die Anwendung muss neu gestartet werden. Möchten Sie sie jetzt neu starten?
tips.restart.error=Selbstneustart fehlgeschlagen, bitte versuchen Sie den manuellen Neustart
turn.off=schließen
turn.on=offen
version.checking={0} Auf Aktualisierungen prüfen
version.new={0}, neue Version: {1}

View File

@ -0,0 +1,93 @@
back=Back
bgm=BGM
blog=Personal blog
delete.error=Delete Error
deprecated=obsolete
developer=developer
download=Download
downloading=Downloading
exit=Exit
fmc.launcher.about.description=\tForever MC is a completely free and open-source Minecraft launcher, and all resource materials used are copyrighted by Mojang AB. The program is only for development, learning, or reference use and is prohibited from any commercial activities.
fmc.launcher.account.checking=Verifying account
fmc.launcher.account.not_support=Login method not supported in this version: {0}
fmc.launcher.download.fmc.downloading=Downloading game package: {0}
fmc.launcher.download.fmc.error=Integrated version list acquisition error
fmc.launcher.download.fmc.exist_warning=Integrated version {0} already exists. Please start it directly or remove it before installing it
fmc.launcher.download.fmc.list=Integrated version list:
fmc.launcher.download.fmc.succeed=Installation of {0} Successful
fmc.launcher.download.fmc.tips=ID: {0} \ n Name: {1} \ n Game Version: {2} \ n Integrated Version: {3} \ n File Size: {4} \ n Release Time: {5} \ n \ n {6}
fmc.launcher.download.fmc.url=Download source:
fmc.launcher.download.fmc.url.tips=From: {0}
fmc.launcher.download.mojang.api_error=List loading failed, official interface exception
fmc.launcher.download.mojang.download_error=Download failed: {0}
fmc.launcher.download.mojang.title=Official Original List
fmc.launcher.empty_game=Please select the startup version
fmc.launcher.empty_name=Please enter the game name
fmc.launcher.empty_password=Please enter password
fmc.launcher.java.error=Java download failed, please try again or manually select available Java
fmc.launcher.lang_Select.tips=(Language translation may not be 100% accurate)
fmc.launcher.lang_select=Language
fmc.launcher.launch_select=Start version selection
fmc.launcher.launch_select.delete.confirm=Confirm ({0})
fmc.launcher.launch_select.delete.tips=Are you sure you want to delete {0}? This will permanently lose all archives and configurations of this version, and this operation will be irreparable!
fmc.launcher.launch_select.delete.working=Deleting {0}
fmc.launcher.launch_select.download.fmc=Integrated version download
fmc.launcher.launch_select.download.mojang=Official Original Download
fmc.launcher.launch_select.empty=No available version, please choose official download or integrated version download
fmc.launcher.launch_select.option=Version settings
fmc.launcher.launching=Launching..
fmc.launcher.menu.account=To Register or Manage Players:\nForeverMC uses imyeyu.net's Account System
fmc.launcher.menu.account.fmc=ForeverMC Unified Account Authentication
fmc.launcher.menu.account.mojang=Mojang genuine certification
fmc.launcher.menu.account.type=Verification method: {0}
fmc.launcher.menu.launch.auto=Automatically start after {0} seconds
fmc.launcher.menu.launch.auto.cancel=Cancel automatic start
fmc.launcher.menu.launch.pack=Version: {0}
fmc.launcher.menu.launch.pack.list=Start version management
fmc.launcher.menu.launch.pack.null=No startup version selected
fmc.launcher.option.auto_connect=Automatically connect to server: {0}
fmc.launcher.option.auto_launch=Auto Launch: {0}
fmc.launcher.option.bgm=Launcher BGM Volume: %.0f%%
fmc.launcher.option.game_source=Download Source: {0}
fmc.launcher.option.java.select=Select
fmc.launcher.option.java.select_title=Select Java
fmc.launcher.option.launch_pack=Launch version
fmc.launcher.option.memory=Maximum memory: {0} MB
fmc.launcher.option.multi_download=Multi threaded download resource: {0}
fmc.launcher.option.runtime_source=Download Source: {0}
fmc.launcher.option.server=Server address:
fmc.launcher.option.sound=Launcher Sound Effect Volume: %.0f%%
fmc.launcher.pack_option.auto_connect.tips=If the game crashes after starting, you can try turning off this option
fmc.launcher.pack_option.fabric.install=Install Fabric..
fmc.launcher.pack_option.fabric.installing=Installing Fabric..
fmc.launcher.pack_option.fabric.not_support=Installation failed, there is no suitable version of Fabric for this version
fmc.launcher.pack_option.fabric.not_support_api=Installation failed, no suitable Fabric API loader available
fmc.launcher.pack_option.login_type=Login verification method: {0}
fmc.launcher.pack_option.login_type.tips=If you are unsure of the purpose of this setting, please do not modify it
fmc.launcher.pack_option.title={0} - Version configuration
fmc.launcher.player.select.title=Choose to start an account
fmc.launcher.resource_download=Downloading necessary files, please be patient
fmc.launcher.resource_download.total=Overall progress:
fmc.launcher.resource_error=Unable to connect to Minecraft official resource station
fmc.launcher.rt.download.tips=Downloading Java from {0}
fmc.launcher.timeout=Launch timeout or inability to detect launched games
install=Install
install.fail=Installation failed
launch=@startup
network=network
offline=Off-line
option=Option
password=Password
proxy.host=Proxy address
proxy.port=Proxy port
select=Select
select.empty=No options available
server.status=Server status
source=Source code
startup=Start-up
tips.restart=Application needs to be restarted. Do you want to restart it now?
tips.restart.error=Self restart failed, please try manual restart
turn.off=Off
turn.on=On
version.checking={0} - Checking for updates
version.new={0}, new version: {1}

Some files were not shown because too many files have changed in this diff Show More