Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1605eb1775 | ||
|
|
94ccee0b76 | ||
|
|
8b94fe5b0b | ||
|
|
abec1abcff | ||
|
|
127938fa38 | ||
|
|
1187ae4f04 | ||
|
|
8efdc536ee | ||
|
|
78f080b6cc | ||
|
|
cc8bcfd924 | ||
|
|
806ca3f64a | ||
|
|
4a16fa13ee | ||
|
|
b88d7c4fd2 | ||
|
|
c0838468b1 | ||
|
|
90cfe3d0b7 | ||
|
|
baf1d22bc4 | ||
|
|
7ca5744559 | ||
|
|
57e728991b | ||
|
|
e08ba4d421 |
No files matched your search
@@ -27,7 +27,7 @@ S = ';'
|
||||
MOD_SEPARATOR = ','
|
||||
|
||||
CURSEFORGE_PATTERN1 = re.compile(
|
||||
r'^/minecraft/(mc-mods|modpacks|customization|mc-addons|customization/configuration)/+(?P<modid>[\w-]+)(/(.*?))?$')
|
||||
r'^/(minecraft|Minecraft|minecraft-bedrock)/(mc-mods|modpacks|customization|mc-addons|texture-packs|customization/configuration|addons)/+(?P<modid>[\w-]+)(/(.*?))?$')
|
||||
CURSEFORGE_PATTERN2 = re.compile(
|
||||
r'^/projects/(?P<modid>[\w-]+)(/(.*?))?$')
|
||||
CURSEFORGE_PATTERN3 = re.compile(
|
||||
@@ -58,30 +58,6 @@ def parseMcmod(url):
|
||||
return match.group('modid')
|
||||
return ''
|
||||
|
||||
|
||||
MCBBS_HTML_PATTERN = re.compile(r'/+thread-(?P<modid>\d+)-(\d+)-(\d+).html')
|
||||
MCBBS_PHP_PATTERN = re.compile(r'')
|
||||
|
||||
|
||||
def parseMcbbs(url):
|
||||
res = urlparse(url)
|
||||
if res.scheme not in ['http', 'https']:
|
||||
return ''
|
||||
if res.netloc != 'www.mcbbs.net':
|
||||
return ''
|
||||
match = MCBBS_HTML_PATTERN.match(res.path)
|
||||
if match:
|
||||
return match.group('modid')
|
||||
|
||||
query = parse_qs(res.query)
|
||||
|
||||
if res.path == '/forum.php':
|
||||
if 'mod' in query and 'tid' in query and query['mod'] == ['viewthread']:
|
||||
return query['tid']
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
skip = [
|
||||
'Minecraft',
|
||||
'The Building Game'
|
||||
@@ -111,7 +87,7 @@ if __name__ == '__main__':
|
||||
exit(1)
|
||||
|
||||
black_lists = [
|
||||
'Master Chef'
|
||||
'Wood Converter'
|
||||
]
|
||||
|
||||
curseforge_id = ''
|
||||
@@ -131,11 +107,6 @@ if __name__ == '__main__':
|
||||
if mcmod_id == '':
|
||||
print('Error mcmod ' + chinese_name)
|
||||
exit(1)
|
||||
if 'mcbbs' in links and links['mcbbs']:
|
||||
mcbbs_id = parseMcbbs(links['mcbbs'][0]['url'])
|
||||
if mcbbs_id == '':
|
||||
print('Error mcbbs ' + chinese_name)
|
||||
exit(1)
|
||||
|
||||
mod_id = []
|
||||
if 'modid' in mod and 'list' in mod['modid']:
|
||||
@@ -148,6 +119,6 @@ if __name__ == '__main__':
|
||||
mod_ids = MOD_SEPARATOR.join([str(id) for id in mod_id])
|
||||
|
||||
outfile.write(
|
||||
f'{curseforge_id}{S}{mcmod_id}{S}{mcbbs_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
|
||||
f'{curseforge_id}{S}{mcmod_id}{S}{mod_ids}{S}{chinese_name}{S}{sub_name}{S}{abbr}\n')
|
||||
|
||||
print('Success!')
|
||||
@@ -12,14 +12,10 @@ import javafx.scene.transform.Rotate;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.scene.transform.Translate;
|
||||
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class SkinCanvas extends Group {
|
||||
|
||||
public static final Image ALEX = FXUtils.newBuiltinImage("/assets/img/skin/alex.png");
|
||||
public static final Image STEVE = FXUtils.newBuiltinImage("/assets/img/skin/steve.png");
|
||||
|
||||
public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true);
|
||||
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
|
||||
|
||||
@@ -152,10 +148,6 @@ public class SkinCanvas extends Group {
|
||||
capeGroup.setVisible(hasCape);
|
||||
}
|
||||
|
||||
public SkinCanvas(double preW, double preH) {
|
||||
this(STEVE, preW, preH, true);
|
||||
}
|
||||
|
||||
public SkinCanvas(Image skin, double preW, double preH, boolean msaa) {
|
||||
this.skin = skin;
|
||||
this.preW = preW;
|
||||
|
||||
@@ -25,9 +25,6 @@ import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
@@ -53,10 +50,8 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@@ -71,8 +66,6 @@ public final class Launcher extends Application {
|
||||
|
||||
CookieHandler.setDefault(COOKIE_MANAGER);
|
||||
|
||||
register();
|
||||
|
||||
LOG.info("JavaFX Version: " + System.getProperty("javafx.runtime.version"));
|
||||
try {
|
||||
Object pipeline = Class.forName("com.sun.prism.GraphicsPipeline").getMethod("getPipeline").invoke(null);
|
||||
@@ -128,45 +121,6 @@ public final class Launcher extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private static void register() {
|
||||
Skin.registerDefaultSkinLoader((type) -> {
|
||||
switch (type) {
|
||||
case ALEX:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/alex.png");
|
||||
case ARI:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/ari.png");
|
||||
case EFE:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/efe.png");
|
||||
case KAI:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/kai.png");
|
||||
case MAKENA:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/makena.png");
|
||||
case NOOR:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/noor.png");
|
||||
case STEVE:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/steve.png");
|
||||
case SUNNY:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/sunny.png");
|
||||
case ZURI:
|
||||
return Skin.class.getResourceAsStream("/assets/img/skin/zuri.png");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
RemoteMod.registerEmptyRemoteMod(new RemoteMod("", "", i18n("mods.broken_dependency.title"), i18n("mods.broken_dependency.desc"), new ArrayList<>(), "", "/assets/img/icon@8x.png", new RemoteMod.IMod() {
|
||||
@Override
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private static ButtonType showAlert(AlertType alertType, String contentText, ButtonType... buttons) {
|
||||
return new Alert(alertType, contentText, buttons).showAndWait().orElse(null);
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ public final class Main {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.setProperty("java.net.useSystemProxies", "true");
|
||||
System.setProperty("javafx.autoproxy.disable", "true");
|
||||
System.getProperties().putIfAbsent("java.net.useSystemProxies", "true");
|
||||
System.getProperties().putIfAbsent("javafx.autoproxy.disable", "true");
|
||||
System.getProperties().putIfAbsent("http.agent", "HMCL/" + Metadata.VERSION);
|
||||
|
||||
checkDirectoryPath();
|
||||
|
||||
@@ -29,7 +29,6 @@ import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -72,7 +71,7 @@ public final class HMCLGameLauncher extends DefaultLauncher {
|
||||
}
|
||||
}
|
||||
|
||||
if (I18n.getCurrentLocale().getLocale() != Locale.CHINA) {
|
||||
if (!I18n.isUseChinese()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -259,8 +259,50 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
return vs;
|
||||
}
|
||||
|
||||
public File getVersionIconFile(String id) {
|
||||
return new File(getVersionRoot(id), "icon.png");
|
||||
public Optional<File> getVersionIconFile(String id) {
|
||||
File root = getVersionRoot(id);
|
||||
|
||||
File iconFile = new File(root, "icon.png");
|
||||
if (iconFile.exists()) {
|
||||
return Optional.of(iconFile);
|
||||
}
|
||||
|
||||
iconFile = new File(root, "icon.jpg");
|
||||
if (iconFile.exists()) {
|
||||
return Optional.of(iconFile);
|
||||
}
|
||||
|
||||
iconFile = new File(root, "icon.bmp");
|
||||
if (iconFile.exists()) {
|
||||
return Optional.of(iconFile);
|
||||
}
|
||||
|
||||
iconFile = new File(root, "icon.gif");
|
||||
if (iconFile.exists()) {
|
||||
return Optional.of(iconFile);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public void setVersionIconFile(String id, File iconFile) throws IOException {
|
||||
String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT);
|
||||
if (!ext.equals("png") && !ext.equals("jpg") && !ext.equals("bmp") && !ext.equals("gif")) {
|
||||
throw new IllegalArgumentException("Unsupported icon file: " + ext);
|
||||
}
|
||||
|
||||
deleteIconFile(id);
|
||||
|
||||
FileUtils.copyFile(iconFile, new File(getVersionRoot(id), "icon." + ext));
|
||||
}
|
||||
|
||||
public void deleteIconFile(String id) {
|
||||
File root = getVersionRoot(id);
|
||||
|
||||
new File(root, "icon.png").delete();
|
||||
new File(root, "icon.jpg").delete();
|
||||
new File(root, "icon.bmp").delete();
|
||||
new File(root, "icon.gif").delete();
|
||||
}
|
||||
|
||||
public Image getVersionIconImage(String id) {
|
||||
@@ -272,9 +314,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
|
||||
if (iconType == VersionIconType.DEFAULT) {
|
||||
Version version = getVersion(id).resolve(this);
|
||||
File iconFile = getVersionIconFile(id);
|
||||
if (iconFile.exists()) {
|
||||
try (InputStream inputStream = new FileInputStream(iconFile)) {
|
||||
Optional<File> iconFile = getVersionIconFile(id);
|
||||
if (iconFile.isPresent()) {
|
||||
try (InputStream inputStream = new FileInputStream(iconFile.get())) {
|
||||
return new Image(inputStream);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to load version icon of " + id, e);
|
||||
|
||||
@@ -333,7 +333,8 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
private static Task<JavaVersion> checkGameState(Profile profile, VersionSetting setting, Version version) {
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(profile.getRepository().getGameVersion(version));
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, profile.getRepository().getGameVersion(version).orElse(null));
|
||||
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT));
|
||||
|
||||
if (setting.isNotCheckJVM()) {
|
||||
return Task.composeAsync(() -> setting.getJavaVersion(gameVersion, version))
|
||||
@@ -417,7 +418,6 @@ public final class LauncherHelper {
|
||||
JavaVersionConstraint violatedMandatoryConstraint = null;
|
||||
List<JavaVersionConstraint> violatedSuggestedConstraints = null;
|
||||
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, gameVersion.toString());
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, javaVersion, analyzer)) {
|
||||
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
|
||||
|
||||
@@ -140,23 +140,35 @@ public final class TexturesLoader {
|
||||
// ====
|
||||
|
||||
// ==== Skins ====
|
||||
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
|
||||
private static final String[] DEFAULT_SKINS = {"alex", "ari", "efe", "kai", "makena", "noor", "steve", "sunny", "zuri"};
|
||||
|
||||
static {
|
||||
loadDefaultSkin("/assets/img/skin/steve.png", TextureModel.STEVE);
|
||||
loadDefaultSkin("/assets/img/skin/alex.png", TextureModel.ALEX);
|
||||
public static Image getDefaultSkinImage() {
|
||||
return FXUtils.newBuiltinImage("/assets/img/skin/wide/steve.png");
|
||||
}
|
||||
|
||||
private static void loadDefaultSkin(String path, TextureModel model) {
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(FXUtils.newBuiltinImage(path), singletonMap("model", model.modelName)));
|
||||
public static LoadedTexture getDefaultSkin(UUID uuid) {
|
||||
int idx = Math.floorMod(uuid.hashCode(), DEFAULT_SKINS.length * 2);
|
||||
TextureModel model;
|
||||
Image skin;
|
||||
if (idx < DEFAULT_SKINS.length) {
|
||||
model = TextureModel.SLIM;
|
||||
skin = FXUtils.newBuiltinImage("/assets/img/skin/slim/" + DEFAULT_SKINS[idx] + ".png");
|
||||
} else {
|
||||
model = TextureModel.WIDE;
|
||||
skin = FXUtils.newBuiltinImage("/assets/img/skin/wide/" + DEFAULT_SKINS[idx - DEFAULT_SKINS.length] + ".png");
|
||||
}
|
||||
|
||||
return new LoadedTexture(skin, singletonMap("model", model.modelName));
|
||||
}
|
||||
|
||||
public static LoadedTexture getDefaultSkin(TextureModel model) {
|
||||
return DEFAULT_SKINS.get(model);
|
||||
public static TextureModel getDefaultModel(UUID uuid) {
|
||||
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).getMetadata().get("model"))
|
||||
? TextureModel.WIDE
|
||||
: TextureModel.SLIM;
|
||||
}
|
||||
|
||||
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(uuid));
|
||||
LoadedTexture uuidFallback = getDefaultSkin(uuid);
|
||||
return BindingMapping.of(service.getProfileRepository().binding(uuid))
|
||||
.map(profile -> profile
|
||||
.flatMap(it -> {
|
||||
@@ -187,7 +199,7 @@ public final class TexturesLoader {
|
||||
}
|
||||
|
||||
public static ObservableValue<LoadedTexture> skinBinding(Account account) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(account.getUUID()));
|
||||
LoadedTexture uuidFallback = getDefaultSkin(account.getUUID());
|
||||
if (account instanceof OfflineAccount) {
|
||||
OfflineAccount offlineAccount = (OfflineAccount) account;
|
||||
|
||||
@@ -348,7 +360,7 @@ public final class TexturesLoader {
|
||||
fxAvatarBinding(canvas, skinBinding(account));
|
||||
else {
|
||||
unbindAvatar(canvas);
|
||||
drawAvatar(canvas, getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image);
|
||||
drawAvatar(canvas, getDefaultSkin(account.getUUID()).image);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import javafx.scene.control.Tooltip;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
@@ -58,7 +57,7 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
tooltip.setText(i18n("account.create"));
|
||||
|
||||
TexturesLoader.unbindAvatar(canvas);
|
||||
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage());
|
||||
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkinImage());
|
||||
|
||||
} else {
|
||||
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
|
||||
|
||||
@@ -25,7 +25,6 @@ import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.*;
|
||||
@@ -35,6 +34,7 @@ import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.ui.construct.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
@@ -71,7 +72,7 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
|
||||
BorderPane pane = new BorderPane();
|
||||
|
||||
SkinCanvas canvas = new SkinCanvas(SkinCanvas.STEVE, 300, 300, true);
|
||||
SkinCanvas canvas = new SkinCanvas(TexturesLoader.getDefaultSkinImage(), 300, 300, true);
|
||||
StackPane canvasPane = new StackPane(canvas);
|
||||
canvasPane.setPrefWidth(300);
|
||||
canvasPane.setPrefHeight(300);
|
||||
@@ -116,11 +117,11 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
));
|
||||
|
||||
modelCombobox.setConverter(stringConverter(model -> i18n("account.skin.model." + model.modelName)));
|
||||
modelCombobox.getItems().setAll(TextureModel.STEVE, TextureModel.ALEX);
|
||||
modelCombobox.getItems().setAll(TextureModel.WIDE, TextureModel.SLIM);
|
||||
|
||||
if (account.getSkin() == null) {
|
||||
skinItem.setSelectedData(Skin.Type.DEFAULT);
|
||||
modelCombobox.setValue(TextureModel.STEVE);
|
||||
modelCombobox.setValue(TextureModel.WIDE);
|
||||
} else {
|
||||
skinItem.setSelectedData(account.getSkin().getType());
|
||||
cslApiField.setText(account.getSkin().getCslApi());
|
||||
@@ -136,13 +137,18 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
LOG.log(Level.WARNING, "Failed to load skin", exception);
|
||||
Controllers.showToast(i18n("message.failed"));
|
||||
} else {
|
||||
UUID uuid = this.account.getUUID();
|
||||
if (result == null || result.getSkin() == null && result.getCape() == null) {
|
||||
canvas.updateSkin(getDefaultTexture(), isDefaultSlim(), null);
|
||||
canvas.updateSkin(
|
||||
TexturesLoader.getDefaultSkin(uuid).getImage(),
|
||||
TexturesLoader.getDefaultModel(uuid) == TextureModel.SLIM,
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
canvas.updateSkin(
|
||||
result.getSkin() != null ? result.getSkin().getImage() : getDefaultTexture(),
|
||||
result.getModel() == TextureModel.ALEX,
|
||||
result.getSkin() != null ? result.getSkin().getImage() : TexturesLoader.getDefaultSkin(uuid).getImage(),
|
||||
result.getModel() == TextureModel.SLIM,
|
||||
result.getCape() != null ? result.getCape().getImage() : null);
|
||||
}
|
||||
}).start();
|
||||
@@ -198,17 +204,4 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
private Skin getSkin() {
|
||||
return new Skin(skinItem.getSelectedData(), cslApiField.getText(), modelCombobox.getValue(), skinSelector.getValue(), capeSelector.getValue());
|
||||
}
|
||||
|
||||
private boolean isDefaultSlim() {
|
||||
return TextureModel.detectUUID(account.getUUID()) == TextureModel.ALEX;
|
||||
}
|
||||
|
||||
private Image getDefaultTexture() {
|
||||
if (isDefaultSlim()) {
|
||||
return SkinCanvas.ALEX;
|
||||
} else {
|
||||
return SkinCanvas.STEVE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -50,6 +50,10 @@ import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -168,8 +172,23 @@ public class DecoratorController {
|
||||
break;
|
||||
case NETWORK:
|
||||
String backgroundImageUrl = config().getBackgroundImageUrl();
|
||||
if (backgroundImageUrl != null && NetworkUtils.isURL(backgroundImageUrl))
|
||||
image = tryLoadImage(backgroundImageUrl).orElse(null);
|
||||
if (backgroundImageUrl != null) {
|
||||
try {
|
||||
URLConnection connection = NetworkUtils.createConnection(new URL(backgroundImageUrl));
|
||||
if (connection instanceof HttpURLConnection) {
|
||||
connection = NetworkUtils.resolveConnection((HttpURLConnection) connection);
|
||||
}
|
||||
|
||||
try (InputStream input = connection.getInputStream()) {
|
||||
image = new Image(input);
|
||||
if (image.isError()) {
|
||||
throw image.getException();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.log(WARNING, "Couldn't load background image", e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CLASSIC:
|
||||
image = newBuiltinImage("/assets/img/background-classic.jpg");
|
||||
|
||||
@@ -179,7 +179,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
|
||||
if (skinnable.controller.getSettings().get(MODPACK_TYPE) == MODPACK_TYPE_SERVER) {
|
||||
Hyperlink hyperlink = new Hyperlink(i18n("modpack.wizard.step.initialization.server"));
|
||||
hyperlink.setOnMouseClicked(e -> {
|
||||
FXUtils.openLink("https://docs.hmcl.net/modpack/serverpack.htm");
|
||||
FXUtils.openLink("https://docs.hmcl.net/modpack/serverpack.html");
|
||||
});
|
||||
borderPane.setTop(hyperlink);
|
||||
} else {
|
||||
|
||||
@@ -177,7 +177,7 @@ public abstract class SettingsView extends StackPane {
|
||||
languagePane.setLeft(left);
|
||||
|
||||
cboLanguage = new JFXComboBox<>();
|
||||
cboLanguage.setConverter(stringConverter(locale -> locale.getName(I18n.getCurrentLocale().getResourceBundle())));
|
||||
cboLanguage.setConverter(stringConverter(I18n::getName));
|
||||
FXUtils.setLimitWidth(cboLanguage, 300);
|
||||
languagePane.setRight(cboLanguage);
|
||||
|
||||
|
||||
@@ -477,7 +477,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
|
||||
protected void updateControl(RemoteMod dataItem, boolean empty) {
|
||||
if (empty) return;
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(getSkinnable().repository.getType()).getModByCurseForgeId(dataItem.getSlug());
|
||||
content.setTitle(mod != null && I18n.getCurrentLocale().getLocale() == Locale.CHINA ? mod.getDisplayName() : dataItem.getTitle());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : dataItem.getTitle());
|
||||
content.setSubtitle(dataItem.getDescription());
|
||||
content.getTags().setAll(dataItem.getCategories().stream()
|
||||
.map(category -> getSkinnable().getLocalizedCategory(category))
|
||||
|
||||
@@ -240,7 +240,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
ModTranslations.Mod mod = getSkinnable().translations.getModByCurseForgeId(getSkinnable().addon.getSlug());
|
||||
content.setTitle(mod != null && I18n.getCurrentLocale().getLocale() == Locale.CHINA ? mod.getDisplayName() : getSkinnable().addon.getTitle());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : getSkinnable().addon.getTitle());
|
||||
content.setSubtitle(getSkinnable().addon.getDescription());
|
||||
content.getTags().setAll(getSkinnable().addon.getCategories().stream()
|
||||
.map(category -> getSkinnable().page.getLocalizedCategory(category))
|
||||
@@ -352,15 +352,21 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
container.setOnMouseClicked(e -> Controllers.navigate(new DownloadPage(page, addon, version, callback)));
|
||||
getChildren().setAll(container);
|
||||
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
|
||||
content.setTitle(mod != null && I18n.getCurrentLocale().getLocale() == Locale.CHINA ? mod.getDisplayName() : addon.getTitle());
|
||||
content.setSubtitle(addon.getDescription());
|
||||
content.getTags().setAll(addon.getCategories().stream()
|
||||
.map(page::getLocalizedCategory)
|
||||
.collect(Collectors.toList()));
|
||||
if (addon != RemoteMod.BROKEN) {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
|
||||
content.setTitle(mod != null && I18n.isUseChinese() ? mod.getDisplayName() : addon.getTitle());
|
||||
content.setSubtitle(addon.getDescription());
|
||||
content.getTags().setAll(addon.getCategories().stream()
|
||||
.map(page::getLocalizedCategory)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
if (StringUtils.isNotBlank(addon.getIconUrl())) {
|
||||
imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true));
|
||||
if (StringUtils.isNotBlank(addon.getIconUrl())) {
|
||||
imageView.setImage(FXUtils.newRemoteImage(addon.getIconUrl(), 40, 40, true, true, true));
|
||||
}
|
||||
} else {
|
||||
content.setTitle(i18n("mods.broken_dependency.title"));
|
||||
content.setSubtitle(i18n("mods.broken_dependency.desc"));
|
||||
imageView.setImage(FXUtils.newBuiltinImage("/assets/img/icon@8x.png", 40, 40, true, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,10 +490,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
content.getTags().add(i18n("install.installer.quilt"));
|
||||
break;
|
||||
}
|
||||
if (dataItem.getMod() != null) {
|
||||
if (I18n.getCurrentLocale().getLocale() == Locale.CHINA) {
|
||||
content.getTags().add(dataItem.getMod().getDisplayName());
|
||||
}
|
||||
if (dataItem.getMod() != null && I18n.isUseChinese()) {
|
||||
content.getTags().add(dataItem.getMod().getDisplayName());
|
||||
}
|
||||
content.setSubtitle(dataItem.getSubtitle());
|
||||
if (booleanProperty != null) {
|
||||
|
||||
@@ -198,10 +198,9 @@ public enum ModTranslations {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static class Mod {
|
||||
public static final class Mod {
|
||||
private final String curseforge;
|
||||
private final String mcmod;
|
||||
private final String mcbbs;
|
||||
private final List<String> modIds;
|
||||
private final String name;
|
||||
private final String subname;
|
||||
@@ -209,23 +208,21 @@ public enum ModTranslations {
|
||||
|
||||
public Mod(String line) {
|
||||
String[] items = line.split(";", -1);
|
||||
if (items.length != 7) {
|
||||
throw new IllegalArgumentException("Illegal mod data line, 7 items expected " + line);
|
||||
if (items.length != 6) {
|
||||
throw new IllegalArgumentException("Illegal mod data line, 6 items expected " + line);
|
||||
}
|
||||
|
||||
curseforge = items[0];
|
||||
mcmod = items[1];
|
||||
mcbbs = items[2];
|
||||
modIds = Collections.unmodifiableList(Arrays.asList(items[3].split(",")));
|
||||
name = items[4];
|
||||
subname = items[5];
|
||||
abbr = items[6];
|
||||
modIds = Collections.unmodifiableList(Arrays.asList(items[2].split(",")));
|
||||
name = items[3];
|
||||
subname = items[4];
|
||||
abbr = items[5];
|
||||
}
|
||||
|
||||
public Mod(String curseforge, String mcmod, String mcbbs, List<String> modIds, String name, String subname, String abbr) {
|
||||
public Mod(String curseforge, String mcmod, List<String> modIds, String name, String subname, String abbr) {
|
||||
this.curseforge = curseforge;
|
||||
this.mcmod = mcmod;
|
||||
this.mcbbs = mcbbs;
|
||||
this.modIds = modIds;
|
||||
this.name = name;
|
||||
this.subname = subname;
|
||||
@@ -252,10 +249,6 @@ public enum ModTranslations {
|
||||
return mcmod;
|
||||
}
|
||||
|
||||
public String getMcbbs() {
|
||||
return mcbbs;
|
||||
}
|
||||
|
||||
public List<String> getModIds() {
|
||||
return modIds;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@ import org.jackhuang.hmcl.game.LocalizedRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.RemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseForgeRemoteModRepository;
|
||||
import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ResourcePackDownloadListPage extends DownloadListPage {
|
||||
@@ -66,10 +71,18 @@ public class ResourcePackDownloadListPage extends DownloadListPage {
|
||||
|
||||
@Override
|
||||
protected String getLocalizedCategory(String category) {
|
||||
String key;
|
||||
if ("mods.modrinth".equals(downloadSource.get())) {
|
||||
return i18n("modrinth.category." + category);
|
||||
key = "modrinth.category." + category;
|
||||
} else {
|
||||
return i18n("curse.category." + category);
|
||||
key = "curse.category." + category;
|
||||
}
|
||||
|
||||
try {
|
||||
return I18n.getResourceBundle().getString(key);
|
||||
} catch (MissingResourceException e) {
|
||||
LOG.log(Level.WARNING, "Cannot find key " + key + " in resource bundle", e);
|
||||
return category;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -74,20 +73,19 @@ public class VersionIconDialog extends DialogPane {
|
||||
|
||||
private void exploreIcon() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.png"), "*.png", "*.jpg", "*.bmp", "*.gif"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile != null) {
|
||||
File iconFile = profile.getRepository().getVersionIconFile(versionId);
|
||||
try {
|
||||
FileUtils.copyFile(selectedFile, iconFile);
|
||||
profile.getRepository().setVersionIconFile(versionId, selectedFile);
|
||||
|
||||
if (vs != null) {
|
||||
vs.setVersionIcon(VersionIconType.DEFAULT);
|
||||
}
|
||||
|
||||
onAccept();
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to copy icon file from " + selectedFile + " to " + iconFile, e);
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to set icon file: " + selectedFile, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +164,9 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
private void clearAssets() {
|
||||
FileUtils.deleteDirectoryQuietly(new File(getProfile().getRepository().getBaseDirectory(), "assets"));
|
||||
File baseDirectory = getProfile().getRepository().getBaseDirectory();
|
||||
FileUtils.deleteDirectoryQuietly(new File(baseDirectory, "assets"));
|
||||
FileUtils.deleteDirectoryQuietly(new File(baseDirectory, "resources"));
|
||||
}
|
||||
|
||||
private void clearJunkFiles() {
|
||||
|
||||
@@ -48,7 +48,6 @@ import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
@@ -642,9 +641,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
if (versionId == null)
|
||||
return;
|
||||
|
||||
File iconFile = profile.getRepository().getVersionIconFile(versionId);
|
||||
if (iconFile.exists())
|
||||
iconFile.delete();
|
||||
profile.getRepository().deleteIconFile(versionId);
|
||||
VersionSetting localVersionSetting = profile.getRepository().getLocalVersionSettingOrCreate(versionId);
|
||||
if (localVersionSetting != null) {
|
||||
localVersionSetting.setVersionIcon(VersionIconType.DEFAULT);
|
||||
|
||||
@@ -20,10 +20,9 @@ package org.jackhuang.hmcl.util.i18n;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.IllegalFormatException;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@@ -34,17 +33,25 @@ public final class I18n {
|
||||
}
|
||||
|
||||
private static volatile SupportedLocale locale = Locales.DEFAULT;
|
||||
private static volatile ResourceBundle resourceBundle = locale.getResourceBundle();
|
||||
private static volatile DateTimeFormatter dateTimeFormatter;
|
||||
|
||||
public static void setLocale(SupportedLocale locale) {
|
||||
I18n.locale = locale;
|
||||
resourceBundle = locale.getResourceBundle();
|
||||
dateTimeFormatter = null;
|
||||
}
|
||||
|
||||
public static SupportedLocale getCurrentLocale() {
|
||||
return locale;
|
||||
public static boolean isUseChinese() {
|
||||
return locale.getLocale() == Locale.CHINA;
|
||||
}
|
||||
|
||||
public static ResourceBundle getResourceBundle() {
|
||||
return getCurrentLocale().getResourceBundle();
|
||||
return resourceBundle;
|
||||
}
|
||||
|
||||
public static String getName(SupportedLocale locale) {
|
||||
return locale == Locales.DEFAULT ? resourceBundle.getString("lang.default") : locale.getResourceBundle().getString("lang");
|
||||
}
|
||||
|
||||
public static String i18n(String key, Object... formatArgs) {
|
||||
@@ -69,7 +76,12 @@ public final class I18n {
|
||||
}
|
||||
|
||||
public static String formatDateTime(Instant instant) {
|
||||
return getCurrentLocale().getDateTimeFormatter().format(instant);
|
||||
DateTimeFormatter formatter = dateTimeFormatter;
|
||||
if (formatter == null) {
|
||||
formatter = dateTimeFormatter = DateTimeFormatter.ofPattern(getResourceBundle().getString("world.time")).withZone(ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
return formatter.format(instant);
|
||||
}
|
||||
|
||||
public static boolean hasKey(String key) {
|
||||
|
||||
@@ -24,8 +24,6 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
@@ -34,7 +32,7 @@ public final class Locales {
|
||||
private Locales() {
|
||||
}
|
||||
|
||||
public static final SupportedLocale DEFAULT = new SupportedLocale(Locale.getDefault(), "lang.default");
|
||||
public static final SupportedLocale DEFAULT = new SupportedLocale(Locale.getDefault());
|
||||
|
||||
/**
|
||||
* English
|
||||
@@ -102,23 +100,10 @@ public final class Locales {
|
||||
@JsonAdapter(SupportedLocale.TypeAdapter.class)
|
||||
public static final class SupportedLocale {
|
||||
private final Locale locale;
|
||||
private final String name;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private DateTimeFormatter dateTimeFormatter;
|
||||
private ResourceBundle resourceBundle;
|
||||
|
||||
SupportedLocale(Locale locale) {
|
||||
this(locale, null);
|
||||
}
|
||||
|
||||
SupportedLocale(Locale locale, String name) {
|
||||
this.locale = locale;
|
||||
this.name = name;
|
||||
if (JavaVersion.CURRENT_JAVA.getParsedVersion() == JavaVersion.JAVA_8) {
|
||||
resourceBundle = ResourceBundle.getBundle("assets.lang.I18N", locale, UTF8Control.INSTANCE);
|
||||
} else {
|
||||
// UTF-8 is supported in Java 9+
|
||||
resourceBundle = ResourceBundle.getBundle("assets.lang.I18N", locale);
|
||||
}
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
@@ -126,20 +111,22 @@ public final class Locales {
|
||||
}
|
||||
|
||||
public ResourceBundle getResourceBundle() {
|
||||
return resourceBundle;
|
||||
}
|
||||
ResourceBundle bundle = resourceBundle;
|
||||
|
||||
public DateTimeFormatter getDateTimeFormatter() {
|
||||
if (dateTimeFormatter == null) {
|
||||
dateTimeFormatter = DateTimeFormatter.ofPattern(resourceBundle.getString("world.time")).withZone(ZoneId.systemDefault());
|
||||
if (resourceBundle == null) {
|
||||
if (this != DEFAULT && this.locale == DEFAULT.locale) {
|
||||
bundle = DEFAULT.getResourceBundle();
|
||||
} else if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9) {
|
||||
bundle = ResourceBundle.getBundle("assets.lang.I18N", locale, UTF8Control.INSTANCE);
|
||||
} else {
|
||||
// Java 9+ uses UTF-8 as the default encoding for resource bundles
|
||||
bundle = ResourceBundle.getBundle("assets.lang.I18N", locale);
|
||||
}
|
||||
|
||||
resourceBundle = bundle;
|
||||
}
|
||||
|
||||
return dateTimeFormatter;
|
||||
}
|
||||
|
||||
public String getName(ResourceBundle newResourceBundle) {
|
||||
if (name == null) return resourceBundle.getString("lang");
|
||||
else return newResourceBundle.getString(name);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
public static final class TypeAdapter extends com.google.gson.TypeAdapter<SupportedLocale> {
|
||||
|
||||
|
Before Width: | Height: | Size: 999 B |
|
Before Width: | Height: | Size: 912 B |
|
Before Width: | Height: | Size: 1001 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 861 B |
|
Before Width: | Height: | Size: 958 B |
|
Before Width: | Height: | Size: 988 B |
|
Before Width: | Height: | Size: 947 B |
@@ -143,8 +143,8 @@ account.portable=Portable Account
|
||||
account.skin=Skin
|
||||
account.skin.file=Skin File
|
||||
account.skin.model=Model
|
||||
account.skin.model.default=Classic (Steve)
|
||||
account.skin.model.slim=Slim (Alex)
|
||||
account.skin.model.default=Classic
|
||||
account.skin.model.slim=Slim
|
||||
account.skin.type.csl_api=Blessing Skin
|
||||
account.skin.type.csl_api.location=Address
|
||||
account.skin.type.csl_api.location.hint=CustomSkinAPI URL
|
||||
@@ -827,26 +827,39 @@ Keep in mind that you are not allowed to add mods and resource packs that are ex
|
||||
modpack.wizard.step.initialization.server=Click here for more tutorials for making a server modpack that can be automatically updated.
|
||||
|
||||
modrinth.category.adventure=Adventure
|
||||
modrinth.category.audio=Audio
|
||||
modrinth.category.blocks=Blocks
|
||||
modrinth.category.bukkit=Bukkit
|
||||
modrinth.category.bungeecord=BungeeCord
|
||||
modrinth.category.challenging=Challenging
|
||||
modrinth.category.core-shaders=Core Shaders
|
||||
modrinth.category.combat=Combat
|
||||
modrinth.category.cursed=Cursed
|
||||
modrinth.category.decoration=Decoration
|
||||
modrinth.category.economy=Economy
|
||||
modrinth.category.entities=Entities
|
||||
modrinth.category.environment=Environment
|
||||
modrinth.category.equipment=Equipment
|
||||
modrinth.category.fabric=Fabric
|
||||
modrinth.category.fonts=Fonts
|
||||
modrinth.category.food=Food
|
||||
modrinth.category.forge=Forge
|
||||
modrinth.category.game-mechanics=Game Mechanics
|
||||
modrinth.category.gui=GUI
|
||||
modrinth.category.items=Items
|
||||
modrinth.category.kitchen-sink=Kitchen-Sink
|
||||
modrinth.category.library=Library
|
||||
modrinth.category.lightweight=Lightweight
|
||||
modrinth.category.liteloader=LiteLoader
|
||||
modrinth.category.locale=Locale
|
||||
modrinth.category.magic=Magic
|
||||
modrinth.category.management=Management
|
||||
modrinth.category.minecraft=Minecraft
|
||||
modrinth.category.minigame=Minigame
|
||||
modrinth.category.misc=Misc
|
||||
modrinth.category.mobs=Mobs
|
||||
modrinth.category.modded=Modded
|
||||
modrinth.category.models=Models
|
||||
modrinth.category.modloader=Modloader
|
||||
modrinth.category.multiplayer=Multiplayer
|
||||
modrinth.category.neoforge=NeoForge
|
||||
@@ -855,19 +868,32 @@ modrinth.category.paper=Paper
|
||||
modrinth.category.purpur=Purpur
|
||||
modrinth.category.quests=Quests
|
||||
modrinth.category.quilt=Quilt
|
||||
modrinth.category.realistic=Realistic
|
||||
modrinth.category.rift=Rift
|
||||
modrinth.category.simplistic=Simplistic
|
||||
modrinth.category.social=Social
|
||||
modrinth.category.spigot=Spigot
|
||||
modrinth.category.sponge=Sponge
|
||||
modrinth.category.storage=Storage
|
||||
modrinth.category.technology=Technology
|
||||
modrinth.category.themed=Themed
|
||||
modrinth.category.transportation=Transportation
|
||||
modrinth.category.tweaks=Tweaks
|
||||
modrinth.category.utility=Utility
|
||||
modrinth.category.vanilla-like=Vanilla-like
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=Worldgen
|
||||
modrinth.category.datapack=Datapack
|
||||
modrinth.category.folia=Folia
|
||||
modrinth.category.8x-=8x-
|
||||
modrinth.category.16x=16x
|
||||
modrinth.category.32x=32x
|
||||
modrinth.category.48x=48x
|
||||
modrinth.category.64x=64x
|
||||
modrinth.category.128x=128x
|
||||
modrinth.category.256x=256x
|
||||
modrinth.category.512x+=512x+
|
||||
|
||||
mods=Mods
|
||||
mods.add=Add Mods
|
||||
|
||||
@@ -125,8 +125,8 @@ account.password=Contraseña
|
||||
account.skin=Skin
|
||||
account.skin.file=Archivo de skin
|
||||
account.skin.model=Modelo
|
||||
account.skin.model.default=Clásico (Steve)
|
||||
account.skin.model.slim=Delgado (Alex)
|
||||
account.skin.model.default=Clásico
|
||||
account.skin.model.slim=Delgado
|
||||
account.skin.type.csl_api=Blessing skin
|
||||
account.skin.type.csl_api.location=URL
|
||||
account.skin.type.csl_api.location.hint=CustomSkinAPI URL
|
||||
|
||||
@@ -112,8 +112,8 @@ account.password=パスワード
|
||||
account.skin=スキン
|
||||
account.skin.file=スキンイメージファイル
|
||||
account.skin.model=モデル
|
||||
account.skin.model.default=Classic (Steve)
|
||||
account.skin.model.slim=Slim (Alex)
|
||||
account.skin.model.default=Classic
|
||||
account.skin.model.slim=Slim
|
||||
account.skin.type.csl_api=Blessing Skin
|
||||
account.skin.type.csl_api.location=アドレス
|
||||
account.skin.type.csl_api.location.hint=CustomSkinAPI
|
||||
|
||||
@@ -120,8 +120,8 @@ account.password=Пароль
|
||||
account.skin=Скин
|
||||
account.skin.file=Файл скина
|
||||
account.skin.model=Модель
|
||||
account.skin.model.default=Классическая (Стив)
|
||||
account.skin.model.slim=Стройная (Алекс)
|
||||
account.skin.model.default=Классическая
|
||||
account.skin.model.slim=Стройная
|
||||
account.skin.type.csl_api=Blessing Skin
|
||||
account.skin.type.csl_api.location=Адрес
|
||||
account.skin.type.csl_api.location.hint=URL-адрес CustomSkinAPI
|
||||
|
||||
@@ -152,8 +152,8 @@ account.portable=便攜帳戶
|
||||
account.skin=皮膚
|
||||
account.skin.file=皮膚圖片檔案
|
||||
account.skin.model=模型
|
||||
account.skin.model.default=經典(Steve)
|
||||
account.skin.model.slim=苗條(Alex)
|
||||
account.skin.model.default=經典
|
||||
account.skin.model.slim=苗條
|
||||
account.skin.type.csl_api=Blessing Skin 伺服器
|
||||
account.skin.type.csl_api.location=伺服器位址
|
||||
account.skin.type.csl_api.location.hint=CustomSkinAPI 位址
|
||||
@@ -511,7 +511,7 @@ install.installer.forge=Forge
|
||||
install.installer.neoforge=NeoForge
|
||||
install.installer.game=Minecraft
|
||||
install.installer.incompatible=與 %s 不相容
|
||||
install.installer.install=安裝%s
|
||||
install.installer.install=安裝 %s
|
||||
install.installer.install_offline=從本機檔案安裝或升級
|
||||
install.installer.install_offline.extension=Forge/OptiFine 安裝器
|
||||
install.installer.install_offline.tooltip=支援匯入已經下載好的 Forge/OptiFine 安裝器
|
||||
@@ -703,26 +703,39 @@ modpack.wizard.step.initialization.warning=在製作模組包前,請您確認
|
||||
modpack.wizard.step.initialization.server=點選此處查看有關伺服器自動更新模組包的製作教學
|
||||
|
||||
modrinth.category.adventure=冒險
|
||||
modrinth.category.audio=聲音
|
||||
modrinth.category.blocks=方塊
|
||||
modrinth.category.bukkit=Bukkit
|
||||
modrinth.category.bungeecord=BungeeCord
|
||||
modrinth.category.challenging=高難度
|
||||
modrinth.category.core-shaders=覈心著色器
|
||||
modrinth.category.combat=戰鬥
|
||||
modrinth.category.cursed=Cursed
|
||||
modrinth.category.decoration=裝飾
|
||||
modrinth.category.economy=經濟
|
||||
modrinth.category.entities=實體
|
||||
modrinth.category.environment=環境
|
||||
modrinth.category.equipment=裝備
|
||||
modrinth.category.fabric=Fabric
|
||||
modrinth.category.fonts=字體
|
||||
modrinth.category.food=食物
|
||||
modrinth.category.forge=Forge
|
||||
modrinth.category.game-mechanics=遊戲機制
|
||||
modrinth.category.gui=GUI
|
||||
modrinth.category.items=物品
|
||||
modrinth.category.kitchen-sink=大雜燴
|
||||
modrinth.category.library=支持庫
|
||||
modrinth.category.lightweight=輕量
|
||||
modrinth.category.liteloader=LiteLoader
|
||||
modrinth.category.locale=在地化
|
||||
modrinth.category.magic=魔法
|
||||
modrinth.category.management=管理
|
||||
modrinth.category.minecraft=Minecraft
|
||||
modrinth.category.minigame=小遊戲
|
||||
modrinth.category.misc=其他
|
||||
modrinth.category.mobs=生物
|
||||
modrinth.category.modded=Modded
|
||||
modrinth.category.models=模型
|
||||
modrinth.category.modloader=Modloader
|
||||
modrinth.category.multiplayer=多人
|
||||
modrinth.category.neoforge=NeoForge
|
||||
@@ -731,14 +744,19 @@ modrinth.category.paper=Paper
|
||||
modrinth.category.purpur=Purpur
|
||||
modrinth.category.quests=任務
|
||||
modrinth.category.quilt=Quilt
|
||||
modrinth.category.realistic=寫實
|
||||
modrinth.category.rift=Rift
|
||||
modrinth.category.simplistic=簡單
|
||||
modrinth.category.social=社交
|
||||
modrinth.category.spigot=Spigot
|
||||
modrinth.category.sponge=Sponge
|
||||
modrinth.category.storage=儲存
|
||||
modrinth.category.technology=科技
|
||||
modrinth.category.themed=主題
|
||||
modrinth.category.transportation=運輸
|
||||
modrinth.category.tweaks=優化
|
||||
modrinth.category.utility=實用
|
||||
modrinth.category.vanilla-like=類原生
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=世界生成
|
||||
|
||||
@@ -153,8 +153,8 @@ account.portable=便携账户
|
||||
account.skin=皮肤
|
||||
account.skin.file=皮肤图片文件
|
||||
account.skin.model=模型
|
||||
account.skin.model.default=经典(Steve)
|
||||
account.skin.model.slim=苗条(Alex)
|
||||
account.skin.model.default=经典
|
||||
account.skin.model.slim=苗条
|
||||
account.skin.type.csl_api=Blessing Skin 服务器
|
||||
account.skin.type.csl_api.location=服务器地址
|
||||
account.skin.type.csl_api.location.hint=CustomSkinAPI 地址
|
||||
@@ -702,26 +702,39 @@ modpack.wizard.step.initialization.warning=在制作整合包前,请您确认
|
||||
modpack.wizard.step.initialization.server=点击此处查看有关服务器自动更新整合包的制作教程
|
||||
|
||||
modrinth.category.adventure=冒险
|
||||
modrinth.category.audio=声音
|
||||
modrinth.category.blocks=方块
|
||||
modrinth.category.bukkit=Bukkit
|
||||
modrinth.category.bungeecord=BungeeCord
|
||||
modrinth.category.challenging=高难度
|
||||
modrinth.category.core-shaders=核心着色器
|
||||
modrinth.category.combat=战斗
|
||||
modrinth.category.cursed=Cursed
|
||||
modrinth.category.decoration=装饰
|
||||
modrinth.category.economy=经济
|
||||
modrinth.category.entities=实体
|
||||
modrinth.category.environment=环境
|
||||
modrinth.category.equipment=装备
|
||||
modrinth.category.fabric=Fabric
|
||||
modrinth.category.fonts=字体
|
||||
modrinth.category.food=食物
|
||||
modrinth.category.forge=Forge
|
||||
modrinth.category.game-mechanics=游戏机制
|
||||
modrinth.category.gui=GUI
|
||||
modrinth.category.items=物品
|
||||
modrinth.category.kitchen-sink=大杂烩
|
||||
modrinth.category.library=支持库
|
||||
modrinth.category.lightweight=轻量
|
||||
modrinth.category.liteloader=LiteLoader
|
||||
modrinth.category.locale=本地化
|
||||
modrinth.category.magic=魔法
|
||||
modrinth.category.management=管理
|
||||
modrinth.category.minecraft=Minecraft
|
||||
modrinth.category.minigame=小游戏
|
||||
modrinth.category.misc=其他
|
||||
modrinth.category.mobs=生物
|
||||
modrinth.category.modded=Modded
|
||||
modrinth.category.models=模型
|
||||
modrinth.category.modloader=Modloader
|
||||
modrinth.category.multiplayer=多人
|
||||
modrinth.category.neoforge=NeoForge
|
||||
@@ -730,14 +743,19 @@ modrinth.category.paper=Paper
|
||||
modrinth.category.purpur=Purpur
|
||||
modrinth.category.quests=任务
|
||||
modrinth.category.quilt=Quilt
|
||||
modrinth.category.realistic=写实
|
||||
modrinth.category.rift=Rift
|
||||
modrinth.category.simplistic=简单
|
||||
modrinth.category.social=社交
|
||||
modrinth.category.spigot=Spigot
|
||||
modrinth.category.sponge=Sponge
|
||||
modrinth.category.storage=存储
|
||||
modrinth.category.technology=科技
|
||||
modrinth.category.themed=主题
|
||||
modrinth.category.transportation=运输
|
||||
modrinth.category.tweaks=优化
|
||||
modrinth.category.utility=实用
|
||||
modrinth.category.vanilla-like=类原生
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=世界生成
|
||||
|
||||
@@ -8,6 +8,7 @@ import moe.mickey.minecraft.skin.fx.SkinCanvas;
|
||||
import moe.mickey.minecraft.skin.fx.SkinCanvasSupport;
|
||||
import moe.mickey.minecraft.skin.fx.animation.SkinAniRunning;
|
||||
import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -16,7 +17,7 @@ public class Test extends Application {
|
||||
public static final String TITLE = "FX - Minecraft skin preview";
|
||||
|
||||
public static SkinCanvas createSkinCanvas() {
|
||||
SkinCanvas canvas = new SkinCanvas(SkinCanvas.STEVE, 400, 400, true);
|
||||
SkinCanvas canvas = new SkinCanvas(TexturesLoader.getDefaultSkinImage(), 400, 400, true);
|
||||
canvas.getAnimationPlayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas));
|
||||
FunctionHelper.alwaysB(Consumer<SkinCanvas>::accept, canvas, new SkinCanvasSupport.Mouse(.5), new SkinCanvasSupport.Drag(TITLE));
|
||||
return canvas;
|
||||
|
||||
@@ -60,6 +60,7 @@ public class AuthlibInjectorServer implements Observable {
|
||||
try {
|
||||
url = addHttpsIfMissing(url);
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
conn.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
|
||||
|
||||
String ali = conn.getHeaderField("x-authlib-injector-api-location");
|
||||
if (ali != null) {
|
||||
@@ -68,6 +69,7 @@ public class AuthlibInjectorServer implements Observable {
|
||||
conn.disconnect();
|
||||
url = absoluteAli.toString();
|
||||
conn = (HttpURLConnection) absoluteAli.openConnection();
|
||||
conn.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactInfo;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
@@ -101,14 +100,7 @@ public class OfflineAccount extends Account {
|
||||
}
|
||||
|
||||
protected boolean loadAuthlibInjector(Skin skin) {
|
||||
if (skin == null) return false;
|
||||
if (skin.getType() == Skin.Type.DEFAULT) return false;
|
||||
TextureModel defaultModel = TextureModel.detectUUID(getUUID());
|
||||
if (skin.getType() == Skin.Type.ALEX && defaultModel == TextureModel.ALEX ||
|
||||
skin.getType() == Skin.Type.STEVE && defaultModel == TextureModel.STEVE) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return skin != null && skin.getType() != Skin.Type.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.auth.offline;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.task.FetchTask;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
@@ -39,7 +40,6 @@ import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
@@ -99,12 +99,6 @@ public class Skin {
|
||||
}
|
||||
}
|
||||
|
||||
private static Function<Type, InputStream> defaultSkinLoader = null;
|
||||
|
||||
public static void registerDefaultSkinLoader(Function<Type, InputStream> defaultSkinLoader0) {
|
||||
defaultSkinLoader = defaultSkinLoader0;
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
private final String cslApi;
|
||||
private final TextureModel textureModel;
|
||||
@@ -128,7 +122,7 @@ public class Skin {
|
||||
}
|
||||
|
||||
public TextureModel getTextureModel() {
|
||||
return textureModel == null ? TextureModel.STEVE : textureModel;
|
||||
return textureModel == null ? TextureModel.WIDE : textureModel;
|
||||
}
|
||||
|
||||
public String getLocalSkinPath() {
|
||||
@@ -152,11 +146,14 @@ public class Skin {
|
||||
case STEVE:
|
||||
case SUNNY:
|
||||
case ZURI:
|
||||
if (defaultSkinLoader == null) {
|
||||
return Task.supplyAsync(() -> null);
|
||||
}
|
||||
TextureModel model = type == Type.ALEX ? TextureModel.ALEX : TextureModel.STEVE;
|
||||
return Task.supplyAsync(() -> new LoadedSkin(model, Texture.loadTexture(defaultSkinLoader.apply(type)), null));
|
||||
TextureModel model = this.textureModel != null ? this.textureModel : type == Type.ALEX ? TextureModel.SLIM : TextureModel.WIDE;
|
||||
String resource = (model == TextureModel.SLIM ? "/assets/img/skin/slim/" : "/assets/img/skin/wide/") + type.name().toLowerCase(Locale.ROOT) + ".png";
|
||||
|
||||
return Task.supplyAsync(() -> new LoadedSkin(
|
||||
model,
|
||||
Texture.loadTexture(new Image(resource)),
|
||||
null
|
||||
));
|
||||
case LOCAL_FILE:
|
||||
return Task.supplyAsync(() -> {
|
||||
Texture skin = null, cape = null;
|
||||
@@ -227,16 +224,7 @@ public class Skin {
|
||||
String localSkinPath = tryCast(storage.get("localSkinPath"), String.class).orElse(null);
|
||||
String localCapePath = tryCast(storage.get("localCapePath"), String.class).orElse(null);
|
||||
|
||||
TextureModel model;
|
||||
if ("default".equals(textureModel)) {
|
||||
model = TextureModel.STEVE;
|
||||
} else if ("slim".equals(textureModel)) {
|
||||
model = TextureModel.ALEX;
|
||||
} else {
|
||||
model = TextureModel.STEVE;
|
||||
}
|
||||
|
||||
return new Skin(type, cslApi, model, localSkinPath, localCapePath);
|
||||
return new Skin(type, cslApi, "slim".equals(textureModel) ? TextureModel.SLIM : TextureModel.WIDE, localSkinPath, localCapePath);
|
||||
}
|
||||
|
||||
private static class FetchBytesTask extends FetchTask<InputStream> {
|
||||
@@ -327,9 +315,9 @@ public class Skin {
|
||||
@Nullable
|
||||
public TextureModel getModel() {
|
||||
if (textures != null && textures.slim != null) {
|
||||
return TextureModel.ALEX;
|
||||
return TextureModel.SLIM;
|
||||
} else if (textures != null && textures.defaultSkin != null) {
|
||||
return TextureModel.STEVE;
|
||||
return TextureModel.WIDE;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -351,9 +339,9 @@ public class Skin {
|
||||
|
||||
public String getHash() {
|
||||
TextureModel model = getModel();
|
||||
if (model == TextureModel.ALEX)
|
||||
if (model == TextureModel.SLIM)
|
||||
return getAlexModelHash();
|
||||
else if (model == TextureModel.STEVE)
|
||||
else if (model == TextureModel.WIDE)
|
||||
return getSteveModelHash();
|
||||
else
|
||||
return null;
|
||||
|
||||
@@ -178,7 +178,7 @@ public class YggdrasilServer extends HttpServer {
|
||||
public Object toCompleteResponse(String rootUrl) {
|
||||
Map<String, Object> realTextures = new HashMap<>();
|
||||
if (skin != null && skin.getSkin() != null) {
|
||||
if (skin.getModel() == TextureModel.ALEX) {
|
||||
if (skin.getModel() == TextureModel.SLIM) {
|
||||
realTextures.put("SKIN", mapOf(
|
||||
pair("url", rootUrl + "/textures/" + skin.getSkin().getHash()),
|
||||
pair("metadata", mapOf(
|
||||
|
||||
@@ -17,27 +17,12 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public enum TextureModel {
|
||||
STEVE("default"), ALEX("slim");
|
||||
WIDE("default"), SLIM("slim");
|
||||
|
||||
public final String modelName;
|
||||
|
||||
TextureModel(String modelName) {
|
||||
this.modelName = modelName;
|
||||
}
|
||||
|
||||
public static TextureModel detectModelName(Map<String, String> metadata) {
|
||||
if (metadata != null && "slim".equals(metadata.get("model"))) {
|
||||
return ALEX;
|
||||
} else {
|
||||
return STEVE;
|
||||
}
|
||||
}
|
||||
|
||||
public static TextureModel detectUUID(UUID uuid) {
|
||||
return (uuid.hashCode() & 1) == 1 ? ALEX : STEVE;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.download.game;
|
||||
|
||||
import org.jackhuang.hmcl.download.AbstractDependencyManager;
|
||||
import org.jackhuang.hmcl.download.ArtifactMalformedException;
|
||||
import org.jackhuang.hmcl.download.DefaultCacheRepository;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.task.DownloadException;
|
||||
@@ -26,13 +25,12 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Pack200Utils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
@@ -41,7 +39,6 @@ import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@@ -51,10 +48,8 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
protected final File jar;
|
||||
protected final DefaultCacheRepository cacheRepository;
|
||||
protected final AbstractDependencyManager dependencyManager;
|
||||
private final File xzFile;
|
||||
protected final Library library;
|
||||
protected final String url;
|
||||
protected boolean xz;
|
||||
private final Library originalLibrary;
|
||||
private boolean cached = false;
|
||||
|
||||
@@ -72,8 +67,6 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
|
||||
url = library.getDownload().getUrl();
|
||||
jar = file;
|
||||
|
||||
xzFile = new File(file.getAbsoluteFile().getParentFile(), file.getName() + ".pack.xz");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,8 +94,6 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
throw new CancellationException();
|
||||
else
|
||||
throw new LibraryDownloadException(library, t);
|
||||
} else {
|
||||
if (xz) unpackLibrary(jar, Files.readAllBytes(xzFile.toPath()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,36 +117,13 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
}
|
||||
}
|
||||
|
||||
if (Pack200Utils.isSupported() && testURLExistence(url)) {
|
||||
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url + ".pack.xz");
|
||||
task = new FileDownloadTask(urls, xzFile, null);
|
||||
task.setCacheRepository(cacheRepository);
|
||||
task.setCaching(true);
|
||||
xz = true;
|
||||
} else {
|
||||
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url);
|
||||
task = new FileDownloadTask(urls, jar,
|
||||
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
|
||||
task.setCacheRepository(cacheRepository);
|
||||
task.setCaching(true);
|
||||
task.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
|
||||
xz = false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean testURLExistence(String rawUrl) {
|
||||
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(rawUrl);
|
||||
for (URL url : urls) {
|
||||
URL xzURL = NetworkUtils.toURL(url.toString() + ".pack.xz");
|
||||
for (int retry = 0; retry < 3; retry++) {
|
||||
try {
|
||||
return NetworkUtils.urlExists(xzURL);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to test for url existence: " + url + ".pack.xz", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false; // maybe some ugly implementation will give timeout for not existent url.
|
||||
List<URL> urls = dependencyManager.getDownloadProvider().injectURLWithCandidates(url);
|
||||
task = new FileDownloadTask(urls, jar,
|
||||
library.getDownload().getSha1() != null ? new IntegrityCheck("SHA-1", library.getDownload().getSha1()) : null);
|
||||
task.setCacheRepository(cacheRepository);
|
||||
task.setCaching(true);
|
||||
task.addIntegrityCheckHandler(FileDownloadTask.ZIP_INTEGRITY_CHECK_HANDLER);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -167,7 +135,7 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
public void postExecute() throws Exception {
|
||||
if (!cached) {
|
||||
try {
|
||||
cacheRepository.cacheLibrary(library, jar.toPath(), xz);
|
||||
cacheRepository.cacheLibrary(library, jar.toPath(), false);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to cache downloaded library " + library, e);
|
||||
}
|
||||
@@ -232,44 +200,4 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void unpackLibrary(File dest, byte[] src) throws IOException {
|
||||
if (dest.exists())
|
||||
if (!dest.delete())
|
||||
throw new IOException("Unable to delete file " + dest);
|
||||
|
||||
byte[] decompressed;
|
||||
try {
|
||||
decompressed = IOUtils.readFullyAsByteArray(new XZInputStream(new ByteArrayInputStream(src)));
|
||||
} catch (IOException e) {
|
||||
throw new ArtifactMalformedException("Library " + dest + " is malformed");
|
||||
}
|
||||
|
||||
String end = new String(decompressed, decompressed.length - 4, 4);
|
||||
if (!end.equals("SIGN"))
|
||||
throw new IOException("Unpacking failed, signature missing " + end);
|
||||
|
||||
int x = decompressed.length;
|
||||
int len = decompressed[(x - 8)] & 0xFF | (decompressed[(x - 7)] & 0xFF) << 8 | (decompressed[(x - 6)] & 0xFF) << 16 | (decompressed[(x - 5)] & 0xFF) << 24;
|
||||
|
||||
Path temp = Files.createTempFile("minecraft", ".pack");
|
||||
|
||||
byte[] checksums = Arrays.copyOfRange(decompressed, decompressed.length - len - 8, decompressed.length - 8);
|
||||
|
||||
try (OutputStream out = Files.newOutputStream(temp)) {
|
||||
out.write(decompressed, 0, decompressed.length - len - 8);
|
||||
}
|
||||
|
||||
try (FileOutputStream jarBytes = new FileOutputStream(dest); JarOutputStream jos = new JarOutputStream(jarBytes)) {
|
||||
Pack200Utils.unpack(temp.toFile(), jos);
|
||||
|
||||
JarEntry checksumsFile = new JarEntry("checksums.sha1");
|
||||
checksumsFile.setTime(0L);
|
||||
jos.putNextEntry(checksumsFile);
|
||||
jos.write(checksums);
|
||||
jos.closeEntry();
|
||||
}
|
||||
|
||||
Files.delete(temp);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,10 @@ public final class AssetIndex {
|
||||
return virtual || mapToResources;
|
||||
}
|
||||
|
||||
public boolean needMapToResources() {
|
||||
return mapToResources;
|
||||
}
|
||||
|
||||
public Map<String, AssetObject> getObjects() {
|
||||
return Collections.unmodifiableMap(objects);
|
||||
}
|
||||
|
||||
@@ -459,6 +459,8 @@ public class DefaultGameRepository implements GameRepository {
|
||||
return assetsDir;
|
||||
|
||||
if (index.isVirtual()) {
|
||||
Path resourcesDir = getBaseDirectory().toPath().resolve("resources");
|
||||
|
||||
int cnt = 0;
|
||||
int tot = index.getObjects().entrySet().size();
|
||||
for (Map.Entry<String, AssetObject> entry : index.getObjects().entrySet()) {
|
||||
@@ -468,6 +470,12 @@ public class DefaultGameRepository implements GameRepository {
|
||||
cnt++;
|
||||
if (!Files.isRegularFile(target))
|
||||
FileUtils.copyFile(original, target);
|
||||
|
||||
if (index.needMapToResources()) {
|
||||
target = resourcesDir.resolve(entry.getKey());
|
||||
if (!Files.isRegularFile(target))
|
||||
FileUtils.copyFile(original, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,8 +104,13 @@ final class GameVersion {
|
||||
if (minecraft != null) {
|
||||
try (InputStream is = gameJar.getInputStream(minecraft)) {
|
||||
Optional<String> result = getVersionOfClassMinecraft(is);
|
||||
if (result.isPresent())
|
||||
if (result.isPresent()) {
|
||||
String version = result.get();
|
||||
if (version.startsWith("Beta ")) {
|
||||
result = Optional.of("b" + version.substring("Beta ".length()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
// Executable
|
||||
if (StringUtils.isNotBlank(options.getWrapper()))
|
||||
res.addAllWithoutParsing(StringUtils.tokenize(options.getWrapper()));
|
||||
res.addAllWithoutParsing(StringUtils.tokenize(options.getWrapper(), getEnvVars()));
|
||||
|
||||
res.add(options.getJava().getBinary().toString());
|
||||
|
||||
@@ -429,9 +429,6 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
|
||||
List<String> rawCommandLine = command.commandLine.asList();
|
||||
if (StringUtils.isNotBlank(options.getWrapper())) {
|
||||
rawCommandLine.addAll(0, StringUtils.tokenize(options.getWrapper(), getEnvVars()));
|
||||
}
|
||||
|
||||
if (command.tempNativeFolder != null) {
|
||||
Files.deleteIfExists(command.tempNativeFolder);
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
@@ -30,18 +31,18 @@ import java.util.stream.Stream;
|
||||
import static org.jackhuang.hmcl.util.io.NetworkUtils.encodeLocation;
|
||||
|
||||
public class RemoteMod {
|
||||
private static RemoteMod EMPTY = null;
|
||||
|
||||
public static void registerEmptyRemoteMod(RemoteMod empty) {
|
||||
EMPTY = empty;
|
||||
}
|
||||
|
||||
public static RemoteMod getEmptyRemoteMod() {
|
||||
if (EMPTY == null) {
|
||||
throw new NullPointerException();
|
||||
public static final RemoteMod BROKEN = new RemoteMod("", "", "RemoteMod.BROKEN", "", Collections.emptyList(), "", "", new RemoteMod.IMod() {
|
||||
@Override
|
||||
public List<RemoteMod> loadDependencies(RemoteModRepository modRepository) throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RemoteMod.Version> loadVersions(RemoteModRepository modRepository) throws IOException {
|
||||
throw new IOException();
|
||||
}
|
||||
});
|
||||
|
||||
private final String slug;
|
||||
private final String author;
|
||||
@@ -158,7 +159,7 @@ public class RemoteMod {
|
||||
public RemoteMod load() throws IOException {
|
||||
if (this.remoteMod == null) {
|
||||
if (this.type == DependencyType.BROKEN) {
|
||||
this.remoteMod = RemoteMod.getEmptyRemoteMod();
|
||||
this.remoteMod = RemoteMod.BROKEN;
|
||||
} else {
|
||||
this.remoteMod = this.remoteModRepository.getModById(this.id);
|
||||
}
|
||||
|
||||
@@ -111,9 +111,8 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
.header("X-API-KEY", apiKey)
|
||||
.getJson(new TypeToken<Response<List<CurseAddon>>>() {
|
||||
}.getType());
|
||||
Stream<RemoteMod> res = response.getData().stream().map(CurseAddon::toMod);
|
||||
if (searchFilter.isEmpty()) {
|
||||
return new SearchResult(res, (int)Math.ceil((double)response.pagination.totalCount / pageSize));
|
||||
return new SearchResult(response.getData().stream().map(CurseAddon::toMod), (int)Math.ceil((double)response.pagination.totalCount / pageSize));
|
||||
}
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/1549
|
||||
@@ -125,7 +124,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
|
||||
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
||||
|
||||
return new SearchResult(res.map(remoteMod -> {
|
||||
return new SearchResult(response.getData().stream().map(CurseAddon::toMod).map(remoteMod -> {
|
||||
String lowerCaseResult = remoteMod.getTitle().toLowerCase();
|
||||
int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult);
|
||||
|
||||
@@ -136,7 +135,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||
}
|
||||
|
||||
return pair(remoteMod, diff);
|
||||
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), res, response.pagination.totalCount);
|
||||
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.getData().stream().map(CurseAddon::toMod), response.pagination.totalCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -94,6 +94,7 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||
break download;
|
||||
}
|
||||
|
||||
List<String> redirects = null;
|
||||
try {
|
||||
beforeDownload(url);
|
||||
|
||||
@@ -103,7 +104,9 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||
if (checkETag) repository.injectConnection(conn);
|
||||
|
||||
if (conn instanceof HttpURLConnection) {
|
||||
conn = NetworkUtils.resolveConnection((HttpURLConnection) conn);
|
||||
redirects = new ArrayList<>();
|
||||
|
||||
conn = NetworkUtils.resolveConnection((HttpURLConnection) conn, redirects);
|
||||
int responseCode = ((HttpURLConnection) conn).getResponseCode();
|
||||
|
||||
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
|
||||
@@ -164,13 +167,13 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||
} catch (FileNotFoundException ex) {
|
||||
failedURL = url;
|
||||
exception = ex;
|
||||
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", not found", ex);
|
||||
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", not found" + ((redirects == null || redirects.isEmpty()) ? "" : ", redirects: " + redirects), ex);
|
||||
|
||||
break; // we will not try this URL again
|
||||
} catch (IOException ex) {
|
||||
failedURL = url;
|
||||
exception = ex;
|
||||
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + (++repeat), ex);
|
||||
Logging.LOG.log(Level.WARNING, "Failed to download " + url + ", repeat times: " + (++repeat) + ((redirects == null || redirects.isEmpty()) ? "" : ", redirects: " + redirects), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,8 +293,12 @@ public class CacheRepository {
|
||||
ETagIndex indexOnDisk = JsonUtils.fromMaybeMalformedJson(new String(IOUtils.readFullyWithoutClosing(Channels.newInputStream(channel)), UTF_8), ETagIndex.class);
|
||||
Map<String, ETagItem> newIndex = joinETagIndexes(indexOnDisk == null ? null : indexOnDisk.eTag, index.values());
|
||||
channel.truncate(0);
|
||||
ETagIndex writeTo = new ETagIndex(newIndex.values());
|
||||
channel.write(ByteBuffer.wrap(JsonUtils.GSON.toJson(writeTo).getBytes(UTF_8)));
|
||||
ByteBuffer writeTo = ByteBuffer.wrap(JsonUtils.GSON.toJson(new ETagIndex(newIndex.values())).getBytes(UTF_8));
|
||||
while (writeTo.hasRemaining()) {
|
||||
if (channel.write(writeTo) == 0) {
|
||||
throw new IOException("No value is written");
|
||||
}
|
||||
}
|
||||
this.index = newIndex;
|
||||
} finally {
|
||||
lock.release();
|
||||
|
||||
@@ -29,7 +29,6 @@ import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.StringUtils.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class NetworkUtils {
|
||||
@@ -91,7 +90,7 @@ public final class NetworkUtils {
|
||||
connection.setUseCaches(false);
|
||||
connection.setConnectTimeout(TIME_OUT);
|
||||
connection.setReadTimeout(TIME_OUT);
|
||||
connection.setRequestProperty("Accept-Language", Locale.getDefault().toString());
|
||||
connection.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
|
||||
return connection;
|
||||
}
|
||||
|
||||
@@ -131,16 +130,20 @@ public final class NetworkUtils {
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
public static HttpURLConnection resolveConnection(HttpURLConnection conn) throws IOException {
|
||||
return resolveConnection(conn, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is a work-around that aims to solve problem when "Location" in
|
||||
* stupid server's response is not encoded.
|
||||
*
|
||||
*
|
||||
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
|
||||
* @param conn the stupid http connection.
|
||||
* @return manually redirected http connection.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
public static HttpURLConnection resolveConnection(HttpURLConnection conn) throws IOException {
|
||||
public static HttpURLConnection resolveConnection(HttpURLConnection conn, List<String> redirects) throws IOException {
|
||||
int redirect = 0;
|
||||
while (true) {
|
||||
conn.setUseCaches(false);
|
||||
@@ -154,6 +157,9 @@ public final class NetworkUtils {
|
||||
String newURL = conn.getHeaderField("Location");
|
||||
conn.disconnect();
|
||||
|
||||
if (redirects != null) {
|
||||
redirects.add(newURL);
|
||||
}
|
||||
if (redirect > 20) {
|
||||
throw new IOException("Too much redirects");
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
@@ -9,22 +9,19 @@ import java.nio.file.Paths;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class NormalizedSkinTest {
|
||||
private static NormalizedSkin getSkin(String name) throws InvalidSkinException {
|
||||
String path = Paths.get("../HMCL/src/main/resources/assets/img/skin/" + name + ".png").normalize().toAbsolutePath().toUri().toString();
|
||||
private static NormalizedSkin getSkin(String name, boolean slim) throws InvalidSkinException {
|
||||
String path = Paths.get(String.format("../HMCLCore/src/main/resources/assets/img/skin/%s/%s.png", slim ? "slim" : "wide", name)).normalize().toAbsolutePath().toUri().toString();
|
||||
return new NormalizedSkin(new Image(path));
|
||||
}
|
||||
|
||||
@Test
|
||||
@EnabledIf("org.jackhuang.hmcl.JavaFXLauncher#isStarted")
|
||||
public void testIsSlim() throws Exception {
|
||||
assertFalse(getSkin("steve").isSlim());
|
||||
assertTrue(getSkin("alex").isSlim());
|
||||
assertTrue(getSkin("noor").isSlim());
|
||||
assertFalse(getSkin("sunny").isSlim());
|
||||
assertFalse(getSkin("ari").isSlim());
|
||||
assertFalse(getSkin("zuri").isSlim());
|
||||
assertTrue(getSkin("makena").isSlim());
|
||||
assertFalse(getSkin("kai").isSlim());
|
||||
assertTrue(getSkin("efe").isSlim());
|
||||
String[] names = {"alex", "ari", "efe", "kai", "makena", "noor", "steve", "sunny", "zuri"};
|
||||
|
||||
for (String skin : names) {
|
||||
assertTrue(getSkin(skin, true).isSlim());
|
||||
assertFalse(getSkin(skin, false).isSlim());
|
||||
}
|
||||
}
|
||||
}
|
||||