Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5abc1f1f9d | ||
|
|
40b4ecd42a | ||
|
|
e0f5c4d91e | ||
|
|
35ecd58460 | ||
|
|
f29c2bc202 | ||
|
|
361e928e73 | ||
|
|
484b65b513 | ||
|
|
19f02e3967 | ||
|
|
495da6b6b0 | ||
|
|
168c4ac708 | ||
|
|
7e8a6679cc | ||
|
|
e90d5185f6 |
No files matched your search
+27
-18
@@ -1,8 +1,11 @@
|
||||
import org.jackhuang.hmcl.gradle.ci.GitHubActionUtils
|
||||
import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||
import org.jackhuang.hmcl.gradle.utils.PropertiesUtils
|
||||
import java.net.URI
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Files
|
||||
@@ -16,30 +19,36 @@ plugins {
|
||||
alias(libs.plugins.shadow)
|
||||
}
|
||||
|
||||
val isOfficial = System.getenv("HMCL_SIGNATURE_KEY") != null
|
||||
|| (System.getenv("GITHUB_REPOSITORY_OWNER") == "HMCL-dev" && System.getenv("GITHUB_BASE_REF")
|
||||
.isNullOrEmpty())
|
||||
val projectConfig = PropertiesUtils.load(rootProject.file("config/project.properties").toPath())
|
||||
|
||||
val isOfficial = JenkinsUtils.IS_ON_CI || GitHubActionUtils.IS_ON_OFFICIAL_REPO
|
||||
|
||||
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
|
||||
val offset = System.getenv("BUILD_NUMBER_OFFSET")?.toInt() ?: 0
|
||||
if (number != null) {
|
||||
(number - offset).toString()
|
||||
} else {
|
||||
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
|
||||
val prefix = if (isOfficial) "dev" else "unofficial"
|
||||
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
|
||||
}
|
||||
}
|
||||
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.6"
|
||||
val versionType = System.getenv("VERSION_TYPE") ?: if (isOfficial) "nightly" else "unofficial"
|
||||
val versionRoot = System.getenv("VERSION_ROOT") ?: projectConfig.getProperty("versionRoot") ?: "3"
|
||||
|
||||
val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
||||
|
||||
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE")
|
||||
val launcherExe = System.getenv("HMCL_LAUNCHER_EXE") ?: ""
|
||||
|
||||
version = "$versionRoot.$buildNumber"
|
||||
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt()
|
||||
if (buildNumber != null) {
|
||||
version = if (JenkinsUtils.IS_ON_CI && versionType == "dev") {
|
||||
"$versionRoot.0.$buildNumber"
|
||||
} else {
|
||||
"$versionRoot.$buildNumber"
|
||||
}
|
||||
} else {
|
||||
val shortCommit = System.getenv("GITHUB_SHA")?.lowercase()?.substring(0, 7)
|
||||
version = if (shortCommit.isNullOrBlank()) {
|
||||
"$versionRoot.SNAPSHOT"
|
||||
} else if (isOfficial) {
|
||||
"$versionRoot.dev-$shortCommit"
|
||||
} else {
|
||||
"$versionRoot.unofficial-$shortCommit"
|
||||
}
|
||||
}
|
||||
|
||||
val embedResources by configurations.registering
|
||||
|
||||
@@ -50,7 +59,7 @@ dependencies {
|
||||
implementation(libs.twelvemonkeys.imageio.webp)
|
||||
implementation(libs.java.info)
|
||||
|
||||
if (launcherExe == null) {
|
||||
if (launcherExe.isBlank()) {
|
||||
implementation(libs.hmclauncher)
|
||||
}
|
||||
|
||||
@@ -195,7 +204,7 @@ tasks.shadowJar {
|
||||
"Enable-Native-Access" to "ALL-UNNAMED"
|
||||
)
|
||||
|
||||
if (launcherExe != null) {
|
||||
if (launcherExe.isNotBlank()) {
|
||||
into("assets") {
|
||||
from(file(launcherExe))
|
||||
}
|
||||
|
||||
@@ -46,21 +46,7 @@ public final class EntryPoint {
|
||||
createHMCLDirectories();
|
||||
LOG.start(Metadata.HMCL_CURRENT_DIRECTORY.resolve("logs"));
|
||||
|
||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU")))
|
||||
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
||||
|
||||
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
||||
if (animationFrameRate != null) {
|
||||
try {
|
||||
if (Integer.parseInt(animationFrameRate) <= 0)
|
||||
throw new NumberFormatException(animationFrameRate);
|
||||
|
||||
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
|
||||
}
|
||||
}
|
||||
|
||||
setupJavaFXVMOptions();
|
||||
checkDirectoryPath();
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
|
||||
@@ -80,6 +66,71 @@ public final class EntryPoint {
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
private static void setupJavaFXVMOptions() {
|
||||
if ("true".equalsIgnoreCase(System.getenv("HMCL_FORCE_GPU"))) {
|
||||
LOG.info("HMCL_FORCE_GPU: true");
|
||||
System.getProperties().putIfAbsent("prism.forceGPU", "true");
|
||||
}
|
||||
|
||||
String animationFrameRate = System.getenv("HMCL_ANIMATION_FRAME_RATE");
|
||||
if (animationFrameRate != null) {
|
||||
LOG.info("HMCL_ANIMATION_FRAME_RATE: " + animationFrameRate);
|
||||
|
||||
try {
|
||||
if (Integer.parseInt(animationFrameRate) <= 0)
|
||||
throw new NumberFormatException(animationFrameRate);
|
||||
|
||||
System.getProperties().putIfAbsent("javafx.animation.pulse", animationFrameRate);
|
||||
} catch (NumberFormatException e) {
|
||||
LOG.warning("Invalid animation frame rate: " + animationFrameRate);
|
||||
}
|
||||
}
|
||||
|
||||
String uiScale = System.getProperty("hmcl.uiScale", System.getenv("HMCL_UI_SCALE"));
|
||||
if (uiScale != null) {
|
||||
uiScale = uiScale.trim();
|
||||
|
||||
LOG.info("HMCL_UI_SCALE: " + uiScale);
|
||||
|
||||
try {
|
||||
float scaleValue;
|
||||
if (uiScale.endsWith("%")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 1)) / 100.0f;
|
||||
} else if (uiScale.endsWith("dpi") || uiScale.endsWith("DPI")) {
|
||||
scaleValue = Integer.parseInt(uiScale.substring(0, uiScale.length() - 3)) / 96.0f;
|
||||
} else {
|
||||
scaleValue = Float.parseFloat(uiScale);
|
||||
}
|
||||
|
||||
float lowerBound;
|
||||
float upperBound;
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
// JavaFX behavior may be abnormal when the DPI scaling factor is too high
|
||||
lowerBound = 0.25f;
|
||||
upperBound = 4f;
|
||||
} else {
|
||||
lowerBound = 0.01f;
|
||||
upperBound = 10f;
|
||||
}
|
||||
|
||||
if (scaleValue >= lowerBound && scaleValue <= upperBound) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
System.getProperties().putIfAbsent("glass.win.uiScale", uiScale);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
LOG.warning("macOS does not support setting UI scale, so it will be ignored");
|
||||
} else {
|
||||
System.getProperties().putIfAbsent("glass.gtk.uiScale", uiScale);
|
||||
}
|
||||
} else {
|
||||
LOG.warning("UI scale out of range: " + uiScale);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Invalid UI scale: " + uiScale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createHMCLDirectories() {
|
||||
if (!Files.isDirectory(Metadata.HMCL_CURRENT_DIRECTORY)) {
|
||||
try {
|
||||
|
||||
@@ -58,6 +58,7 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
@@ -706,6 +707,7 @@ public final class LauncherHelper {
|
||||
*/
|
||||
private final class HMCLProcessListener implements ProcessListener {
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final HMCLGameRepository repository;
|
||||
private final Version version;
|
||||
private final LaunchOptions launchOptions;
|
||||
@@ -849,21 +851,27 @@ public final class LauncherHelper {
|
||||
level = Lang.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
|
||||
logBuffer.add(new Log(log, level));
|
||||
} else {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
logs.addLast(new Log(log, level));
|
||||
if (logs.size() > Log.getLogLines())
|
||||
logs.removeFirst();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
if (!lwjgl) {
|
||||
String lowerCaseLog = log.toLowerCase(Locale.ROOT);
|
||||
if (!detectWindow || lowerCaseLog.contains("lwjgl version") || lowerCaseLog.contains("lwjgl openal")) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (!lwjgl) {
|
||||
lwjgl = true;
|
||||
finishLaunch();
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -888,9 +896,12 @@ public final class LauncherHelper {
|
||||
|
||||
// Game crashed before opening the game window.
|
||||
if (!lwjgl) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (!lwjgl)
|
||||
finishLaunch();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -771,6 +771,7 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
obj.addProperty("wrapper", src.getWrapper());
|
||||
obj.addProperty("fullscreen", src.isFullscreen());
|
||||
obj.addProperty("noJVMArgs", src.isNoJVMArgs());
|
||||
obj.addProperty("noOptimizingJVMArgs", src.isNoOptimizingJVMArgs());
|
||||
obj.addProperty("notCheckGame", src.isNotCheckGame());
|
||||
obj.addProperty("notCheckJVM", src.isNotCheckJVM());
|
||||
obj.addProperty("notPatchNatives", src.isNotPatchNatives());
|
||||
@@ -841,6 +842,7 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
vs.setNativesDir(Optional.ofNullable(obj.get("nativesDir")).map(JsonElement::getAsString).orElse(""));
|
||||
vs.setFullscreen(Optional.ofNullable(obj.get("fullscreen")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNoJVMArgs(Optional.ofNullable(obj.get("noJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNoOptimizingJVMArgs(Optional.ofNullable(obj.get("noOptimizingJVMArgs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckGame(Optional.ofNullable(obj.get("notCheckGame")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
|
||||
@@ -25,6 +25,8 @@ import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -61,7 +63,12 @@ public class OptionToggleButton extends StackPane {
|
||||
toggleButton.setSize(8);
|
||||
FXUtils.setLimitHeight(toggleButton, 30);
|
||||
|
||||
FXUtils.onClicked(container, () -> toggleButton.setSelected(!toggleButton.isSelected()));
|
||||
container.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (e.getButton() == MouseButton.PRIMARY) {
|
||||
toggleButton.setSelected(!toggleButton.isSelected());
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
|
||||
FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> {
|
||||
if (StringUtils.isNotBlank(subtitle)) {
|
||||
|
||||
@@ -40,6 +40,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public class TaskExecutorDialogPane extends BorderPane {
|
||||
private TaskExecutor executor;
|
||||
private TaskCancellationAction onCancel;
|
||||
@SuppressWarnings({"unused", "FieldCanBeLocal"})
|
||||
private final Consumer<FetchTask.SpeedEvent> speedEventHandler;
|
||||
|
||||
private final Label lblTitle;
|
||||
@@ -84,7 +85,7 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
}
|
||||
});
|
||||
|
||||
speedEventHandler = speedEvent -> {
|
||||
speedEventHandler = FetchTask.SPEED_EVENT.registerWeak(speedEvent -> {
|
||||
String unit = "B/s";
|
||||
double speed = speedEvent.getSpeed();
|
||||
if (speed > 1024) {
|
||||
@@ -95,11 +96,10 @@ public class TaskExecutorDialogPane extends BorderPane {
|
||||
speed /= 1024;
|
||||
unit = "MiB/s";
|
||||
}
|
||||
double finalSpeed = speed;
|
||||
String finalUnit = unit;
|
||||
Platform.runLater(() -> lblProgress.setText(String.format("%.1f %s", finalSpeed, finalUnit)));
|
||||
};
|
||||
FileDownloadTask.speedEvent.channel(FetchTask.SpeedEvent.class).registerWeak(speedEventHandler);
|
||||
|
||||
String message = String.format("%.1f %s", speed, unit);
|
||||
Platform.runLater(() -> lblProgress.setText(message));
|
||||
});
|
||||
|
||||
onEscPressed(this, btnCancel::fire);
|
||||
}
|
||||
|
||||
+2
-2
@@ -287,7 +287,7 @@ public class PartialInflaterInputStream extends FilterInputStream {
|
||||
* the mark position becomes invalid.
|
||||
* @see InputStream#reset()
|
||||
*/
|
||||
public synchronized void mark(int readlimit) {
|
||||
public void mark(int readlimit) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,7 +302,7 @@ public class PartialInflaterInputStream extends FilterInputStream {
|
||||
* @see InputStream#mark(int)
|
||||
* @see IOException
|
||||
*/
|
||||
public synchronized void reset() throws IOException {
|
||||
public void reset() throws IOException {
|
||||
throw new IOException("mark/reset not supported");
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,7 @@ import org.jackhuang.hmcl.util.Holder;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
@@ -115,6 +116,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
|
||||
ImageView titleIcon = new ImageView(FXUtils.newBuiltinImage("/assets/img/icon-title.png"));
|
||||
Label titleLabel = new Label(Metadata.FULL_TITLE);
|
||||
if (I18n.isUpsideDown()) {
|
||||
titleIcon.setRotate(180);
|
||||
titleLabel.setRotate(180);
|
||||
}
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
titleNode.getChildren().setAll(titleIcon, titleLabel);
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@ public class PersonalizationPage extends StackPane {
|
||||
}
|
||||
|
||||
Label textOpacity = new Label();
|
||||
FXUtils.setLimitWidth(textOpacity, 35);
|
||||
|
||||
StringBinding valueBinding = Bindings.createStringBinding(() -> ((int) slider.getValue()) + "%", slider.valueProperty());
|
||||
textOpacity.textProperty().bind(valueBinding);
|
||||
|
||||
@@ -43,6 +43,10 @@ public final class I18n {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public static boolean isUpsideDown() {
|
||||
return LocaleUtils.getScript(locale.getLocale()).equals("Qabs");
|
||||
}
|
||||
|
||||
public static boolean isUseChinese() {
|
||||
return LocaleUtils.isChinese(locale.getLocale());
|
||||
}
|
||||
|
||||
@@ -17,37 +17,46 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAccessor;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public final class UpsideDownUtils {
|
||||
private static final Map<Integer, Integer> MAPPER = new LinkedHashMap<>();
|
||||
private static final Map<Integer, Integer> MAPPER = loadMap();
|
||||
|
||||
private static void putChars(char baseChar, String upsideDownChars) {
|
||||
for (int i = 0; i < upsideDownChars.length(); i++) {
|
||||
MAPPER.put(baseChar + i, (int) upsideDownChars.charAt(i));
|
||||
private static Map<Integer, Integer> loadMap() {
|
||||
var map = new LinkedHashMap<Integer, Integer>();
|
||||
|
||||
InputStream inputStream = UpsideDownUtils.class.getResourceAsStream("/assets/lang/upside_down.txt");
|
||||
if (inputStream != null) {
|
||||
try (inputStream) {
|
||||
new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).lines().forEach(line -> {
|
||||
if (line.isBlank() || line.startsWith("#"))
|
||||
return;
|
||||
|
||||
if (line.length() != 2) {
|
||||
LOG.warning("Invalid line: " + line);
|
||||
return;
|
||||
}
|
||||
|
||||
map.put((int) line.charAt(0), (int) line.charAt(1));
|
||||
});
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to load upside_down.txt", e);
|
||||
}
|
||||
} else {
|
||||
LOG.warning("upside_down.txt not found");
|
||||
}
|
||||
}
|
||||
|
||||
private static void putChars(String baseChars, String upsideDownChars) {
|
||||
if (baseChars.length() != upsideDownChars.length()) {
|
||||
throw new IllegalArgumentException("baseChars and upsideDownChars must have same length");
|
||||
}
|
||||
|
||||
for (int i = 0; i < baseChars.length(); i++) {
|
||||
MAPPER.put((int) baseChars.charAt(i), (int) upsideDownChars.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
putChars('a', "ɐqɔpǝɟbɥıظʞןɯuodbɹsʇnʌʍxʎz");
|
||||
putChars('A', "ⱯᗺƆᗡƎℲ⅁HIſʞꞀWNOԀὉᴚS⟘∩ΛMXʎZ");
|
||||
putChars('0', "0ƖᘔƐㄣϛ9ㄥ86");
|
||||
putChars("_,;.?!/\\'", "‾'⸵˙¿¡/\\,");
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
public static String translate(String str) {
|
||||
@@ -56,7 +65,7 @@ public final class UpsideDownUtils {
|
||||
return builder.reverse().toString();
|
||||
}
|
||||
|
||||
private static DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a")
|
||||
private static final DateTimeFormatter BASE_FORMATTER = DateTimeFormatter.ofPattern("MMM d, yyyy, h:mm:ss a")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
|
||||
public static String formatDateTime(TemporalAccessor time) {
|
||||
|
||||
@@ -17,29 +17,32 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.event;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class EventBus {
|
||||
/// @author huangyuhui
|
||||
public final class EventBus extends ClassValue<EventManager<?>> {
|
||||
|
||||
private final ConcurrentHashMap<Class<?>, EventManager<?>> events = new ConcurrentHashMap<>();
|
||||
public static final EventBus EVENT_BUS = new EventBus();
|
||||
|
||||
private EventBus() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EventManager<?> computeValue(@NotNull Class<?> type) {
|
||||
return new EventManager<>();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Event> EventManager<T> channel(Class<T> clazz) {
|
||||
return (EventManager<T>) events.computeIfAbsent(clazz, ignored -> new EventManager<>());
|
||||
return (EventManager<T>) get(clazz);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Event.Result fireEvent(Event obj) {
|
||||
LOG.info(obj + " gets fired");
|
||||
|
||||
return channel((Class<Event>) obj.getClass()).fireEvent(obj);
|
||||
return ((EventManager<Event>) get(obj.getClass())).fireEvent(obj);
|
||||
}
|
||||
|
||||
public static final EventBus EVENT_BUS = new EventBus();
|
||||
}
|
||||
@@ -18,26 +18,28 @@
|
||||
package org.jackhuang.hmcl.event;
|
||||
|
||||
import org.jackhuang.hmcl.util.SimpleMultimap;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.EnumMap;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
/// @author huangyuhui
|
||||
public final class EventManager<T extends Event> {
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final SimpleMultimap<EventPriority, Consumer<T>, CopyOnWriteArraySet<Consumer<T>>> handlers
|
||||
= new SimpleMultimap<>(() -> new EnumMap<>(EventPriority.class), CopyOnWriteArraySet::new);
|
||||
|
||||
@Contract("_ -> param1")
|
||||
public Consumer<T> registerWeak(Consumer<T> consumer) {
|
||||
register(new WeakListener(consumer));
|
||||
return consumer;
|
||||
}
|
||||
|
||||
@Contract("_, _ -> param1")
|
||||
public Consumer<T> registerWeak(Consumer<T> consumer, EventPriority priority) {
|
||||
register(new WeakListener(consumer), priority);
|
||||
return consumer;
|
||||
@@ -47,9 +49,13 @@ public final class EventManager<T extends Event> {
|
||||
register(consumer, EventPriority.NORMAL);
|
||||
}
|
||||
|
||||
public synchronized void register(Consumer<T> consumer, EventPriority priority) {
|
||||
if (!handlers.get(priority).contains(consumer))
|
||||
handlers.put(priority, consumer);
|
||||
public void register(Consumer<T> consumer, EventPriority priority) {
|
||||
lock.lock();
|
||||
try {
|
||||
handlers.get(priority).add(consumer);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void register(Runnable runnable) {
|
||||
@@ -60,23 +66,30 @@ public final class EventManager<T extends Event> {
|
||||
register(t -> runnable.run(), priority);
|
||||
}
|
||||
|
||||
public synchronized Event.Result fireEvent(T event) {
|
||||
for (EventPriority priority : EventPriority.values()) {
|
||||
for (Consumer<T> handler : handlers.get(priority))
|
||||
handler.accept(event);
|
||||
public Event.Result fireEvent(T event) {
|
||||
lock.lock();
|
||||
try {
|
||||
for (EventPriority priority : EventPriority.values()) {
|
||||
for (Consumer<T> handler : handlers.get(priority))
|
||||
handler.accept(event);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
if (event.hasResult())
|
||||
return event.getResult();
|
||||
else
|
||||
return Event.Result.DEFAULT;
|
||||
return event.hasResult() ? event.getResult() : Event.Result.DEFAULT;
|
||||
}
|
||||
|
||||
public synchronized void unregister(Consumer<T> consumer) {
|
||||
handlers.removeValue(consumer);
|
||||
public void unregister(Consumer<T> consumer) {
|
||||
lock.lock();
|
||||
try {
|
||||
handlers.removeValue(consumer);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private class WeakListener implements Consumer<T> {
|
||||
private final class WeakListener implements Consumer<T> {
|
||||
private final WeakReference<Consumer<T>> ref;
|
||||
|
||||
public WeakListener(Consumer<T> listener) {
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.task;
|
||||
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.io.ContentEncoding;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
@@ -355,13 +356,13 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||
|
||||
private static final Timer timer = new Timer("DownloadSpeedRecorder", true);
|
||||
private static final AtomicLong downloadSpeed = new AtomicLong(0L);
|
||||
public static final EventBus speedEvent = new EventBus();
|
||||
public static final EventManager<SpeedEvent> SPEED_EVENT = EventBus.EVENT_BUS.channel(SpeedEvent.class);
|
||||
|
||||
static {
|
||||
timer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
speedEvent.channel(SpeedEvent.class).fireEvent(new SpeedEvent(speedEvent, downloadSpeed.getAndSet(0)));
|
||||
SPEED_EVENT.fireEvent(new SpeedEvent(SPEED_EVENT, downloadSpeed.getAndSet(0)));
|
||||
}
|
||||
}, 0, 1000);
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ public final class Schedulers {
|
||||
return ForkJoinPool.commonPool();
|
||||
}
|
||||
|
||||
public static synchronized void shutdown() {
|
||||
public static void shutdown() {
|
||||
LOG.info("Shutting down executor services.");
|
||||
|
||||
// shutdownNow will interrupt all threads.
|
||||
|
||||
@@ -25,6 +25,7 @@ import javafx.beans.value.ObservableValue;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -144,6 +145,7 @@ public abstract class BindingMapping<T, U> extends ObjectBinding<U> {
|
||||
|
||||
private static final class AsyncMappedBinding<T, U> extends BindingMapping<T, U> {
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private boolean initialized = false;
|
||||
private T prev;
|
||||
private U value;
|
||||
@@ -159,13 +161,16 @@ public abstract class BindingMapping<T, U> extends ObjectBinding<U> {
|
||||
}
|
||||
|
||||
private void tryUpdateValue(T currentPrev) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
if ((initialized && Objects.equals(prev, currentPrev))
|
||||
|| isComputing(currentPrev)) {
|
||||
return;
|
||||
}
|
||||
computing = true;
|
||||
computingPrev = currentPrev;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
CompletableFuture<? extends U> task;
|
||||
@@ -189,7 +194,8 @@ public abstract class BindingMapping<T, U> extends ObjectBinding<U> {
|
||||
}
|
||||
|
||||
private void valueUpdate(T currentPrev, U computed) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (isComputing(currentPrev)) {
|
||||
computing = false;
|
||||
computingPrev = null;
|
||||
@@ -197,15 +203,20 @@ public abstract class BindingMapping<T, U> extends ObjectBinding<U> {
|
||||
value = computed;
|
||||
initialized = true;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void valueUpdateFailed(T currentPrev) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (isComputing(currentPrev)) {
|
||||
computing = false;
|
||||
computingPrev = null;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
@@ -37,6 +38,7 @@ import javafx.beans.binding.ObjectBinding;
|
||||
*/
|
||||
public final class ObservableCache<K, V, E extends Exception> {
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final ExceptionalFunction<K, V, E> source;
|
||||
private final BiConsumer<K, Throwable> exceptionHandler;
|
||||
private final V fallbackValue;
|
||||
@@ -54,22 +56,29 @@ public final class ObservableCache<K, V, E extends Exception> {
|
||||
}
|
||||
|
||||
public Optional<V> getImmediately(K key) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
return Optional.ofNullable(cache.get(key));
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public void put(K key, V value) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
cache.put(key, value);
|
||||
invalidated.remove(key);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
Platform.runLater(observable::invalidate);
|
||||
}
|
||||
|
||||
private CompletableFuture<V> query(K key, Executor executor) {
|
||||
CompletableFuture<V> future;
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
CompletableFuture<V> prev = pendings.get(key);
|
||||
if (prev != null) {
|
||||
return prev;
|
||||
@@ -77,6 +86,8 @@ public final class ObservableCache<K, V, E extends Exception> {
|
||||
future = new CompletableFuture<>();
|
||||
pendings.put(key, future);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
@@ -84,18 +95,24 @@ public final class ObservableCache<K, V, E extends Exception> {
|
||||
try {
|
||||
result = source.apply(key);
|
||||
} catch (Throwable ex) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
pendings.remove(key);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
exceptionHandler.accept(key, ex);
|
||||
future.completeExceptionally(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
cache.put(key, result);
|
||||
invalidated.remove(key);
|
||||
pendings.remove(key, future);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
future.complete(result);
|
||||
Platform.runLater(observable::invalidate);
|
||||
@@ -106,11 +123,14 @@ public final class ObservableCache<K, V, E extends Exception> {
|
||||
|
||||
public V get(K key) {
|
||||
V cached;
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
cached = cache.get(key);
|
||||
if (cached != null && !invalidated.containsKey(key)) {
|
||||
return cached;
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -143,7 +163,9 @@ public final class ObservableCache<K, V, E extends Exception> {
|
||||
return Bindings.createObjectBinding(() -> {
|
||||
V result;
|
||||
boolean refresh;
|
||||
synchronized (this) {
|
||||
|
||||
lock.lock();
|
||||
try {
|
||||
result = cache.get(key);
|
||||
if (result == null) {
|
||||
result = fallbackValue;
|
||||
@@ -151,6 +173,8 @@ public final class ObservableCache<K, V, E extends Exception> {
|
||||
} else {
|
||||
refresh = invalidated.containsKey(key);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
if (!quiet && refresh) {
|
||||
query(key, executor);
|
||||
@@ -160,10 +184,13 @@ public final class ObservableCache<K, V, E extends Exception> {
|
||||
}
|
||||
|
||||
public void invalidate(K key) {
|
||||
synchronized (this) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (cache.containsKey(key)) {
|
||||
invalidated.put(key, Boolean.TRUE);
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
Platform.runLater(observable::invalidate);
|
||||
}
|
||||
|
||||
@@ -22,17 +22,17 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* The managed process.
|
||||
*
|
||||
* @author huangyuhui
|
||||
* @see org.jackhuang.hmcl.launch.ExitWaiter
|
||||
* @see org.jackhuang.hmcl.launch.StreamPump
|
||||
*/
|
||||
/// The managed process.
|
||||
///
|
||||
/// @author huangyuhui
|
||||
/// <!-- @see org.jackhuang.hmcl.launch.ExitWaiter -->
|
||||
/// @see org.jackhuang.hmcl.launch.StreamPump
|
||||
public final class ManagedProcess {
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
private final Process process;
|
||||
private final List<String> commands;
|
||||
private final String classpath;
|
||||
@@ -54,7 +54,7 @@ public final class ManagedProcess {
|
||||
*/
|
||||
public ManagedProcess(Process process, List<String> commands) {
|
||||
this.process = process;
|
||||
this.commands = Collections.unmodifiableList(new ArrayList<>(commands));
|
||||
this.commands = List.copyOf(commands);
|
||||
this.classpath = null;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ public final class ManagedProcess {
|
||||
*/
|
||||
public ManagedProcess(Process process, List<String> commands, String classpath) {
|
||||
this.process = process;
|
||||
this.commands = Collections.unmodifiableList(new ArrayList<>(commands));
|
||||
this.commands = List.copyOf(commands);
|
||||
this.classpath = classpath;
|
||||
}
|
||||
|
||||
@@ -111,9 +111,11 @@ public final class ManagedProcess {
|
||||
*
|
||||
* @see #addLine
|
||||
*/
|
||||
public synchronized List<String> getLines(Predicate<String> lineFilter) {
|
||||
public List<String> getLines(Predicate<String> lineFilter) {
|
||||
lock.lock();
|
||||
|
||||
if (lineFilter == null)
|
||||
return Collections.unmodifiableList(Arrays.asList(lines.toArray(new String[0])));
|
||||
return List.copyOf(lines);
|
||||
|
||||
ArrayList<String> res = new ArrayList<>();
|
||||
for (String line : this.lines) {
|
||||
@@ -123,8 +125,13 @@ public final class ManagedProcess {
|
||||
return Collections.unmodifiableList(res);
|
||||
}
|
||||
|
||||
public synchronized void addLine(String line) {
|
||||
lines.add(line);
|
||||
public void addLine(String line) {
|
||||
lock.lock();
|
||||
try {
|
||||
lines.add(line);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,15 +140,20 @@ public final class ManagedProcess {
|
||||
* If a thread is monitoring this raw process,
|
||||
* you are required to add the instance by this method.
|
||||
*/
|
||||
public synchronized void addRelatedThread(Thread thread) {
|
||||
relatedThreads.add(thread);
|
||||
public void addRelatedThread(Thread thread) {
|
||||
lock.lock();
|
||||
try {
|
||||
relatedThreads.add(thread);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void pumpInputStream(Consumer<String> onLogLine) {
|
||||
public void pumpInputStream(Consumer<String> onLogLine) {
|
||||
addRelatedThread(Lang.thread(new StreamPump(process.getInputStream(), onLogLine, OperatingSystem.NATIVE_CHARSET), "ProcessInputStreamPump", true));
|
||||
}
|
||||
|
||||
public synchronized void pumpErrorStream(Consumer<String> onLogLine) {
|
||||
public void pumpErrorStream(Consumer<String> onLogLine) {
|
||||
addRelatedThread(Lang.thread(new StreamPump(process.getErrorStream(), onLogLine, OperatingSystem.NATIVE_CHARSET), "ProcessErrorStreamPump", true));
|
||||
}
|
||||
|
||||
@@ -172,8 +184,13 @@ public final class ManagedProcess {
|
||||
destroyRelatedThreads();
|
||||
}
|
||||
|
||||
public synchronized void destroyRelatedThreads() {
|
||||
relatedThreads.forEach(Thread::interrupt);
|
||||
public void destroyRelatedThreads() {
|
||||
lock.lock();
|
||||
try {
|
||||
relatedThreads.forEach(Thread::interrupt);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -24,15 +24,20 @@ import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Closeable {
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class ArchiveFileTree<R, E extends ArchiveEntry> implements Closeable {
|
||||
|
||||
public static ArchiveFileTree<?, ?> open(Path file) throws IOException {
|
||||
Path namePath = file.getFileName();
|
||||
@@ -50,23 +55,55 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
|
||||
}
|
||||
}
|
||||
|
||||
protected final F file;
|
||||
protected final Dir<E> root = new Dir<>("");
|
||||
protected final R reader;
|
||||
protected final Dir<E> root = new Dir<>("", "");
|
||||
|
||||
public ArchiveFileTree(F file) {
|
||||
this.file = file;
|
||||
public ArchiveFileTree(R reader) {
|
||||
this.reader = reader;
|
||||
}
|
||||
|
||||
public F getFile() {
|
||||
return file;
|
||||
public R getReader() {
|
||||
return reader;
|
||||
}
|
||||
|
||||
public Dir<E> getRoot() {
|
||||
return root;
|
||||
}
|
||||
|
||||
public @Nullable E getEntry(@NotNull String entryPath) {
|
||||
Dir<E> dir = root;
|
||||
String fileName;
|
||||
if (entryPath.indexOf('/') < 0) {
|
||||
fileName = entryPath;
|
||||
if (fileName.isEmpty())
|
||||
return root.getEntry();
|
||||
} else {
|
||||
String[] path = entryPath.split("/");
|
||||
if (path.length == 0)
|
||||
return root.getEntry();
|
||||
|
||||
for (int i = 0; i < path.length - 1; i++) {
|
||||
String item = path[i];
|
||||
if (item.isEmpty())
|
||||
continue;
|
||||
dir = dir.getSubDirs().get(item);
|
||||
if (dir == null)
|
||||
return null;
|
||||
}
|
||||
|
||||
fileName = path[path.length - 1];
|
||||
E entry = dir.getFiles().get(fileName);
|
||||
if (entry != null)
|
||||
return entry;
|
||||
}
|
||||
|
||||
Dir<E> subDir = dir.getSubDirs().get(fileName);
|
||||
return subDir != null ? subDir.getEntry() : null;
|
||||
}
|
||||
|
||||
protected void addEntry(E entry) throws IOException {
|
||||
String[] path = entry.getName().split("/");
|
||||
List<String> pathList = Arrays.asList(path);
|
||||
|
||||
Dir<E> dir = root;
|
||||
|
||||
@@ -81,7 +118,9 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
|
||||
throw new IOException("A file and a directory have the same name: " + entry.getName());
|
||||
}
|
||||
|
||||
dir = dir.subDirs.computeIfAbsent(item, Dir::new);
|
||||
final int nameEnd = i + 1;
|
||||
dir = dir.subDirs.computeIfAbsent(item, name ->
|
||||
new Dir<>(name, String.join("/", pathList.subList(0, nameEnd))));
|
||||
}
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
@@ -106,6 +145,55 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
|
||||
|
||||
public abstract InputStream getInputStream(E entry) throws IOException;
|
||||
|
||||
public @NotNull InputStream getInputStream(String entryPath) throws IOException {
|
||||
E entry = getEntry(entryPath);
|
||||
if (entry == null)
|
||||
throw new FileNotFoundException("Entry not found: " + entryPath);
|
||||
return getInputStream(entry);
|
||||
}
|
||||
|
||||
public byte[] readBinaryEntry(@NotNull E entry) throws IOException {
|
||||
try (InputStream input = getInputStream(entry)) {
|
||||
return input.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
public String readTextEntry(@NotNull String entryPath) throws IOException {
|
||||
E entry = getEntry(entryPath);
|
||||
if (entry == null)
|
||||
throw new FileNotFoundException("Entry not found: " + entryPath);
|
||||
return readTextEntry(entry);
|
||||
}
|
||||
|
||||
public String readTextEntry(@NotNull E entry) throws IOException {
|
||||
return new String(readBinaryEntry(entry), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
protected void copyAttributes(@NotNull E source, @NotNull Path targetFile) throws IOException {
|
||||
FileTime lastModifiedTime = source.getLastModifiedTime();
|
||||
if (lastModifiedTime != null)
|
||||
Files.setLastModifiedTime(targetFile, lastModifiedTime);
|
||||
}
|
||||
|
||||
public void extractTo(@NotNull String entryPath, @NotNull Path targetFile) throws IOException {
|
||||
E entry = getEntry(entryPath);
|
||||
if (entry == null)
|
||||
throw new FileNotFoundException("Entry not found: " + entryPath);
|
||||
|
||||
extractTo(entry, targetFile);
|
||||
}
|
||||
|
||||
public void extractTo(@NotNull E entry, @NotNull Path targetFile) throws IOException {
|
||||
try (InputStream input = getInputStream(entry)) {
|
||||
Files.copy(input, targetFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
try {
|
||||
copyAttributes(entry, targetFile);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to copy attributes to " + targetFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract boolean isLink(E entry);
|
||||
|
||||
public abstract String getLink(E entry) throws IOException;
|
||||
@@ -117,13 +205,15 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
|
||||
|
||||
public static final class Dir<E extends ArchiveEntry> {
|
||||
private final String name;
|
||||
private final String fullName;
|
||||
private E entry;
|
||||
|
||||
final Map<String, Dir<E>> subDirs = new HashMap<>();
|
||||
final Map<String, E> files = new HashMap<>();
|
||||
|
||||
public Dir(String name) {
|
||||
public Dir(String name, String fullName) {
|
||||
this.name = name;
|
||||
this.fullName = fullName;
|
||||
}
|
||||
|
||||
public boolean isRoot() {
|
||||
@@ -134,6 +224,11 @@ public abstract class ArchiveFileTree<F, E extends ArchiveEntry> implements Clos
|
||||
return name;
|
||||
}
|
||||
|
||||
/// Get the normalized full path. Leading `/` and all `.` in the path will be removed.
|
||||
public @NotNull String getFullName() {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
public @Nullable E getEntry() {
|
||||
return entry;
|
||||
}
|
||||
|
||||
@@ -20,11 +20,13 @@ package org.jackhuang.hmcl.util.tree;
|
||||
|
||||
import kala.compress.archivers.tar.TarArchiveEntry;
|
||||
import kala.compress.archivers.tar.TarArchiveReader;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributeView;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
@@ -98,9 +100,22 @@ public final class TarFileTree extends ArchiveFileTree<TarArchiveReader, TarArch
|
||||
this.shutdownHook = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void copyAttributes(@NotNull TarArchiveEntry source, @NotNull Path targetFile) throws IOException {
|
||||
var fileAttributeView = Files.getFileAttributeView(targetFile, BasicFileAttributeView.class);
|
||||
if (fileAttributeView == null)
|
||||
return;
|
||||
|
||||
fileAttributeView.setTimes(
|
||||
source.getLastModifiedTime(),
|
||||
source.getLastAccessTime(),
|
||||
source.getCreationTime()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream(TarArchiveEntry entry) throws IOException {
|
||||
return file.getInputStream(entry);
|
||||
return reader.getInputStream(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,7 +136,7 @@ public final class TarFileTree extends ArchiveFileTree<TarArchiveReader, TarArch
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
file.close();
|
||||
reader.close();
|
||||
} finally {
|
||||
if (tempFile != null) {
|
||||
Runtime.getRuntime().removeShutdownHook(shutdownHook);
|
||||
|
||||
@@ -20,9 +20,17 @@ package org.jackhuang.hmcl.util.tree;
|
||||
import kala.compress.archivers.zip.ZipArchiveEntry;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributeView;
|
||||
import java.nio.file.attribute.PosixFileAttributeView;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
@@ -51,12 +59,53 @@ public final class ZipFileTree extends ArchiveFileTree<ZipArchiveReader, ZipArch
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (closeReader)
|
||||
file.close();
|
||||
reader.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("OctalInteger")
|
||||
protected void copyAttributes(@NotNull ZipArchiveEntry source, @NotNull Path targetFile) throws IOException {
|
||||
BasicFileAttributeView targetView = Files.getFileAttributeView(targetFile, PosixFileAttributeView.class);
|
||||
|
||||
// target might not support posix even if source does
|
||||
if (targetView == null)
|
||||
targetView = Files.getFileAttributeView(targetFile, BasicFileAttributeView.class);
|
||||
|
||||
if (targetView == null)
|
||||
return;
|
||||
|
||||
targetView.setTimes(
|
||||
source.getLastModifiedTime(),
|
||||
source.getLastAccessTime(),
|
||||
source.getCreationTime()
|
||||
);
|
||||
|
||||
int unixMode = source.getUnixMode();
|
||||
if (unixMode != 0 && targetView instanceof PosixFileAttributeView posixView) {
|
||||
Set<PosixFilePermission> permissions = EnumSet.noneOf(PosixFilePermission.class);
|
||||
|
||||
// Owner permissions
|
||||
if ((unixMode & 0400) != 0) permissions.add(PosixFilePermission.OWNER_READ);
|
||||
if ((unixMode & 0200) != 0) permissions.add(PosixFilePermission.OWNER_WRITE);
|
||||
if ((unixMode & 0100) != 0) permissions.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
|
||||
// Group permissions
|
||||
if ((unixMode & 0040) != 0) permissions.add(PosixFilePermission.GROUP_READ);
|
||||
if ((unixMode & 0020) != 0) permissions.add(PosixFilePermission.GROUP_WRITE);
|
||||
if ((unixMode & 0010) != 0) permissions.add(PosixFilePermission.GROUP_EXECUTE);
|
||||
|
||||
// Others permissions
|
||||
if ((unixMode & 0004) != 0) permissions.add(PosixFilePermission.OTHERS_READ);
|
||||
if ((unixMode & 0002) != 0) permissions.add(PosixFilePermission.OTHERS_WRITE);
|
||||
if ((unixMode & 0001) != 0) permissions.add(PosixFilePermission.OTHERS_EXECUTE);
|
||||
|
||||
posixView.setPermissions(permissions);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream(ZipArchiveEntry entry) throws IOException {
|
||||
return getFile().getInputStream(entry);
|
||||
return getReader().getInputStream(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,7 +115,7 @@ public final class ZipFileTree extends ArchiveFileTree<ZipArchiveReader, ZipArch
|
||||
|
||||
@Override
|
||||
public String getLink(ZipArchiveEntry entry) throws IOException {
|
||||
return getFile().getUnixSymlink(entry);
|
||||
return getReader().getUnixSymlink(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -831,4 +831,19 @@
|
||||
1.21.7-rc2
|
||||
1.21.7
|
||||
1.21.8-rc1
|
||||
1.21.8
|
||||
1.21.8
|
||||
25w31a
|
||||
25w32a
|
||||
25w33a
|
||||
25w34a
|
||||
25w34b
|
||||
25w35a
|
||||
25w36a
|
||||
25w36b
|
||||
25w37a
|
||||
1.21.9-pre1
|
||||
1.21.9-pre2
|
||||
1.21.9-pre3
|
||||
1.21.9-pre4
|
||||
1.21.9-rc1
|
||||
1.21.9
|
||||
@@ -0,0 +1,71 @@
|
||||
aɐ
|
||||
bq
|
||||
cɔ
|
||||
dp
|
||||
eǝ
|
||||
fɟ
|
||||
gᵷ
|
||||
hɥ
|
||||
iᴉ
|
||||
jɾ
|
||||
kʞ
|
||||
lꞁ
|
||||
mɯ
|
||||
nu
|
||||
oo
|
||||
pd
|
||||
qb
|
||||
rɹ
|
||||
ss
|
||||
tʇ
|
||||
un
|
||||
vʌ
|
||||
wʍ
|
||||
xx
|
||||
yʎ
|
||||
zz
|
||||
AⱯ
|
||||
Bᗺ
|
||||
CƆ
|
||||
Dᗡ
|
||||
EƎ
|
||||
FℲ
|
||||
G⅁
|
||||
HH
|
||||
II
|
||||
Jſ
|
||||
Kʞ
|
||||
LꞀ
|
||||
MW
|
||||
NN
|
||||
OO
|
||||
PԀ
|
||||
QὉ
|
||||
Rᴚ
|
||||
SS
|
||||
T⟘
|
||||
U∩
|
||||
VΛ
|
||||
WM
|
||||
XX
|
||||
Yʎ
|
||||
ZZ
|
||||
00
|
||||
1Ɩ
|
||||
2ᘔ
|
||||
3Ɛ
|
||||
4ㄣ
|
||||
5ϛ
|
||||
69
|
||||
7ㄥ
|
||||
88
|
||||
96
|
||||
_‾
|
||||
,'
|
||||
;⸵
|
||||
.˙
|
||||
?¿
|
||||
!¡
|
||||
//
|
||||
\\
|
||||
',
|
||||
+10
-3
@@ -21,7 +21,14 @@ subprojects {
|
||||
name = "libs"
|
||||
dirs = setOf(rootProject.file("lib"))
|
||||
}
|
||||
mavenCentral()
|
||||
|
||||
System.getenv("MAVEN_CENTRAL_REPO").let { repo ->
|
||||
if (repo.isNullOrBlank())
|
||||
mavenCentral()
|
||||
else
|
||||
maven(url = repo)
|
||||
}
|
||||
|
||||
maven(url = "https://jitpack.io")
|
||||
maven(url = "https://libraries.minecraft.net")
|
||||
}
|
||||
@@ -71,10 +78,10 @@ tasks.register<UpdateDocuments>("updateDocuments") {
|
||||
|
||||
tasks.register<CheckUpdate>("checkUpdateDev") {
|
||||
tagPrefix.set("v")
|
||||
api.set("https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json")
|
||||
uri.set("https://ci.huangyuhui.net/job/HMCL")
|
||||
}
|
||||
|
||||
tasks.register<CheckUpdate>("checkUpdateStable") {
|
||||
tagPrefix.set("release-")
|
||||
api.set("https://ci.huangyuhui.net/job/HMCL-stable/lastSuccessfulBuild/api/json")
|
||||
uri.set("https://ci.huangyuhui.net/job/HMCL-stable")
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
repositories {
|
||||
mavenCentral()
|
||||
System.getenv("MAVEN_CENTRAL_REPO").let { repo ->
|
||||
if (repo.isNullOrBlank())
|
||||
mavenCentral()
|
||||
else
|
||||
maven(url = repo)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.jackhuang.hmcl.gradle.ci;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.logging.Logger;
|
||||
@@ -35,6 +37,7 @@ import java.net.http.HttpResponse;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -45,8 +48,12 @@ import java.util.regex.Pattern;
|
||||
public abstract class CheckUpdate extends DefaultTask {
|
||||
private static final Logger LOGGER = Logging.getLogger(CheckUpdate.class);
|
||||
|
||||
private static final String WORKFLOW_JOB = "org.jenkinsci.plugins.workflow.job.WorkflowJob";
|
||||
private static final String WORKFLOW_MULTI_BRANCH_PROJECT = "org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject";
|
||||
private static final String FREE_STYLE_PROJECT = "hudson.model.FreeStyleProject";
|
||||
|
||||
@Input
|
||||
public abstract Property<String> getApi();
|
||||
public abstract Property<String> getUri();
|
||||
|
||||
@Input
|
||||
public abstract Property<String> getTagPrefix();
|
||||
@@ -55,81 +62,150 @@ public abstract class CheckUpdate extends DefaultTask {
|
||||
getOutputs().upToDateWhen(task -> false);
|
||||
}
|
||||
|
||||
private static <T> T fetch(URI uri, Class<T> type) throws IOException, InterruptedException {
|
||||
// // HttpClient implements Closeable since Java 21
|
||||
//noinspection resource
|
||||
var client = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS)
|
||||
.build();
|
||||
|
||||
HttpResponse<String> response = client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() / 100 != 2) {
|
||||
throw new IOException("Bad status code: " + response.statusCode());
|
||||
}
|
||||
|
||||
return new Gson().fromJson(response.body(), type);
|
||||
private static URI toURI(String baseUri, String suffix) {
|
||||
return URI.create(baseUri.endsWith("/")
|
||||
? baseUri + suffix
|
||||
: baseUri + "/" + suffix
|
||||
);
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException, InterruptedException {
|
||||
public void run() throws Exception {
|
||||
String uri = getUri().get();
|
||||
URI apiUri = toURI(uri, "api/json");
|
||||
LOGGER.quiet("Fetching metadata from {}", apiUri);
|
||||
|
||||
BuildMetadata buildMetadata;
|
||||
|
||||
try (var helper = new Helper()) {
|
||||
JsonObject body = Objects.requireNonNull(helper.fetch(apiUri, JsonObject.class));
|
||||
String jobType = Objects.requireNonNull(body.getAsJsonPrimitive("_class"), "Missing _class property")
|
||||
.getAsString();
|
||||
|
||||
if (WORKFLOW_MULTI_BRANCH_PROJECT.equals(jobType)) {
|
||||
Pattern namePattern = Pattern.compile("release%2F3\\.\\d+");
|
||||
|
||||
List<BuildMetadata> metadatas = Objects.requireNonNull(helper.gson.fromJson(body.get("jobs"), new TypeToken<List<SubJobInfo>>() {
|
||||
}), "jobs")
|
||||
.stream()
|
||||
.filter(it -> WORKFLOW_JOB.equals(it._class()))
|
||||
.filter(it -> namePattern.matcher(it.name()).matches())
|
||||
.filter(it -> !it.color().equals("disabled"))
|
||||
.map(it -> {
|
||||
try {
|
||||
return fetchBuildInfo(helper, toURI(it.url, "lastSuccessfulBuild/api/json"));
|
||||
} catch (Throwable e) {
|
||||
throw new GradleException("Failed to retrieve build info from " + it.url(), e);
|
||||
}
|
||||
}).sorted(Comparator.comparing(BuildMetadata::timestamp))
|
||||
.toList();
|
||||
|
||||
if (metadatas.isEmpty())
|
||||
throw new GradleException("Failed to retrieve build metadata from " + apiUri);
|
||||
|
||||
buildMetadata = metadatas.get(metadatas.size() - 1);
|
||||
} else if (WORKFLOW_JOB.equals(jobType) || FREE_STYLE_PROJECT.equals(jobType)) {
|
||||
buildMetadata = fetchBuildInfo(helper, toURI(uri, "lastSuccessfulBuild/api/json"));
|
||||
} else {
|
||||
throw new GradleException("Unsupported job type: " + jobType);
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.quiet("Build metadata found: {}", buildMetadata);
|
||||
|
||||
String githubEnv = Objects.requireNonNullElse(System.getenv("GITHUB_ENV"), "");
|
||||
if (githubEnv.isBlank())
|
||||
LOGGER.warn("GITHUB_ENV is not set");
|
||||
|
||||
String uri = getApi().get();
|
||||
LOGGER.info("Fetching metadata from {}", uri);
|
||||
BuildInfo buildInfo = Objects.requireNonNull(fetch(URI.create(uri), BuildInfo.class),
|
||||
"Could not fetch build info");
|
||||
|
||||
try (PrintWriter writer = githubEnv.isBlank()
|
||||
? null
|
||||
: new PrintWriter(Files.newBufferedWriter(Path.of(githubEnv), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.APPEND))) {
|
||||
|
||||
BiConsumer<String, String> addEnv = (name, value) -> {
|
||||
String item = name + "=" + value;
|
||||
LOGGER.info(item);
|
||||
LOGGER.quiet(item);
|
||||
if (writer != null)
|
||||
writer.println(item);
|
||||
};
|
||||
|
||||
String revision = Objects.requireNonNullElse(buildInfo.actions(), List.<BuildInfo.ActionInfo>of())
|
||||
.stream()
|
||||
.filter(action -> "hudson.plugins.git.util.BuildData".equals(action._class))
|
||||
.map(BuildInfo.ActionInfo::lastBuiltRevision)
|
||||
.map(BuildInfo.ActionInfo.BuiltRevision::SHA1)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new GradleException("Could not find revision"));
|
||||
if (revision.matches("[0-9a-z]{40}"))
|
||||
addEnv.accept("HMCL_COMMIT_SHA", revision);
|
||||
else
|
||||
throw new GradleException("Invalid revision: " + revision);
|
||||
|
||||
Pattern fileNamePattern = Pattern.compile("HMCL-(?<version>\\d+(?:\\.\\d+)+)\\.jar");
|
||||
String version = Objects.requireNonNullElse(buildInfo.artifacts(), List.<BuildInfo.ArtifactInfo>of())
|
||||
.stream()
|
||||
.map(BuildInfo.ArtifactInfo::fileName)
|
||||
.map(fileNamePattern::matcher)
|
||||
.filter(Matcher::matches)
|
||||
.map(matcher -> matcher.group("version"))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new GradleException("Could not find .jar artifact"));
|
||||
addEnv.accept("HMCL_VERSION", version);
|
||||
addEnv.accept("HMCL_TAG_NAME", getTagPrefix().get() + version);
|
||||
addEnv.accept("HMCL_COMMIT_SHA", buildMetadata.revision());
|
||||
addEnv.accept("HMCL_VERSION", buildMetadata.version());
|
||||
addEnv.accept("HMCL_TAG_NAME", getTagPrefix().get() + buildMetadata.version());
|
||||
}
|
||||
}
|
||||
|
||||
private record BuildInfo(long number,
|
||||
List<ArtifactInfo> artifacts,
|
||||
List<ActionInfo> actions
|
||||
) {
|
||||
private record BuildMetadata(String version, String revision, long timestamp) {
|
||||
}
|
||||
|
||||
record ArtifactInfo(String fileName) {
|
||||
private BuildMetadata fetchBuildInfo(Helper helper, URI uri) throws IOException, InterruptedException {
|
||||
LOGGER.quiet("Fetching build info from {}", uri);
|
||||
|
||||
BuildInfo buildInfo = Objects.requireNonNull(helper.fetch(uri, BuildInfo.class), "build info");
|
||||
|
||||
String revision = Objects.requireNonNullElse(buildInfo.actions(), List.<ActionInfo>of())
|
||||
.stream()
|
||||
.filter(action -> "hudson.plugins.git.util.BuildData".equals(action._class()))
|
||||
.map(ActionInfo::lastBuiltRevision)
|
||||
.map(BuiltRevision::SHA1)
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new GradleException("Could not find revision"));
|
||||
if (!revision.matches("[0-9a-z]{40}"))
|
||||
throw new GradleException("Invalid revision: " + revision);
|
||||
|
||||
Pattern fileNamePattern = Pattern.compile("HMCL-(?<version>\\d+(?:\\.\\d+)+)\\.jar");
|
||||
String version = Objects.requireNonNullElse(buildInfo.artifacts(), List.<ArtifactInfo>of())
|
||||
.stream()
|
||||
.map(ArtifactInfo::fileName)
|
||||
.map(fileNamePattern::matcher)
|
||||
.filter(Matcher::matches)
|
||||
.map(matcher -> matcher.group("version"))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new GradleException("Could not find .jar artifact"));
|
||||
|
||||
return new BuildMetadata(version, revision, buildInfo.timestamp());
|
||||
}
|
||||
|
||||
private static final class Helper implements AutoCloseable {
|
||||
private final HttpClient client = HttpClient.newBuilder()
|
||||
.followRedirects(HttpClient.Redirect.ALWAYS)
|
||||
.build();
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
private <T> T fetch(URI uri, Class<T> type) throws IOException, InterruptedException {
|
||||
HttpResponse<String> response = client.send(HttpRequest.newBuilder(uri).build(), HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() / 100 != 2) {
|
||||
throw new IOException("Bad status code " + response.statusCode() + " for " + uri);
|
||||
}
|
||||
|
||||
return gson.fromJson(response.body(), type);
|
||||
}
|
||||
|
||||
record ActionInfo(String _class, BuiltRevision lastBuiltRevision) {
|
||||
record BuiltRevision(String SHA1) {
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
// HttpClient implements AutoCloseable since Java 21
|
||||
if (((Object) client) instanceof AutoCloseable closeable) {
|
||||
closeable.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record SubJobInfo(String _class, String name, String url, String color) {
|
||||
|
||||
}
|
||||
|
||||
private record BuildInfo(long number,
|
||||
long timestamp,
|
||||
List<ArtifactInfo> artifacts,
|
||||
List<ActionInfo> actions
|
||||
) {
|
||||
}
|
||||
|
||||
record ArtifactInfo(String fileName) {
|
||||
}
|
||||
|
||||
record ActionInfo(String _class, BuiltRevision lastBuiltRevision) {
|
||||
}
|
||||
|
||||
record BuiltRevision(String SHA1) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.ci;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/// @author Glavo
|
||||
public final class GitHubActionUtils {
|
||||
private static final String OFFICIAL_ORGANIZATION = "HMCL-dev";
|
||||
|
||||
public static final boolean IS_ON_OFFICIAL_REPO =
|
||||
OFFICIAL_ORGANIZATION.equalsIgnoreCase(System.getenv("GITHUB_REPOSITORY_OWNER"))
|
||||
&& Objects.requireNonNullElse(System.getenv("GITHUB_BASE_REF"), "").isBlank();
|
||||
|
||||
private GitHubActionUtils() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.ci;
|
||||
|
||||
/// @author Glavo
|
||||
public final class JenkinsUtils {
|
||||
|
||||
public static final boolean IS_ON_CI = "1".equals(System.getenv("HMCL_CI"));
|
||||
|
||||
private JenkinsUtils() {
|
||||
}
|
||||
}
|
||||
@@ -18,18 +18,18 @@
|
||||
package org.jackhuang.hmcl.gradle.l10n;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -75,29 +75,31 @@ public abstract class UpsideDownTranslate extends DefaultTask {
|
||||
}
|
||||
|
||||
static final class Translator {
|
||||
private static final Map<Integer, Integer> MAPPER = new LinkedHashMap<>();
|
||||
private static final Map<Integer, Integer> MAPPER = loadMap();
|
||||
|
||||
private static void putChars(char baseChar, String upsideDownChars) {
|
||||
for (int i = 0; i < upsideDownChars.length(); i++) {
|
||||
MAPPER.put(baseChar + i, (int) upsideDownChars.charAt(i));
|
||||
private static Map<Integer, Integer> loadMap() {
|
||||
var map = new LinkedHashMap<Integer, Integer>();
|
||||
|
||||
InputStream inputStream = UpsideDownTranslate.class.getResourceAsStream("upside_down.txt");
|
||||
if (inputStream != null) {
|
||||
try (inputStream) {
|
||||
new String(inputStream.readAllBytes(), StandardCharsets.UTF_8).lines().forEach(line -> {
|
||||
if (line.isBlank() || line.startsWith("#"))
|
||||
return;
|
||||
|
||||
if (line.length() != 2) {
|
||||
throw new GradleException("Invalid line: " + line);
|
||||
}
|
||||
|
||||
map.put((int) line.charAt(0), (int) line.charAt(1));
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new GradleException("Failed to load upside_down.txt", e);
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("upside_down.txt not found");
|
||||
}
|
||||
}
|
||||
|
||||
private static void putChars(String baseChars, String upsideDownChars) {
|
||||
if (baseChars.length() != upsideDownChars.length()) {
|
||||
throw new IllegalArgumentException("baseChars and upsideDownChars must have same length");
|
||||
}
|
||||
|
||||
for (int i = 0; i < baseChars.length(); i++) {
|
||||
MAPPER.put((int) baseChars.charAt(i), (int) upsideDownChars.charAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
putChars('a', "ɐqɔpǝɟbɥıظʞןɯuodbɹsʇnʌʍxʎz");
|
||||
putChars('A', "ⱯᗺƆᗡƎℲ⅁HIſʞꞀWNOԀὉᴚS⟘∩ΛMXʎZ");
|
||||
putChars('0', "0ƖᘔƐㄣϛ9ㄥ86");
|
||||
putChars("_,;.?!/\\'", "‾'⸵˙¿¡/\\,");
|
||||
return Collections.unmodifiableMap(map);
|
||||
}
|
||||
|
||||
private static final Pattern FORMAT_PATTERN = Pattern.compile("^%(\\d\\$)?(\\d+)?(\\.\\d+)?([sdf])");
|
||||
|
||||
+16
-24
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@@ -15,33 +15,25 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.event;
|
||||
package org.jackhuang.hmcl.gradle.utils;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huang
|
||||
*/
|
||||
public class FailedEvent<T> extends Event {
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
private final int failedTime;
|
||||
private T newResult;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Properties;
|
||||
|
||||
public FailedEvent(Object source, int failedTime, T newResult) {
|
||||
super(source);
|
||||
this.failedTime = failedTime;
|
||||
this.newResult = newResult;
|
||||
/// @author Glavo
|
||||
public final class PropertiesUtils {
|
||||
public static @NotNull Properties load(Path path) throws IOException {
|
||||
Properties properties = new Properties();
|
||||
try (var reader = Files.newBufferedReader(path)) {
|
||||
properties.load(reader);
|
||||
}
|
||||
return properties;
|
||||
}
|
||||
|
||||
public int getFailedTime() {
|
||||
return failedTime;
|
||||
private PropertiesUtils() {
|
||||
}
|
||||
|
||||
public T getNewResult() {
|
||||
return newResult;
|
||||
}
|
||||
|
||||
public void setNewResult(T newResult) {
|
||||
this.newResult = newResult;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
_HMCL_PROJECT_ROOT=$(dirname "${BASH_SOURCE[0]}")
|
||||
|
||||
sed -i 's,//services.gradle.org/distributions/,//mirrors.cloud.tencent.com/gradle/,g' "$_HMCL_PROJECT_ROOT/gradle/wrapper/gradle-wrapper.properties"
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
sed -i 's,//services.gradle.org/distributions/,//mirrors.cloud.tencent.com/gradle/,g' "$PWD/gradle/wrapper/gradle-wrapper.properties"
|
||||
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
HMCL_CI = '1'
|
||||
|
||||
VERSION_TYPE = 'dev'
|
||||
MICROSOFT_AUTH_ID = credentials('microsoft_auth_id')
|
||||
MICROSOFT_AUTH_SECRET = credentials('microsoft_auth_secret')
|
||||
CURSEFORGE_API_KEY = credentials('curseforge_api_key')
|
||||
HMCL_SIGNATURE_KEY = credentials('hmcl_signature_key')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh 'bash ./config/jenkins/config-jenkins.sh'
|
||||
sh './gradlew clean makeExecutables --stacktrace --no-daemon'
|
||||
archiveArtifacts artifacts: 'HMCL/build/libs/*'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
pipeline {
|
||||
agent any
|
||||
|
||||
environment {
|
||||
HMCL_CI = '1'
|
||||
|
||||
VERSION_TYPE = 'stable'
|
||||
MICROSOFT_AUTH_ID = credentials('microsoft_auth_id')
|
||||
MICROSOFT_AUTH_SECRET = credentials('microsoft_auth_secret')
|
||||
CURSEFORGE_API_KEY = credentials('curseforge_api_key')
|
||||
HMCL_SIGNATURE_KEY = credentials('hmcl_signature_key')
|
||||
}
|
||||
|
||||
stages {
|
||||
stage('Build') {
|
||||
steps {
|
||||
sh 'bash ./config/jenkins/config-jenkins.sh'
|
||||
sh './gradlew clean makeExecutables --stacktrace --no-daemon'
|
||||
archiveArtifacts artifacts: 'HMCL/build/libs/*'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#
|
||||
# Hello Minecraft! Launcher
|
||||
# Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
versionRoot=3.7
|
||||
Reference in new issue
Block a user