Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
e92a02c
Add April Fools feature logging
Glavo Mar 15, 2026
19a9071
Add April Fools disable setting
Glavo Mar 15, 2026
02d6692
Refactor AprilFools to add getter methods
Glavo Mar 15, 2026
2a2d003
update
Glavo Mar 15, 2026
97f82d9
Add near April Fools' Day detection
Glavo Mar 19, 2026
26a12b9
i18n: Add April Fools disable setting strings
Glavo Mar 19, 2026
d798606
Apply April Fools rotation to title when enabled
Glavo Mar 19, 2026
8fba9d7
Refactor April Fools config check priority
Glavo Mar 19, 2026
9228cec
Refactor April Fools settings visibility logic
Glavo Mar 19, 2026
c544b19
Rename aprilMode variable to aprilFoolsMode
Glavo Mar 19, 2026
fa7a39a
i18n: Fix Traditional Chinese text in April Fools setting
Glavo Mar 19, 2026
d0a8857
Add regional whitelist for April Fools support
Glavo Mar 19, 2026
8b160e3
Remove unused Set import from AprilFools
Glavo Mar 19, 2026
d77e682
Update regional whitelist for April Fools
Glavo Mar 19, 2026
266b912
Add April Fools language switch dialog
Glavo Mar 19, 2026
0bc3084
i18n: Externalize April Fools dialog strings
Glavo Mar 19, 2026
cb93893
Remove unused Locale import from Controllers
Glavo Mar 19, 2026
6ec6871
Wait for file saves before application exit
Glavo Mar 19, 2026
aa40158
Merge remote-tracking branch 'upstream/main' into april-fools
Glavo Mar 19, 2026
d7137a9
Fix file save completion check in waitForAllSaves
Glavo Mar 19, 2026
ec2f0cf
Clarify shutdown hook behavior in FileSaver
Glavo Mar 19, 2026
ad989af
Add application restart capability
Glavo Mar 19, 2026
f4a720f
i18n: Update April Fools dialog text
Glavo Mar 19, 2026
7cd5d16
i18n: Simplify Classical Chinese language label
Glavo Mar 19, 2026
67cc924
i18n: Refine April Fools dialog wording
Glavo Mar 19, 2026
3d39ce8
Simplify April Fools dialog confirmation flow
Glavo Mar 19, 2026
2938e64
Remove AprilFools check from title rotation
Glavo Mar 19, 2026
85b6049
Refactor FileSaver queue to use sealed Action interface
Glavo Mar 19, 2026
db7a18f
i18n: Fix Classical Chinese language label text
Glavo Mar 19, 2026
62b174e
Refactor FileSaver waitForAllSaves with CountDownLatch
Glavo Mar 19, 2026
94c26db
Convert FileSaver javadoc to line comment
Glavo Mar 19, 2026
b695a99
Clarify FileSaver waitForAllSaves javadoc
Glavo Mar 19, 2026
fb8befd
Replace wildcard import with explicit util imports
Glavo Mar 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,21 @@ public void setDisableAutoShowUpdateDialog(boolean disableAutoShowUpdateDialog)
this.disableAutoShowUpdateDialog.set(disableAutoShowUpdateDialog);
}

@SerializedName("disableAprilFools")
private final BooleanProperty disableAprilFools = new SimpleBooleanProperty(false);

public BooleanProperty disableAprilFoolsProperty() {
return disableAprilFools;
}

public boolean isDisableAprilFools() {
return disableAprilFools.get();
}

public void setDisableAprilFools(boolean disableAprilFools) {
this.disableAprilFools.set(disableAprilFools);
}

@SerializedName("shownTips")
private final ObservableMap<String, Object> shownTips = FXCollections.observableHashMap();

Expand Down
55 changes: 55 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.property.DoubleProperty;
Expand Down Expand Up @@ -64,12 +65,16 @@
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.CompletableFuture;

