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

138
.gitignore vendored
View File

@ -1,98 +1,48 @@
# ---> JetBrains
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
# ---> Maven
target/ target/
pom.xml.tag !.mvn/wrapper/maven-wrapper.jar
pom.xml.releaseBackup !**/src/main/**/target/
pom.xml.versionsBackup !**/src/test/**/target/
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
.mvn/wrapper/maven-wrapper.jar
# Eclipse m2e generated files ### IntelliJ IDEA ###
# Eclipse Core .idea/modules.xml
.project .idea/jarRepositories.xml
# JDT-specific (Eclipse Java Development Tools) .idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath .classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
/.minecraft
/.physics_mod_cache
/config
/CustomSkinLoader
/etc
/run
/logs
/jre*
/ForeverMC.yaml

4
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,4 @@
# Default ignored files
/shelf/
/workspace.xml
/developer-tools.xml

7
.idea/encodings.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>
</project>

View File

@ -0,0 +1,9 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ForCanBeForeach" enabled="false" level="WARNING" enabled_by_default="false">
<option name="REPORT_INDEXED_LOOP" value="true" />
<option name="ignoreUntypedCollections" value="false" />
</inspection_tool>
</profile>
</component>

14
.idea/misc.xml generated Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

124
.idea/uiDesigner.xml generated Normal file
View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

99
pom.xml Normal file
View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.forevermc.launcher</groupId>
<artifactId>ForeverMC</artifactId>
<version>2.3.0</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.7.1</version>
<configuration>
<archive>
<manifest>
<mainClass>cn.forevermc.launcher.ForeverMC</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<repository>
<id>maven2</id>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.imyeyu.fx.ui</groupId>
<artifactId>timi-fx-ui</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.imyeyu.inject</groupId>
<artifactId>timi-inject</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-media</artifactId>
<version>21.0.2</version>
</dependency>
<dependency>
<groupId>com.imyeyu.compress</groupId>
<artifactId>timi-compress</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.imyeyu.network</groupId>
<artifactId>timi-network</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

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

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