Compare commits
8
Commits
v3.8.0.306
...
v3.8.0.307
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bbc6ef777 | ||
|
|
f4c6a4f891 | ||
|
|
733b2c90c4 | ||
|
|
81b61523df | ||
|
|
d93f4d6bbb | ||
|
|
f0a16b0cd9 | ||
|
|
783cadcf77 | ||
|
|
cbb4b0c51b |
No files matched your search
@@ -21,5 +21,9 @@ jobs:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-cleanup: never
|
||||
- name: Check Codes
|
||||
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
|
||||
@@ -9,9 +9,19 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
dev-check-update:
|
||||
check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
include:
|
||||
- channel: dev
|
||||
task: checkUpdateDev
|
||||
- channel: stable
|
||||
task: checkUpdateStable
|
||||
runs-on: ubuntu-latest
|
||||
name: check-update-${{ matrix.channel }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up JDK
|
||||
@@ -22,7 +32,7 @@ jobs:
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Fetch last version
|
||||
run: ./gradlew checkUpdateDev --no-daemon --info --stacktrace
|
||||
run: ./gradlew ${{ matrix.task }} --no-daemon --info --stacktrace
|
||||
- name: Check for existing tags
|
||||
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||
- name: Download artifacts
|
||||
@@ -34,66 +44,16 @@ jobs:
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
env:
|
||||
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "*Notice: changelogs are written in Chinese.*" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "| File Name | SHA-256 Checksum |" >> RELEASE_NOTE
|
||||
echo "| --- | --- |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.exe]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.exe) | \`$(cat HMCL-$HMCL_VERSION.exe.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.jar]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.jar) | \`$(cat HMCL-$HMCL_VERSION.jar.sha256)\` |" >> RELEASE_NOTE
|
||||
echo "| [HMCL-$HMCL_VERSION.sh]($GH_DOWNLOAD_BASE_URL/v$HMCL_VERSION/HMCL-$HMCL_VERSION.sh) | \`$(cat HMCL-$HMCL_VERSION.sh.sha256)\` |" >> RELEASE_NOTE
|
||||
env:
|
||||
GH_DOWNLOAD_BASE_URL: https://github.com/HMCL-dev/HMCL/releases/download
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.sh
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
prerelease: true
|
||||
stable-check-update:
|
||||
if: ${{ github.repository_owner == 'HMCL-dev' }}
|
||||
needs: dev-check-update
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '25'
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
|
||||
- name: Fetch last version
|
||||
run: ./gradlew checkUpdateStable --no-daemon --info --stacktrace
|
||||
- name: Check for existing tags
|
||||
run: if ! git tag -l | grep -q "$HMCL_TAG_NAME"; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||
- name: Download artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.exe.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.jar.sha256"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh"
|
||||
wget "$HMCL_CI_DOWNLOAD_BASE_URI/HMCL-$HMCL_VERSION.sh.sha256"
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "**This version is a stable version.**" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/stable.html" >> RELEASE_NOTE
|
||||
if [ "${{ matrix.channel }}" = "stable" ]; then
|
||||
echo "**This version is a stable version.**" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
fi
|
||||
echo "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/${{ matrix.channel }}.html" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "*Notice: changelogs are written in Chinese.*" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
|
||||
@@ -19,8 +19,12 @@ jobs:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
java-package: 'jdk+fx'
|
||||
- name: Setup Gradle
|
||||
uses: gradle/actions/setup-gradle@v5
|
||||
with:
|
||||
cache-cleanup: never
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build --no-daemon
|
||||
run: ./gradlew build --no-daemon --parallel
|
||||
env:
|
||||
MICROSOFT_AUTH_ID: ${{ secrets.MICROSOFT_AUTH_ID }}
|
||||
MICROSOFT_AUTH_SECRET: ${{ secrets.MICROSOFT_AUTH_SECRET }}
|
||||
@@ -47,4 +51,4 @@ jobs:
|
||||
name: HMCL-${{ env.SHORT_SHA }}-sh
|
||||
path: |
|
||||
HMCL/build/libs/HMCL-*.sh
|
||||
HMCL/build/libs/HMCL-*.sh.sha256
|
||||
HMCL/build/libs/HMCL-*.sh.sha256
|
||||
@@ -20,7 +20,6 @@ package org.jackhuang.hmcl.setting;
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -38,18 +37,15 @@ import org.jackhuang.hmcl.java.JavaRuntime;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.gson.*;
|
||||
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
|
||||
import org.jackhuang.hmcl.util.javafx.DirtyTracker;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.*;
|
||||
import java.net.Proxy;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
@JsonAdapter(value = Config.Adapter.class)
|
||||
public final class Config implements Observable {
|
||||
public final class Config extends ObservableSetting {
|
||||
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
public static final int CURRENT_UI_VERSION = 0;
|
||||
@@ -67,54 +63,15 @@ public final class Config implements Observable {
|
||||
.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
|
||||
.create();
|
||||
|
||||
private static final List<ObservableField<Config>> FIELDS;
|
||||
|
||||
static {
|
||||
final MethodHandles.Lookup lookup = MethodHandles.lookup();
|
||||
Field[] fields = Config.class.getDeclaredFields();
|
||||
|
||||
var configFields = new ArrayList<ObservableField<Config>>(fields.length);
|
||||
for (Field field : fields) {
|
||||
int modifiers = field.getModifiers();
|
||||
if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers))
|
||||
continue;
|
||||
|
||||
configFields.add(ObservableField.of(lookup, field));
|
||||
}
|
||||
FIELDS = List.copyOf(configFields);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Config fromJson(String json) throws JsonParseException {
|
||||
return CONFIG_GSON.fromJson(json, Config.class);
|
||||
}
|
||||
|
||||
private transient final ObservableHelper helper = new ObservableHelper(this);
|
||||
private transient final DirtyTracker tracker = new DirtyTracker();
|
||||
private transient final Map<String, JsonElement> unknownFields = new HashMap<>();
|
||||
|
||||
public Config() {
|
||||
var shouldBeWrite = Collections.<Observable>newSetFromMap(new IdentityHashMap<>());
|
||||
Collections.addAll(shouldBeWrite, configVersion, uiVersion);
|
||||
|
||||
for (var field : FIELDS) {
|
||||
Observable observable = field.get(this);
|
||||
if (shouldBeWrite.contains(observable))
|
||||
tracker.markDirty(observable);
|
||||
else
|
||||
tracker.track(observable);
|
||||
observable.addListener(helper);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
helper.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvalidationListener listener) {
|
||||
helper.removeListener(listener);
|
||||
tracker.markDirty(configVersion);
|
||||
tracker.markDirty(uiVersion);
|
||||
register();
|
||||
}
|
||||
|
||||
public String toJson() {
|
||||
@@ -775,53 +732,10 @@ public final class Config implements Observable {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
public static final class Adapter implements JsonSerializer<Config>, JsonDeserializer<Config> {
|
||||
|
||||
public static final class Adapter extends ObservableSetting.Adapter<Config> {
|
||||
@Override
|
||||
public JsonElement serialize(Config config, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (config == null)
|
||||
return JsonNull.INSTANCE;
|
||||
|
||||
JsonObject result = new JsonObject();
|
||||
for (var field : FIELDS) {
|
||||
Observable observable = field.get(config);
|
||||
if (config.tracker.isDirty(observable)) {
|
||||
field.serialize(result, config, context);
|
||||
}
|
||||
}
|
||||
config.unknownFields.forEach(result::add);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Config deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == null || json.isJsonNull())
|
||||
return null;
|
||||
|
||||
if (!json.isJsonObject())
|
||||
throw new JsonParseException("Config is not an object: " + json);
|
||||
|
||||
Config config = new Config();
|
||||
|
||||
var values = new LinkedHashMap<>(json.getAsJsonObject().asMap());
|
||||
for (ObservableField<Config> field : FIELDS) {
|
||||
JsonElement value = values.remove(field.getSerializedName());
|
||||
if (value == null) {
|
||||
for (String alternateName : field.getAlternateNames()) {
|
||||
value = values.remove(alternateName);
|
||||
if (value != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
config.tracker.markDirty(field.get(config));
|
||||
field.deserialize(config, value, context);
|
||||
}
|
||||
}
|
||||
|
||||
config.unknownFields.putAll(values);
|
||||
return config;
|
||||
protected Config createInstance() {
|
||||
return new Config();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,7 +250,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
twoLineListItem.addTag(i18n("version.game.release"));
|
||||
imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
}
|
||||
case PENDING, SNAPSHOT -> {
|
||||
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
twoLineListItem.addTag(i18n("version.game.april_fools"));
|
||||
@@ -455,7 +455,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
return switch (filter) {
|
||||
case RELEASE -> versionType == RemoteVersion.Type.RELEASE;
|
||||
case SNAPSHOTS -> versionType == RemoteVersion.Type.SNAPSHOT
|
||||
|| versionType == RemoteVersion.Type.PENDING;
|
||||
|| versionType == RemoteVersion.Type.PENDING
|
||||
|| versionType == RemoteVersion.Type.UNOBFUSCATED;
|
||||
case APRIL_FOOLS -> versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(it.getGameVersion()).isAprilFools();
|
||||
case OLD -> versionType == RemoteVersion.Type.OLD;
|
||||
|
||||
@@ -112,7 +112,14 @@ public final class Versions {
|
||||
|
||||
JFXButton deleteButton = new JFXButton(i18n("button.delete"));
|
||||
deleteButton.getStyleClass().add("dialog-error");
|
||||
deleteButton.setOnAction(e -> profile.getRepository().removeVersionFromDisk(version));
|
||||
deleteButton.setOnAction(e -> {
|
||||
Task.supplyAsync(Schedulers.io(), () -> profile.getRepository().removeVersionFromDisk(version))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (exception != null || !Boolean.TRUE.equals(result)) {
|
||||
Controllers.dialog(i18n("version.manage.remove.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}).start();
|
||||
});
|
||||
|
||||
Controllers.confirmAction(message, i18n("message.warning"), MessageDialogPane.MessageType.WARNING, deleteButton);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.translator.Translator_lzh;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
@@ -79,6 +80,8 @@ public final class MinecraftWiki {
|
||||
if (SNAPSHOT_PATTERN.matcher(wikiVersion).matches()) {
|
||||
if (wikiVersion.equals("22w13oneblockatatime"))
|
||||
wikiVersion = "22w13oneBlockAtATime";
|
||||
else
|
||||
wikiVersion = StringUtils.removeSuffix(wikiVersion, "_unobfuscated");
|
||||
return locale.i18n("wiki.version.game.snapshot", wikiVersion) + variantSuffix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1569,6 +1569,7 @@ version.manage.redownload_assets_index=Update Game Assets
|
||||
version.manage.remove=Delete Instance
|
||||
version.manage.remove.confirm.trash=Are you sure you want to remove the instance "%1$s"? You can still find its files in your recycle bin by the name of "%2$s".
|
||||
version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its saves and other data. Do you still want to delete the instance "%s"?
|
||||
version.manage.remove.failed=Failed to delete the instance. Some files might be in use.
|
||||
version.manage.remove_assets=Delete All Assets
|
||||
version.manage.remove_libraries=Delete All Libraries
|
||||
version.manage.rename=Rename Instance
|
||||
|
||||
@@ -1353,6 +1353,7 @@ version.manage.redownload_assets_index=更新遊戲資源檔案
|
||||
version.manage.remove=刪除該實例
|
||||
version.manage.remove.confirm.trash=真的要刪除實例「%s」嗎? 你可以在系統的資源回收筒 (或垃圾桶) 中還原目錄「%s」來找回該實例。
|
||||
version.manage.remove.confirm.independent=由於該實例啟用了「(全域/實例特定) 遊戲設定 → 執行路徑 → 各實例獨立」設定,刪除該實例將導致該遊戲的存檔等資料一同被刪除!真的要刪除實例「%s」嗎?
|
||||
version.manage.remove.failed=刪除實例失敗。可能檔案被占用。
|
||||
version.manage.remove_assets=刪除所有遊戲資源檔案
|
||||
version.manage.remove_libraries=刪除所有支援庫檔案
|
||||
version.manage.rename=重新命名該實例
|
||||
|
||||
@@ -1363,6 +1363,7 @@ version.manage.redownload_assets_index=更新游戏资源文件
|
||||
version.manage.remove=删除该实例
|
||||
version.manage.remove.confirm.trash=真的要删除实例“%s”吗?你可以在系统的回收站中还原“%s”文件夹来找回该实例。
|
||||
version.manage.remove.confirm.independent=由于该游戏启用了“(全局/实例特定) 游戏设置 → 版本隔离 → 各实例独立”选项,删除该实例将导致该游戏的存档等数据一同被删除!真的要删除实例“%s”吗?
|
||||
version.manage.remove.failed=删除实例失败。可能文件被占用。
|
||||
version.manage.remove_assets=删除所有游戏资源文件
|
||||
version.manage.remove_libraries=删除所有库文件
|
||||
version.manage.rename=重命名该实例
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -548,7 +548,7 @@ gregic-skies;555;;Gregic Skies;;
|
||||
;557;;齿轮颂歌:暮光;Gear Carol;
|
||||
pluma;558;;Pluma, a Journey to the Future!;;Pluma
|
||||
all-the-mods-7-to-the-sky;559;;All the Mods 7 - To the Sky;;ATM7S
|
||||
;560;;月见苍穹传说:大地演化;TskimiCanopyCraft: Terra Evolution;TSCC:TE
|
||||
;560;;月见苍穹传说;TskimiCanopyCraft;TSCC
|
||||
;561;;最后的战役:劫后余生;The Last Stand:Aftermath;LSA/TLS
|
||||
;562;;小猪配平;Peppa Pig's Burning Broom;2P2B
|
||||
zerblands-remastered;563;;Zerblands-Remastered;;
|
||||
@@ -623,7 +623,7 @@ crop-block;632;;Crop Block;;
|
||||
;634;;Technical Electrical;;
|
||||
cobblemon-forge;635;;Cobblemon Official Modpack;;
|
||||
sbeevs-industrial-revolution;636;;Sbeev's Industrial Revolution;;
|
||||
;637;;Gensokyo Reimagined QOL;;
|
||||
;637;;Gensokyo Reimagined QOL;;GR
|
||||
statech-industry;638;;StaTech Industry;;
|
||||
vault-hunters-1-18-2;639;;宝藏猎人3;Vault Hunters 3rd Edition;
|
||||
survival-hard-rock;640;;生存硬摇滚;Survival Hard Rock;
|
||||
@@ -791,7 +791,7 @@ craftingcraft;800;;合成工艺;CraftingCraft;
|
||||
chemillas-reborn;804;;化学:重生;Chemilla's Reborn;CR
|
||||
;805;;群峦传说·异世界行纪;Terra Firma Craft:Travel in Another World;TFC:TAW
|
||||
milkyway;806;;Create: Milkyway;;
|
||||
farming-crossing-4;807;;Farming Crossing 4;;
|
||||
farming-crossing-4;807;;农业森友会 4;Farming Crossing 4;
|
||||
hardrock-terrafirmacraft-4-realistic-extreme;808;;HardRock TerraFirmaCraft;;TFCH/HTFC
|
||||
terrafirma-rebirth;809;;群峦:重生;TerraFirma: Rebirth;TFCR
|
||||
create-live-5-skyblock;810;;Create Live 5;;
|
||||
@@ -868,7 +868,7 @@ aeon-automata;881;;Aeon Automata;;AA
|
||||
;882;;被恐惧支配2:收容失效;Dominated by Fear 2 Containment Breach;DoF2
|
||||
;883;;剑拔弩张之时;;TC
|
||||
project-architect-2;884;;Project Architect 2;;
|
||||
;885;;路网通途 / 更好的我的世界铁路;Railway Network Access;RNA
|
||||
;885;;路网通途 / 接入铁路网络;Railway Network Access;RNA
|
||||
create-mekanized;886;;Create: Mekanized;;
|
||||
create-perfect-world-2;887;;Create: Perfect World 2;;
|
||||
create-stellar;888;;Create Stellar;;
|
||||
@@ -1048,7 +1048,7 @@ beyond-depth;1062;;Beyond Depth;;
|
||||
;1064;;真幻世界;Realfantacy World;R.F.
|
||||
minenufactory;1065;;创世工厂;Minenufactory;MF
|
||||
craft-to-exile-2;1066;;放逐之路2;Craft to Exile 2 (VR Support);CTE2
|
||||
;1067;;龙境III:菌土重来;Dragon Odyssey:Return of the Fungal Earth;DO3
|
||||
;1067;;龙境III:菌土重来;Dragon Odyssey: Fungreda;DO3
|
||||
the-culinary-journey;1068;;美食的旅途;The Culinary Journey;TCJ
|
||||
;1069;;缥缈方舟;;PMFZ
|
||||
;1070;;东方匠魂传;Touhou Tinker Fantasy;TTF
|
||||
@@ -1075,7 +1075,7 @@ roguelike-adventures-and-dungeons-3;1090;;冒险与地牢3;Roguelike Adventures
|
||||
;1092;;玄理2:洗剪版;EnigTech 2 - Dishwasher's Cut;ET2DC
|
||||
wheelchair-gregtech;1093;;Wheelchair Gregtech;;
|
||||
egged-up;1094;;Egged Up;;
|
||||
;1095;;香草纪元:食旅纪行;VanillaEra: FaresChron;
|
||||
;1095;;香草纪元:食旅纪行;VanillaEra: FaresChron;VEFC
|
||||
;1096;;愚者;The Fool;TF
|
||||
;1097;;时间之书2:星梦指南;BookofTime II Stardream Compass;BKSC
|
||||
;1098;;群峦之上:无垠伊土;EndlessFirma;EnF
|
||||
@@ -1131,7 +1131,7 @@ endless-horror-the-ultimate-horror-modpack;1146;;Endless Horror - The ULTIMATE H
|
||||
picky;1148;;Picky;;
|
||||
resource-factory;1149;;资源工厂;Resource Factory;
|
||||
stacia-2-expert;1150;;Stacia 2 Expert;;S2E
|
||||
;1151;;田园乐事;;
|
||||
;1151;;田园乐事;Conutryside_Delight;
|
||||
;1152;;世界之旅;Journey of the world;JOTW
|
||||
;1153;;清新优化;Fresh optimization;FRO
|
||||
;1154;;遗世之匠;Touhou:The Forgotten Swordsmith;
|
||||
@@ -1159,8 +1159,7 @@ society-sunlit-valley;1176;;Society: Sunlit Valley;;
|
||||
ciscos-fantasy-medieval-rpg-dragonfyre;1177;;Cisco's Fantasy Medieval RPG [Dragonfyre];;
|
||||
all-the-mods-10-sky;1178;;All the Mods 10: To the Sky;;ATM10S
|
||||
;1179;;谐伴匠途;;
|
||||
the-cursed-wanderer-reborn;1180;;咒游诸界:重生;The Cursed Wanderer: Reborn;TCWR
|
||||
;1181;;Turbo Vulkan;;TuVk
|
||||
;1181;;Turbo Vulkan;;TV
|
||||
uls-zsurvival-craft;1182;;Z-Liminex;;
|
||||
;1183;;MITE 重生 3;MITE Reborn 3;MR
|
||||
reclamation-reclaim-the-world;1184;;Reclamation - Reclaim the World!;;
|
||||
@@ -1179,3 +1178,49 @@ pvzw;1195;;植物大战僵尸:世界;Plants VS Zombies : World;PVZW
|
||||
new-century-adventure;1197;;新世纪之险;New Century Adventure;NCA
|
||||
repalmon;1198;;Re帕鲁梦;;RPLM
|
||||
structural-ingenuity;1199;;结构型奇思妙想;Structural Ingenuity;
|
||||
arcaneaxis-ars;1200;;秘法象限;ArcaneAxis;ARS
|
||||
gregtech-easy;1201;;格雷科技简单版;GregTech Easy;GTE
|
||||
;1202;;Creation Exploring the Road of;;CETR
|
||||
multiblockful-skies;1203;;Multiblockful Skies;;
|
||||
;1204;;刀剑物语;SwordStory;SS
|
||||
;1205;;无垠征途;The Unbounded Expedition;TUE
|
||||
;1206;;云栖谧土;Nephosia;
|
||||
custom-battleroyale-complete;1207;;完整自定义大逃杀;Custome BattleRoyale Complete;CBRC
|
||||
paradise-of-lucky-blocks;1208;;Paradise of Lucky Blocks;;
|
||||
;1209;;极致优化;Xavier Optimized;XO
|
||||
;1210;;神秘时代4花园;Thaumcraft IV Sky Islands;
|
||||
;1211;;弥散往生;Diffusion: Soulfare;
|
||||
mana-metal-new-horizons;1212;;魔法金屬:新視野;Mana Metal: New Horizons;M3NH
|
||||
farmy-skyblock;1213;;Farmy Skyblock;;
|
||||
bloody-skyblock-modpack;1214;;Bloody Skyblock;;
|
||||
starages;1215;;Star Space Ages;;
|
||||
create-spectral-cuisine;1216;;Create: Spectral Cuisine;;
|
||||
project-infinity-0-1;1217;;Project Infinity 0.1;;
|
||||
create-live-6;1218;;Create Live 6;;
|
||||
;1219;;全面包围-硬核惊变;Full encirclement;FLET
|
||||
;1220;;科波菲尔;Copperfield;CPF
|
||||
;1221;;试用核心/I.O.P的水槽基础整合;Trial Essential;
|
||||
tnp-limitless-8;1222;;TNP Limitless 8;;
|
||||
;1223;;重度机械症;Mechanomania;
|
||||
;1224;;寻此苦旅3;Per Aspera3;PA3
|
||||
calamity-rpg-iii-wounds-of-the-world;1225;;Calamity RPG III : Wounds of the World;;
|
||||
;1226;;烛枫公会优化整合包;;ZFGH
|
||||
legends-reborn-medieval;1227;;Legends Reborn: Medieval;;LRM
|
||||
;1228;;双衰变;;DEGENE
|
||||
nd-new-dawn;1229;;New Dawn;;ND
|
||||
;1230;;星遥优化;StarCarefree Optimized;SO
|
||||
;1231;;寄生仙途;;JSXT
|
||||
;1232;;神秘启旅;Thaum Journey;
|
||||
nightfallcraft-the-casket-of-reveries;1233;;远梦之棺;NightfallCraft - The Casket of Reveries;
|
||||
openworld-battleroyale;1234;;开放世界大逃杀;Openworld BattleRoyale;OBR
|
||||
;1235;;未尽之路;Unfinished Path;UP
|
||||
;1236;;命运齿轮;Fate's Machinery;FOM
|
||||
linggango;1237;;Linggango;;
|
||||
create-industrialized-tech;1238;;Create: Industrialized Tech;;
|
||||
;1239;;灵璟铁路都市;LingJing Train City;LJTC
|
||||
erstwhile-journey-2;1240;;往昔之旅2;Erstwhile Journey 2;EJ2
|
||||
;1241;;诡厄:使徒;;
|
||||
;1242;;时间之书 : 星之旅行-重制版;BookofTime StarTravel;BKSL
|
||||
cook-in-skyland-elegant-feast;1243;;天空厨房:典雅盛宴;Cook in Skyland Elegant Feast;CiS2
|
||||
ftb-stoneblock-4;1244;;FTB Stoneblock 4;;SB4
|
||||
techevdiscovery;1245;;TechEv || Discovery;;
|
||||
@@ -128,6 +128,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
|
||||
RELEASE,
|
||||
SNAPSHOT,
|
||||
OLD,
|
||||
PENDING
|
||||
PENDING,
|
||||
UNOBFUSCATED,
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,7 @@ public final class GameRemoteVersion extends RemoteVersion {
|
||||
case SNAPSHOT -> Type.SNAPSHOT;
|
||||
case UNKNOWN -> Type.UNCATEGORIZED;
|
||||
case PENDING -> Type.PENDING;
|
||||
case UNOBFUSCATED -> Type.UNOBFUSCATED;
|
||||
default -> Type.OLD;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public enum ReleaseType {
|
||||
OLD_BETA("old-beta"),
|
||||
OLD_ALPHA("old-alpha"),
|
||||
PENDING("pending"),
|
||||
UNOBFUSCATED("unobfuscated"),
|
||||
UNKNOWN("unknown");
|
||||
|
||||
private final String id;
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
/*
|
||||
* 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.util.gson;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.MapProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SetProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.jackhuang.hmcl.util.TypeUtils;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class ObservableField<T> {
|
||||
|
||||
public static <T> ObservableField<T> of(MethodHandles.Lookup lookup, Field field) {
|
||||
String name;
|
||||
List<String> alternateNames;
|
||||
|
||||
SerializedName serializedName = field.getAnnotation(SerializedName.class);
|
||||
if (serializedName == null) {
|
||||
name = field.getName();
|
||||
alternateNames = List.of();
|
||||
} else {
|
||||
name = serializedName.value();
|
||||
alternateNames = List.of(serializedName.alternate());
|
||||
}
|
||||
|
||||
VarHandle varHandle;
|
||||
try {
|
||||
varHandle = lookup.unreflectVarHandle(field);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
if (ObservableList.class.isAssignableFrom(field.getType())) {
|
||||
Type listType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), List.class);
|
||||
if (!(listType instanceof ParameterizedType))
|
||||
throw new IllegalArgumentException("Cannot resolve the list type of " + field.getName());
|
||||
return new CollectionField<>(name, alternateNames, varHandle, listType, listType);
|
||||
} else if (ObservableSet.class.isAssignableFrom(field.getType())) {
|
||||
Type setType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), Set.class);
|
||||
if (!(setType instanceof ParameterizedType))
|
||||
throw new IllegalArgumentException("Cannot resolve the set type of " + field.getName());
|
||||
|
||||
ParameterizedType listType = TypeUtils.newParameterizedTypeWithOwner(
|
||||
null,
|
||||
List.class,
|
||||
((ParameterizedType) setType).getActualTypeArguments()[0]
|
||||
);
|
||||
return new CollectionField<>(name, alternateNames, varHandle, setType, listType);
|
||||
} else if (ObservableMap.class.isAssignableFrom(field.getType())) {
|
||||
Type mapType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), Map.class);
|
||||
if (!(mapType instanceof ParameterizedType))
|
||||
throw new IllegalArgumentException("Cannot resolve the map type of " + field.getName());
|
||||
return new MapField<>(name, alternateNames, varHandle, mapType);
|
||||
} else if (Property.class.isAssignableFrom(field.getType())) {
|
||||
Type propertyType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), Property.class);
|
||||
if (!(propertyType instanceof ParameterizedType))
|
||||
throw new IllegalArgumentException("Cannot resolve the element type of " + field.getName());
|
||||
Type elementType = ((ParameterizedType) propertyType).getActualTypeArguments()[0];
|
||||
return new PropertyField<>(name, alternateNames, varHandle, elementType);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Field " + field.getName() + " is not a property or observable collection");
|
||||
}
|
||||
}
|
||||
|
||||
protected final String serializedName;
|
||||
protected final List<String> alternateNames;
|
||||
protected final VarHandle varHandle;
|
||||
|
||||
private ObservableField(String serializedName, List<String> alternateNames, VarHandle varHandle) {
|
||||
this.serializedName = serializedName;
|
||||
this.alternateNames = alternateNames;
|
||||
this.varHandle = varHandle;
|
||||
}
|
||||
|
||||
public String getSerializedName() {
|
||||
return serializedName;
|
||||
}
|
||||
|
||||
public List<String> getAlternateNames() {
|
||||
return alternateNames;
|
||||
}
|
||||
|
||||
public Observable get(T value) {
|
||||
return (Observable) varHandle.get(value);
|
||||
}
|
||||
|
||||
public abstract void serialize(JsonObject result, T value, JsonSerializationContext context);
|
||||
|
||||
public abstract void deserialize(T value, JsonElement element, JsonDeserializationContext context);
|
||||
|
||||
private static final class PropertyField<T> extends ObservableField<T> {
|
||||
private final Type elementType;
|
||||
|
||||
PropertyField(String serializedName, List<String> alternate, VarHandle varHandle, Type elementType) {
|
||||
super(serializedName, alternate, varHandle);
|
||||
this.elementType = elementType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(JsonObject result, T value, JsonSerializationContext context) {
|
||||
Property<?> property = (Property<?>) get(value);
|
||||
|
||||
if (property instanceof RawPreservingProperty<?> rawPreserving) {
|
||||
JsonElement rawJson = rawPreserving.getRawJson();
|
||||
if (rawJson != null) {
|
||||
result.add(getSerializedName(), rawJson);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
JsonElement serialized = context.serialize(property.getValue(), elementType);
|
||||
if (serialized != null && !serialized.isJsonNull())
|
||||
result.add(getSerializedName(), serialized);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public void deserialize(T value, JsonElement element, JsonDeserializationContext context) {
|
||||
Property property = (Property) get(value);
|
||||
|
||||
try {
|
||||
property.setValue(context.deserialize(element, elementType));
|
||||
} catch (Throwable e) {
|
||||
if (property instanceof RawPreservingProperty<?>) {
|
||||
((RawPreservingProperty<?>) property).setRawJson(element);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CollectionField<T> extends ObservableField<T> {
|
||||
private final Type collectionType;
|
||||
|
||||
/// When deserializing a Set, we first deserialize it into a `List`, then put the elements into the Set.
|
||||
private final Type listType;
|
||||
|
||||
CollectionField(String serializedName, List<String> alternate, VarHandle varHandle,
|
||||
Type collectionType, Type listType) {
|
||||
super(serializedName, alternate, varHandle);
|
||||
this.collectionType = collectionType;
|
||||
this.listType = listType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(JsonObject result, T value, JsonSerializationContext context) {
|
||||
result.add(getSerializedName(), context.serialize(get(value), collectionType));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
public void deserialize(T value, JsonElement element, JsonDeserializationContext context) {
|
||||
List<?> deserialized = context.deserialize(element, listType);
|
||||
Object fieldValue = get(value);
|
||||
|
||||
if (fieldValue instanceof ListProperty) {
|
||||
((ListProperty<Object>) fieldValue).set(FXCollections.observableList((List<Object>) deserialized));
|
||||
} else if (fieldValue instanceof ObservableList) {
|
||||
((ObservableList<Object>) fieldValue).setAll(deserialized);
|
||||
} else if (fieldValue instanceof SetProperty) {
|
||||
((SetProperty<Object>) fieldValue).set(FXCollections.observableSet(new HashSet<>(deserialized)));
|
||||
} else if (fieldValue instanceof ObservableSet) {
|
||||
ObservableSet<Object> set = (ObservableSet<Object>) fieldValue;
|
||||
set.clear();
|
||||
set.addAll(deserialized);
|
||||
} else {
|
||||
throw new JsonParseException("Unsupported field type: " + fieldValue.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MapField<T> extends ObservableField<T> {
|
||||
private final Type mapType;
|
||||
|
||||
MapField(String serializedName, List<String> alternate, VarHandle varHandle, Type mapType) {
|
||||
super(serializedName, alternate, varHandle);
|
||||
this.mapType = mapType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(JsonObject result, T value, JsonSerializationContext context) {
|
||||
result.add(getSerializedName(), context.serialize(get(value), mapType));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
public void deserialize(T config, JsonElement element, JsonDeserializationContext context) {
|
||||
Map<Object, Object> deserialized = context.deserialize(element, mapType);
|
||||
ObservableMap<Object, Object> map = (ObservableMap<Object, Object>) varHandle.get(config);
|
||||
if (map instanceof MapProperty<?, ?>)
|
||||
((MapProperty<Object, Object>) map).set(FXCollections.observableMap(deserialized));
|
||||
else {
|
||||
map.clear();
|
||||
map.putAll(deserialized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
* 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.util.gson;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.MapProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SetProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.collections.ObservableSet;
|
||||
import org.jackhuang.hmcl.util.TypeUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.DirtyTracker;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
|
||||
/// Represents a settings object with multiple [Observable] fields.
|
||||
///
|
||||
/// All instance fields in this object, unless marked as `transient`, are considered observable fields.
|
||||
/// The field types should be subclasses of one of [javafx.beans.property.Property], [javafx.collections.ObservableList], [javafx.collections.ObservableSet], or [javafx.collections.ObservableMap].
|
||||
///
|
||||
/// This class implements the [Observable] interface.
|
||||
/// If any field in a settings object changes, all listeners installed via [#addListener(InvalidationListener)] will be triggered.
|
||||
///
|
||||
/// For each observable field, this object tracks whether it has been modified. When serializing, fields that have never been modified will not be serialized by default.
|
||||
///
|
||||
/// All subclasses of this class must call [#register()] once in their constructor.
|
||||
///
|
||||
/// @author Glavo
|
||||
public abstract class ObservableSetting implements Observable {
|
||||
|
||||
private static final ClassValue<List<? extends ObservableField<?>>> FIELDS = new ClassValue<>() {
|
||||
@Override
|
||||
protected List<? extends ObservableField<?>> computeValue(@NotNull Class<?> type) {
|
||||
if (!ObservableSetting.class.isAssignableFrom(type))
|
||||
throw new AssertionError("Type: " + type);
|
||||
|
||||
try {
|
||||
var allFields = new ArrayList<ObservableField<?>>();
|
||||
|
||||
for (Class<?> current = type;
|
||||
current != ObservableSetting.class;
|
||||
current = current.getSuperclass()) {
|
||||
final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(current, MethodHandles.lookup());
|
||||
|
||||
Field[] fields = current.getDeclaredFields();
|
||||
for (Field field : fields) {
|
||||
int modifiers = field.getModifiers();
|
||||
if (Modifier.isTransient(modifiers) || Modifier.isStatic(modifiers))
|
||||
continue;
|
||||
|
||||
allFields.add(ObservableField.of(lookup, field));
|
||||
}
|
||||
}
|
||||
|
||||
return allFields;
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new ExceptionInInitializerError(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected transient final ObservableHelper helper = new ObservableHelper(this);
|
||||
protected transient final Map<String, JsonElement> unknownFields = new HashMap<>();
|
||||
protected transient final DirtyTracker tracker = new DirtyTracker();
|
||||
|
||||
private boolean registered = false;
|
||||
|
||||
protected final void register() {
|
||||
if (registered)
|
||||
return;
|
||||
|
||||
registered = true;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
var fields = (List<ObservableField<ObservableSetting>>) FIELDS.get(this.getClass());
|
||||
for (var field : fields) {
|
||||
Observable observable = field.get(this);
|
||||
tracker.track(observable);
|
||||
observable.addListener(helper);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addListener(InvalidationListener listener) {
|
||||
helper.addListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeListener(InvalidationListener listener) {
|
||||
helper.removeListener(listener);
|
||||
}
|
||||
|
||||
private static sealed abstract class ObservableField<T> {
|
||||
|
||||
public static <T> ObservableField<T> of(MethodHandles.Lookup lookup, Field field) {
|
||||
String name;
|
||||
List<String> alternateNames;
|
||||
|
||||
SerializedName serializedName = field.getAnnotation(SerializedName.class);
|
||||
if (serializedName == null) {
|
||||
name = field.getName();
|
||||
alternateNames = List.of();
|
||||
} else {
|
||||
name = serializedName.value();
|
||||
alternateNames = List.of(serializedName.alternate());
|
||||
}
|
||||
|
||||
VarHandle varHandle;
|
||||
try {
|
||||
varHandle = lookup.unreflectVarHandle(field);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
|
||||
if (ObservableList.class.isAssignableFrom(field.getType())) {
|
||||
Type listType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), List.class);
|
||||
if (!(listType instanceof ParameterizedType))
|
||||
throw new IllegalArgumentException("Cannot resolve the list type of " + field.getName());
|
||||
return new CollectionField<>(name, alternateNames, varHandle, listType, listType);
|
||||
} else if (ObservableSet.class.isAssignableFrom(field.getType())) {
|
||||
Type setType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), Set.class);
|
||||
if (!(setType instanceof ParameterizedType))
|
||||
throw new IllegalArgumentException("Cannot resolve the set type of " + field.getName());
|
||||
|
||||
ParameterizedType listType = TypeUtils.newParameterizedTypeWithOwner(
|
||||
null,
|
||||
List.class,
|
||||
((ParameterizedType) setType).getActualTypeArguments()[0]
|
||||
);
|
||||
return new CollectionField<>(name, alternateNames, varHandle, setType, listType);
|
||||
} else if (ObservableMap.class.isAssignableFrom(field.getType())) {
|
||||
Type mapType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), Map.class);
|
||||
if (!(mapType instanceof ParameterizedType))
|
||||
throw new IllegalArgumentException("Cannot resolve the map type of " + field.getName());
|
||||
return new MapField<>(name, alternateNames, varHandle, mapType);
|
||||
} else if (Property.class.isAssignableFrom(field.getType())) {
|
||||
Type propertyType = TypeUtils.getSupertype(field.getGenericType(), field.getType(), Property.class);
|
||||
if (!(propertyType instanceof ParameterizedType))
|
||||
throw new IllegalArgumentException("Cannot resolve the element type of " + field.getName());
|
||||
Type elementType = ((ParameterizedType) propertyType).getActualTypeArguments()[0];
|
||||
return new PropertyField<>(name, alternateNames, varHandle, elementType);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Field " + field.getName() + " is not a property or observable collection");
|
||||
}
|
||||
}
|
||||
|
||||
protected final String serializedName;
|
||||
protected final List<String> alternateNames;
|
||||
protected final VarHandle varHandle;
|
||||
|
||||
private ObservableField(String serializedName, List<String> alternateNames, VarHandle varHandle) {
|
||||
this.serializedName = serializedName;
|
||||
this.alternateNames = alternateNames;
|
||||
this.varHandle = varHandle;
|
||||
}
|
||||
|
||||
public String getSerializedName() {
|
||||
return serializedName;
|
||||
}
|
||||
|
||||
public List<String> getAlternateNames() {
|
||||
return alternateNames;
|
||||
}
|
||||
|
||||
public Observable get(T value) {
|
||||
return (Observable) varHandle.get(value);
|
||||
}
|
||||
|
||||
public abstract void serialize(JsonObject result, T value, JsonSerializationContext context);
|
||||
|
||||
public abstract void deserialize(T value, JsonElement element, JsonDeserializationContext context);
|
||||
|
||||
private static final class PropertyField<T> extends ObservableField<T> {
|
||||
private final Type elementType;
|
||||
|
||||
PropertyField(String serializedName, List<String> alternate, VarHandle varHandle, Type elementType) {
|
||||
super(serializedName, alternate, varHandle);
|
||||
this.elementType = elementType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(JsonObject result, T value, JsonSerializationContext context) {
|
||||
Property<?> property = (Property<?>) get(value);
|
||||
|
||||
if (property instanceof RawPreservingProperty<?> rawPreserving) {
|
||||
JsonElement rawJson = rawPreserving.getRawJson();
|
||||
if (rawJson != null) {
|
||||
result.add(getSerializedName(), rawJson);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
JsonElement serialized = context.serialize(property.getValue(), elementType);
|
||||
if (serialized != null && !serialized.isJsonNull())
|
||||
result.add(getSerializedName(), serialized);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public void deserialize(T value, JsonElement element, JsonDeserializationContext context) {
|
||||
Property property = (Property) get(value);
|
||||
|
||||
try {
|
||||
property.setValue(context.deserialize(element, elementType));
|
||||
} catch (Throwable e) {
|
||||
if (property instanceof RawPreservingProperty<?>) {
|
||||
((RawPreservingProperty<?>) property).setRawJson(element);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class CollectionField<T> extends ObservableField<T> {
|
||||
private final Type collectionType;
|
||||
|
||||
/// When deserializing a Set, we first deserialize it into a `List`, then put the elements into the Set.
|
||||
private final Type listType;
|
||||
|
||||
CollectionField(String serializedName, List<String> alternate, VarHandle varHandle,
|
||||
Type collectionType, Type listType) {
|
||||
super(serializedName, alternate, varHandle);
|
||||
this.collectionType = collectionType;
|
||||
this.listType = listType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(JsonObject result, T value, JsonSerializationContext context) {
|
||||
result.add(getSerializedName(), context.serialize(get(value), collectionType));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
public void deserialize(T value, JsonElement element, JsonDeserializationContext context) {
|
||||
List<?> deserialized = context.deserialize(element, listType);
|
||||
Object fieldValue = get(value);
|
||||
|
||||
if (fieldValue instanceof ListProperty) {
|
||||
((ListProperty<Object>) fieldValue).set(FXCollections.observableList((List<Object>) deserialized));
|
||||
} else if (fieldValue instanceof ObservableList) {
|
||||
((ObservableList<Object>) fieldValue).setAll(deserialized);
|
||||
} else if (fieldValue instanceof SetProperty) {
|
||||
((SetProperty<Object>) fieldValue).set(FXCollections.observableSet(new HashSet<>(deserialized)));
|
||||
} else if (fieldValue instanceof ObservableSet) {
|
||||
ObservableSet<Object> set = (ObservableSet<Object>) fieldValue;
|
||||
set.clear();
|
||||
set.addAll(deserialized);
|
||||
} else {
|
||||
throw new JsonParseException("Unsupported field type: " + fieldValue.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MapField<T> extends ObservableField<T> {
|
||||
private final Type mapType;
|
||||
|
||||
MapField(String serializedName, List<String> alternate, VarHandle varHandle, Type mapType) {
|
||||
super(serializedName, alternate, varHandle);
|
||||
this.mapType = mapType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(JsonObject result, T value, JsonSerializationContext context) {
|
||||
result.add(getSerializedName(), context.serialize(get(value), mapType));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Override
|
||||
public void deserialize(T config, JsonElement element, JsonDeserializationContext context) {
|
||||
Map<Object, Object> deserialized = context.deserialize(element, mapType);
|
||||
ObservableMap<Object, Object> map = (ObservableMap<Object, Object>) varHandle.get(config);
|
||||
if (map instanceof MapProperty<?, ?>)
|
||||
((MapProperty<Object, Object>) map).set(FXCollections.observableMap(deserialized));
|
||||
else {
|
||||
map.clear();
|
||||
map.putAll(deserialized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class Adapter<T extends ObservableSetting>
|
||||
implements JsonSerializer<T>, JsonDeserializer<T> {
|
||||
|
||||
protected abstract T createInstance();
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(T setting, Type typeOfSrc, JsonSerializationContext context) {
|
||||
if (setting == null)
|
||||
return JsonNull.INSTANCE;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
var fields = (List<ObservableField<T>>) FIELDS.get(setting.getClass());
|
||||
|
||||
JsonObject result = new JsonObject();
|
||||
for (var field : fields) {
|
||||
Observable observable = field.get(setting);
|
||||
if (setting.tracker.isDirty(observable)) {
|
||||
field.serialize(result, setting, context);
|
||||
}
|
||||
}
|
||||
setting.unknownFields.forEach(result::add);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == null || json.isJsonNull())
|
||||
return null;
|
||||
|
||||
if (!json.isJsonObject())
|
||||
throw new JsonParseException("Config is not an object: " + json);
|
||||
|
||||
T setting = createInstance();
|
||||
@SuppressWarnings("unchecked")
|
||||
var fields = (List<ObservableField<T>>) FIELDS.get(setting.getClass());
|
||||
|
||||
var values = new LinkedHashMap<>(json.getAsJsonObject().asMap());
|
||||
for (ObservableField<T> field : fields) {
|
||||
JsonElement value = values.remove(field.getSerializedName());
|
||||
if (value == null) {
|
||||
for (String alternateName : field.getAlternateNames()) {
|
||||
value = values.remove(alternateName);
|
||||
if (value != null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
setting.tracker.markDirty(field.get(setting));
|
||||
field.deserialize(setting, value, context);
|
||||
}
|
||||
}
|
||||
|
||||
setting.unknownFields.putAll(values);
|
||||
return setting;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// @author Glavo
|
||||
/// @see ObservableSetting
|
||||
public class RawPreservingObjectProperty<T> extends SimpleObjectProperty<T> implements RawPreservingProperty<T> {
|
||||
private JsonElement rawJson;
|
||||
|
||||
|
||||
@@ -144,8 +144,7 @@ public final class NetworkUtils {
|
||||
}
|
||||
connection.setConnectTimeout(TIME_OUT);
|
||||
connection.setReadTimeout(TIME_OUT);
|
||||
if (connection instanceof HttpURLConnection) {
|
||||
var httpConnection = (HttpURLConnection) connection;
|
||||
if (connection instanceof HttpURLConnection httpConnection) {
|
||||
httpConnection.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
|
||||
httpConnection.setInstanceFollowRedirects(false);
|
||||
}
|
||||
@@ -276,7 +275,11 @@ public final class NetworkUtils {
|
||||
}
|
||||
|
||||
public static String doGet(URI uri) throws IOException {
|
||||
return readFullyAsString(resolveConnection(createHttpConnection(uri)));
|
||||
URLConnection connection = createConnection(uri);
|
||||
if (connection instanceof HttpURLConnection httpURLConnection) {
|
||||
connection = resolveConnection(httpURLConnection);
|
||||
}
|
||||
return readFullyAsString(connection);
|
||||
}
|
||||
|
||||
public static String doGet(List<URI> uris) throws IOException {
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.util.platform;
|
||||
|
||||
import org.jackhuang.hmcl.util.KeyValuePairUtils;
|
||||
import org.jackhuang.hmcl.util.platform.windows.Kernel32;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinTypes;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
@@ -150,13 +150,35 @@ public enum OperatingSystem {
|
||||
OSVersion.Windows windowsVersion = null;
|
||||
|
||||
Kernel32 kernel32 = Kernel32.INSTANCE;
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
|
||||
// Get Windows version number
|
||||
if (kernel32 != null) {
|
||||
WinTypes.OSVERSIONINFOEXW osVersionInfo = new WinTypes.OSVERSIONINFOEXW();
|
||||
if (kernel32.GetVersionExW(osVersionInfo)) {
|
||||
windowsVersion = new OSVersion.Windows(osVersionInfo.dwMajorVersion, osVersionInfo.dwMinorVersion, osVersionInfo.dwBuildNumber);
|
||||
} else
|
||||
System.err.println("Failed to obtain OS version number (" + kernel32.GetLastError() + ")");
|
||||
if (reg != null) {
|
||||
var baseVersion = OSVersion.Windows.parse(System.getProperty("os.version"));
|
||||
int majorVersion = baseVersion.major();
|
||||
int minorVersion = baseVersion.minor();
|
||||
int buildNumber = baseVersion.build();
|
||||
int revision = baseVersion.revision();
|
||||
|
||||
Object currentBuild = reg.queryValue(WinReg.HKEY.HKEY_LOCAL_MACHINE,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "CurrentBuild");
|
||||
if (currentBuild instanceof String currentBuildStr) {
|
||||
try {
|
||||
buildNumber = Integer.parseInt(currentBuildStr);
|
||||
} catch (NumberFormatException e) {
|
||||
System.err.println("Invalid Windows build number: " + currentBuildStr);
|
||||
}
|
||||
}
|
||||
|
||||
if (majorVersion >= 10) {
|
||||
Object ubr = reg.queryValue(WinReg.HKEY.HKEY_LOCAL_MACHINE,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "UBR");
|
||||
|
||||
if (ubr instanceof Integer ubrValue)
|
||||
revision = ubrValue;
|
||||
}
|
||||
|
||||
windowsVersion = new OSVersion.Windows(majorVersion, minorVersion, buildNumber, revision);
|
||||
}
|
||||
|
||||
if (windowsVersion == null) {
|
||||
@@ -201,6 +223,16 @@ public enum OperatingSystem {
|
||||
if (osName.equals("Windows 10") && windowsVersion.isAtLeast(OSVersion.WINDOWS_11))
|
||||
osName = "Windows 11";
|
||||
|
||||
if (windowsVersion.isAtLeast(OSVersion.WINDOWS_10) && reg != null) {
|
||||
Object displayVersion = reg.queryValue(WinReg.HKEY.HKEY_LOCAL_MACHINE,
|
||||
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "DisplayVersion");
|
||||
|
||||
if (displayVersion instanceof String displayVersionStr
|
||||
&& displayVersionStr.matches("\\d{2}H\\d")) {
|
||||
osName = osName + " " + displayVersionStr;
|
||||
}
|
||||
}
|
||||
|
||||
SYSTEM_NAME = osName;
|
||||
SYSTEM_VERSION = windowsVersion;
|
||||
SYSTEM_BUILD_NUMBER = windowsVersion.build();
|
||||
|
||||
@@ -100,7 +100,7 @@ public abstract sealed class GameVersionNumber implements Comparable<GameVersion
|
||||
}
|
||||
|
||||
public boolean isAprilFools() {
|
||||
if (this instanceof Special)
|
||||
if (this instanceof Special && !value.endsWith("_unobfuscated"))
|
||||
return true;
|
||||
|
||||
if (this instanceof Snapshot snapshot) {
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"id": "25w45a_unobfuscated",
|
||||
"type" : "unobfuscated",
|
||||
"url": "https://piston-meta.mojang.com/v1/packages/7a3c149f148b6aa5ac3af48c4f701adea7e5b615/25w45a_unobfuscated.json",
|
||||
"time" : "2025-11-04T14:07:08+00:00",
|
||||
"releaseTime" : "2025-11-04T14:07:08+00:00"
|
||||
},
|
||||
{
|
||||
"id": "1.19_deep_dark_experimental_snapshot-1",
|
||||
"type": "pending",
|
||||
|
||||
@@ -848,4 +848,10 @@
|
||||
1.21.9-rc1
|
||||
1.21.9
|
||||
1.21.10-rc1
|
||||
1.21.10
|
||||
1.21.10
|
||||
25w41a
|
||||
25w42a
|
||||
25w43a
|
||||
25w44a
|
||||
25w45a
|
||||
25w45a_unobfuscated
|
||||
@@ -121,6 +121,7 @@ public final class GameVersionNumberTest {
|
||||
assertFalse(isAprilFools("25w21a"));
|
||||
assertFalse(isAprilFools("13w12~"));
|
||||
assertFalse(isAprilFools("15w14b"));
|
||||
assertFalse(isAprilFools("25w45a_unobfuscated"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -272,6 +273,8 @@ public final class GameVersionNumberTest {
|
||||
"24w13a",
|
||||
"24w14potato",
|
||||
"24w14a",
|
||||
"25w45a",
|
||||
"25w45a_unobfuscated",
|
||||
"Unknown",
|
||||
"100.0"
|
||||
);
|
||||
|
||||
@@ -58,19 +58,23 @@ public abstract class ParseModDataTask extends DefaultTask {
|
||||
private static final String MOD_SEPARATOR = ",";
|
||||
|
||||
private static final Pattern[] CURSEFORGE_PATTERNS = {
|
||||
Pattern.compile("^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|data-packs|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons)/+(?<modid>[\\w-]+)(/(.*?))?$"),
|
||||
Pattern.compile("^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|data-packs|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons|scripts)/+(?<modid>[\\w-]+)(/(.*?))?$"),
|
||||
Pattern.compile("^/projects/(?<modid>[\\w-]+)(/(.*?))?$"),
|
||||
Pattern.compile("^/mc-mods/minecraft/(?<modid>[\\w-]+)(/(.*?))?$"),
|
||||
Pattern.compile("^/legacy/mc-mods/minecraft/(\\d+)-(?<modid>[\\w-]+)"),
|
||||
};
|
||||
|
||||
private static String parseCurseforge(String url) {
|
||||
URI res = URI.create(url);
|
||||
URI res = URI.create(url.replace(" ", "%20"));
|
||||
|
||||
if (!"http".equals(res.getScheme()) && !"https".equals(res.getScheme())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if ("edge.forgecdn.net".equals(res.getHost())) {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (Pattern pattern : CURSEFORGE_PATTERNS) {
|
||||
Matcher matcher = pattern.matcher(res.getPath());
|
||||
if (matcher.matches()) {
|
||||
|
||||
Reference in new issue
Block a user