Expand All @@ -82,6 +87,7 @@ public final class Controllers {
public static final String JAVA_VERSION_TIP = "javaVersion";
public static final String JAVA_INTERPRETED_MODE_TIP = "javaInterpretedMode";
public static final String SOFTWARE_RENDERING = "softwareRendering";
public static final String APRIL_FOOLS = "aprilFools";

public static final int MIN_WIDTH = 800 + 2 + 16; // bg width + border width*2 + shadow width*2
public static final int MIN_HEIGHT = 450 + 2 + 40 + 16; // bg height + border width*2 + toolbar height + shadow width*2
Expand Down Expand Up @@ -225,6 +231,8 @@ public static void onApplicationStop() {
public static void initialize(Stage stage) {
LOG.info("Start initializing application");

LOG.info("April Fools: " + AprilFools.isEnabled());

if (System.getProperty("prism.lcdtext") == null) {
String fontAntiAliasing = globalConfig().getFontAntiAliasing();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
Expand Down Expand Up @@ -432,6 +440,53 @@ public static void initialize(Stage stage) {
agreementPane.setActions(agreementLink, yesButton, noButton);
Controllers.dialog(agreementPane);
}

aprilFools:
if (AprilFools.isEnabled()) {
int currentYear = LocalDate.now().getYear();
if (config().getShownTips().get(APRIL_FOOLS) instanceof Number year && year.intValue() >= currentYear)
break aprilFools;

if (!I18n.getLocale().getLocale().getLanguage().equals("zh"))
break aprilFools;

SupportedLocale lzh = SupportedLocale.getSupportedLocales().stream()
.filter(locale -> "lzh".equals(locale.getName()))
.findFirst().orElse(null);

if (lzh == null) {
LOG.warning("No supported locale found for lzh");
break aprilFools;
}

Runnable updateShowTips = () -> config().getShownTips().put(APRIL_FOOLS, currentYear);

Controllers.confirmWithCountdown(i18n("launcher.april_fools.switch_lzh"), null, 10,
MessageType.QUESTION, () -> {
Controllers.confirm(i18n("launcher.april_fools.switch_lzh.confirm"), null, MessageType.QUESTION, () -> {
LOG.info("Switching locale to " + lzh);

updateShowTips.run();
config().setLocalization(lzh);

Controllers.onApplicationStop();

try {
FileSaver.waitForAllSaves();
} catch (InterruptedException ignored) {
// Ignore
}

try {
Restarter.restartSelf();
} catch (IOException e) {
LOG.warning("Failed to restart self", e);
}

Platform.exit();
}, updateShowTips);
}, updateShowTips);
}
}

public static void dialog(Region content) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import org.jackhuang.hmcl.upgrade.UpdateChannel;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
import org.jackhuang.hmcl.util.AprilFools;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
Expand Down Expand Up @@ -224,6 +225,14 @@ public SettingsPage() {
settingsPane.getContent().add(disableAutoShowUpdateDialogPane);
}

if (AprilFools.isShowAprilFoolsSettings()) {
LineToggleButton disableAprilFools = new LineToggleButton();
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
disableAprilFools.selectedProperty().bindBidirectional(config().disableAprilFoolsProperty());
settingsPane.getContent().add(disableAprilFools);
}
Comment on lines +228 to +234

{
MultiFileItem<EnumCommonDirectory> fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.loadChildren(Arrays.asList(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ private static void requestUpdate(Path updateTo, Path self) throws IOException {
startJava(updateTo, "--apply-to", self.toString());
}

private static void startJava(Path jar, String... appArgs) throws IOException {
public static void startJava(Path jar, String... appArgs) throws IOException {
List<String> commandline = new ArrayList<>();
commandline.add(JavaRuntime.getDefault().getBinary().toString());

Expand Down
80 changes: 80 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/util/AprilFools.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 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.util;

import org.jackhuang.hmcl.util.i18n.LocaleUtils;

import java.time.LocalDate;
import java.time.Month;
import java.util.List;

import static org.jackhuang.hmcl.setting.ConfigHolder.config;

/// April Fools' Day utilities.
///
/// This class provides methods to check if it is April Fools' Day or near April Fools' Day.
/// It also provides a method to check if April Fools is enabled.
///
/// @author Glavo
public final class AprilFools {

private static final boolean ENABLED;
private static final boolean SHOW_APRIL_FOOLS_SETTINGS;

static {
var date = LocalDate.now();

// Some countries/regions may oppose April Fools' Day for various reasons.
// Therefore, we use a regional whitelist to avoid risks.
// Currently, we have only listed a limited set of countries/regions for testing.
// We will investigate more countries/regions in the future to expand this list.
boolean supportedRegion = List.of(
"CN", "TW", "HK", "MO", "JP", "KR", "VN", "SG", "MY",
"ES", "DE", "FR", "GB", "RU", "UA", "US"
).contains(LocaleUtils.SYSTEM_DEFAULT.getCountry());

boolean aprilFoolsMode;
String value = System.getProperty("hmcl.april_fools", System.getenv("HMCL_APRIL_FOOLS"));
if ("true".equalsIgnoreCase(value))
aprilFoolsMode = true;
else if ("false".equalsIgnoreCase(value) || !supportedRegion)
aprilFoolsMode = false;
else
aprilFoolsMode = date.getMonth() == Month.APRIL && date.getDayOfMonth() == 1;

ENABLED = aprilFoolsMode && !config().isDisableAprilFools();
SHOW_APRIL_FOOLS_SETTINGS = aprilFoolsMode || supportedRegion && date.getMonth() == Month.MARCH && date.getDayOfMonth() > 30;
}

Comment on lines +51 to +63
/// Whether April Fools settings should be shown.
///
/// This method returns true if April Fools settings should be shown.
public static boolean isShowAprilFoolsSettings() {
return SHOW_APRIL_FOOLS_SETTINGS;
}

/// Whether April Fools is enabled.
///
/// This method returns true if April Fools is enabled.
public static boolean isEnabled() {
return ENABLED;
}

private AprilFools() {
}
}
94 changes: 70 additions & 24 deletions HMCL/src/main/java/org/jackhuang/hmcl/util/FileSaver.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,29 @@
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;

import static org.jackhuang.hmcl.util.logging.Logger.LOG;

/**
* @author Glavo
*/
/// @author Glavo
public final class FileSaver extends Thread {

private static final Pair<Path, String> SHUTDOWN = Pair.pair(null, null);

private static final BlockingQueue<Pair<Path, String>> queue = new LinkedBlockingQueue<>();
private static final BlockingQueue<Action> queue = new LinkedBlockingQueue<>();
private static final AtomicBoolean running = new AtomicBoolean(false);
private static final ReentrantLock runningLock = new ReentrantLock();
private static volatile boolean shutdown = false;

private static void addAction(Action action) {
queue.add(action);
if (running.compareAndSet(false, true)) {
new FileSaver().start();
}
}

private static void doSave(Map<Path, String> map) {
for (Map.Entry<Path, String> entry : map.entrySet()) {
saveSync(entry.getKey(), entry.getValue());
Expand All @@ -53,10 +57,7 @@ public static void save(Path file, String content) {

ShutdownHook.ensureInstalled();

queue.add(Pair.pair(file, content));
if (running.compareAndSet(false, true)) {
new FileSaver().start();
}
addAction(new DoSave(file, content));
}

public static void saveSync(Path file, String content) {
Expand All @@ -70,7 +71,17 @@ public static void saveSync(Path file, String content) {

public static void shutdown() {
shutdown = true;
queue.add(SHUTDOWN);
queue.add(Shutdown.INSTANCE);
}

/// Wait for all saves to complete.
///
/// This method should be called before the [#shutdown()] method.
public static void waitForAllSaves() throws InterruptedException {
assert !shutdown;
Wait wait = new Wait();
addAction(wait);
wait.await();
}

private FileSaver() {
Expand All @@ -92,34 +103,46 @@ public void run() {
runningLock.lock();
try {
HashMap<Path, String> map = new HashMap<>();
ArrayList<Pair<Path, String>> buffer = new ArrayList<>();
ArrayList<Action> buffer = new ArrayList<>();
ArrayList<Wait> waits = new ArrayList<>();

while (!stopped) {
if (shutdown) {
stopCurrentSaver();
} else {
Pair<Path, String> head = queue.poll(30, TimeUnit.SECONDS);
if (head == null || head == SHUTDOWN) {
stopCurrentSaver();
} else {
map.put(head.getKey(), head.getValue());
Action head = queue.poll(30, TimeUnit.SECONDS);
if (head instanceof DoSave save) {
map.put(save.file(), save.content());
//noinspection BusyWait
Thread.sleep(200); // Waiting for more changes
} else if (head instanceof Wait wait) {
waits.add(wait);
} else if (head == null || head instanceof Shutdown) {
// Shutdown or timeout
stopCurrentSaver();
}
}

while (queue.drainTo(buffer) > 0) {
for (Pair<Path, String> pair : buffer) {
if (pair == SHUTDOWN)
for (Action action : buffer) {
if (action instanceof DoSave save) {
map.put(save.file(), save.content());
} else if (action instanceof Wait wait) {
waits.add(wait);
} else if (action instanceof Shutdown) {
stopCurrentSaver();
else
map.put(pair.getKey(), pair.getValue());
}
}
buffer.clear();
}

doSave(map);
map.clear();

for (Wait wait : waits) {
wait.countDown();
}
waits.clear();
}
} catch (InterruptedException e) {
throw new AssertionError("This thread cannot be interrupted", e);
Expand All @@ -128,6 +151,28 @@ public void run() {
}
}

private sealed interface Action {
}

private record DoSave(Path file, String content) implements Action {
}

private static final class Wait implements Action {
private final CountDownLatch latch = new CountDownLatch(1);

public void await() throws InterruptedException {
latch.await();
}

public void countDown() {
latch.countDown();
}
}

private enum Shutdown implements Action {
INSTANCE
}

private static final class ShutdownHook extends Thread {

static {
Expand All @@ -144,9 +189,10 @@ public void run() {
runningLock.lock();
try {
HashMap<Path, String> map = new HashMap<>();
for (Pair<Path, String> pair : queue) {
if (pair != SHUTDOWN)
map.put(pair.getKey(), pair.getValue());
for (Action action : queue) {
if (action instanceof DoSave save) {
map.put(save.file(), save.content());
}
}
doSave(map);
} finally {
Expand Down
Loading