mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-01-30 14:39:56 +08:00
feat: automatically detect skin model when uploading
This commit is contained in:
parent
7ece35e28a
commit
e3fa7428bf
@ -41,14 +41,21 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import static java.util.Collections.emptySet;
|
||||
import static javafx.beans.binding.Bindings.createBooleanBinding;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@ -137,9 +144,13 @@ public class AccountListItem extends RadioButton {
|
||||
}
|
||||
}
|
||||
|
||||
public void uploadSkin() {
|
||||
/**
|
||||
* @return the skin upload task, null if no file is selected
|
||||
*/
|
||||
@Nullable
|
||||
public Task<?> uploadSkin() {
|
||||
if (!(account instanceof YggdrasilAccount)) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
FileChooser chooser = new FileChooser();
|
||||
@ -147,23 +158,31 @@ public class AccountListItem extends RadioButton {
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("account.skin.file"), "*.png"));
|
||||
File selectedFile = chooser.showOpenDialog(Controllers.getStage());
|
||||
if (selectedFile == null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("account.skin.upload"), (questions, resolve, reject) -> {
|
||||
PromptDialogPane.Builder.CandidatesQuestion q = (PromptDialogPane.Builder.CandidatesQuestion) questions.get(0);
|
||||
String model = q.getValue() == 0 ? "" : "slim";
|
||||
refreshAsync()
|
||||
.thenRunAsync(() -> ((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath()))
|
||||
.thenComposeAsync(this::refreshAsync)
|
||||
.thenRunAsync(Schedulers.javafx(), resolve::run)
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e != null) {
|
||||
reject.accept(AddAccountPane.accountException(e));
|
||||
}
|
||||
}).start();
|
||||
}).addQuestion(new PromptDialogPane.Builder.CandidatesQuestion(i18n("account.skin.model"),
|
||||
i18n("account.skin.model.default"), i18n("account.skin.model.slim"))));
|
||||
return refreshAsync()
|
||||
.thenRunAsync(() -> {
|
||||
BufferedImage skinImg;
|
||||
try {
|
||||
skinImg = ImageIO.read(selectedFile);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidSkinException("Failed to read skin image", e);
|
||||
}
|
||||
if (skinImg == null) {
|
||||
throw new InvalidSkinException("Failed to read skin image");
|
||||
}
|
||||
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
||||
String model = skin.isSlim() ? "slim" : "";
|
||||
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
|
||||
((YggdrasilAccount) account).uploadSkin(model, selectedFile.toPath());
|
||||
})
|
||||
.thenComposeAsync(refreshAsync())
|
||||
.whenComplete(Schedulers.javafx(), e -> {
|
||||
if (e != null) {
|
||||
Controllers.dialog(AddAccountPane.accountException(e), i18n("account.skin.upload.failed"), MessageType.ERROR);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
|
@ -31,8 +31,11 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
@ -93,13 +96,24 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
right.getChildren().add(btnRefresh);
|
||||
|
||||
JFXButton btnUpload = new JFXButton();
|
||||
btnUpload.setOnMouseClicked(e -> skinnable.uploadSkin());
|
||||
SpinnerPane spinnerUpload = new SpinnerPane();
|
||||
btnUpload.setOnMouseClicked(e -> {
|
||||
Task<?> uploadTask = skinnable.uploadSkin();
|
||||
if (uploadTask != null) {
|
||||
spinnerUpload.showSpinner();
|
||||
uploadTask
|
||||
.whenComplete(Schedulers.javafx(), ex -> spinnerUpload.hideSpinner())
|
||||
.start();
|
||||
}
|
||||
});
|
||||
btnUpload.getStyleClass().add("toggle-icon4");
|
||||
btnUpload.setGraphic(SVG.hanger(Theme.blackFillBinding(), -1, -1));
|
||||
runInFX(() -> FXUtils.installFastTooltip(btnUpload, i18n("account.skin.upload")));
|
||||
btnUpload.managedProperty().bind(btnUpload.visibleProperty());
|
||||
btnUpload.visibleProperty().bind(skinnable.canUploadSkin());
|
||||
right.getChildren().add(btnUpload);
|
||||
spinnerUpload.managedProperty().bind(spinnerUpload.visibleProperty());
|
||||
spinnerUpload.visibleProperty().bind(skinnable.canUploadSkin());
|
||||
spinnerUpload.setContent(btnUpload);
|
||||
spinnerUpload.getStyleClass().add("small-spinner-pane");
|
||||
right.getChildren().add(spinnerUpload);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
btnRemove.setOnMouseClicked(e -> skinnable.remove());
|
||||
|
@ -49,6 +49,7 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -342,6 +343,8 @@ public class AddAccountPane extends StackPane {
|
||||
return i18n("account.failed.injector_download_failure");
|
||||
} else if (exception instanceof CharacterDeletedException) {
|
||||
return i18n("account.failed.character_deleted");
|
||||
} else if (exception instanceof InvalidSkinException) {
|
||||
return i18n("account.skin.invalid_skin");
|
||||
} else if (exception.getClass() == AuthenticationException.class) {
|
||||
return exception.getLocalizedMessage();
|
||||
} else {
|
||||
|
@ -64,10 +64,9 @@ account.missing=No Account
|
||||
account.missing.add=Click here to add
|
||||
account.password=Password
|
||||
account.skin.file=Skin file
|
||||
account.skin.model=Character model
|
||||
account.skin.model.default=Steve
|
||||
account.skin.model.slim=Alex
|
||||
account.skin.upload=Upload skin
|
||||
account.skin.upload.failed=Failed to upload skin
|
||||
account.skin.invalid_skin=Unrecognized skin file
|
||||
account.username=Name
|
||||
|
||||
archive.author=Authors
|
||||
|
@ -64,10 +64,9 @@ account.missing=沒有遊戲帳戶
|
||||
account.missing.add=按一下此處加入帳戶
|
||||
account.password=密碼
|
||||
account.skin.file=皮膚圖片檔案
|
||||
account.skin.model=人物模型
|
||||
account.skin.model.default=Steve
|
||||
account.skin.model.slim=Alex
|
||||
account.skin.upload=上傳皮膚
|
||||
account.skin.upload=皮膚上傳失敗
|
||||
account.skin.invalid_skin=無法識別的皮膚文件
|
||||
account.username=使用者名稱
|
||||
|
||||
archive.author=作者
|
||||
|
@ -64,10 +64,9 @@ account.missing=没有游戏账户
|
||||
account.missing.add=点击此处添加账户
|
||||
account.password=密码
|
||||
account.skin.file=皮肤图片文件
|
||||
account.skin.model=人物模型
|
||||
account.skin.model.default=Steve
|
||||
account.skin.model.slim=Alex
|
||||
account.skin.upload=上传皮肤
|
||||
account.skin.upload.failed=皮肤上传失败
|
||||
account.skin.invalid_skin=无法识别的皮肤文件
|
||||
account.username=用户名
|
||||
|
||||
archive.author=作者
|
||||
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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.skin;
|
||||
|
||||
public class InvalidSkinException extends Exception {
|
||||
|
||||
public InvalidSkinException() {}
|
||||
|
||||
public InvalidSkinException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InvalidSkinException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public InvalidSkinException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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.skin;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* Describes a Minecraft 1.8+ skin (64x64).
|
||||
* Old format skins are converted to the new format.
|
||||
*
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public class NormalizedSkin {
|
||||
|
||||
private static void copyImage(BufferedImage src, BufferedImage dst, int sx, int sy, int dx, int dy, int w, int h, boolean flipHorizontal) {
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
int pixel = src.getRGB(sx + x, sy + y);
|
||||
dst.setRGB(dx + (flipHorizontal ? w - x - 1 : x), dy + y, pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final BufferedImage texture;
|
||||
private final BufferedImage normalizedTexture;
|
||||
private final int scale;
|
||||
private final boolean oldFormat;
|
||||
|
||||
public NormalizedSkin(BufferedImage texture) throws InvalidSkinException {
|
||||
this.texture = texture;
|
||||
|
||||
// check format
|
||||
int w = texture.getWidth();
|
||||
int h = texture.getHeight();
|
||||
if (w % 64 != 0) {
|
||||
throw new InvalidSkinException("Invalid size " + w + "x" + h);
|
||||
}
|
||||
if (w == h) {
|
||||
oldFormat = false;
|
||||
} else if (w == h * 2) {
|
||||
oldFormat = true;
|
||||
} else {
|
||||
throw new InvalidSkinException("Invalid size " + w + "x" + h);
|
||||
}
|
||||
|
||||
// compute scale
|
||||
scale = w / 64;
|
||||
|
||||
normalizedTexture = new BufferedImage(w, w, BufferedImage.TYPE_INT_ARGB);
|
||||
copyImage(texture, normalizedTexture, 0, 0, 0, 0, w, h, false);
|
||||
if (oldFormat) {
|
||||
convertOldSkin();
|
||||
}
|
||||
}
|
||||
|
||||
private void convertOldSkin() {
|
||||
copyImageRelative(4, 16, 20, 48, 4, 4, true); // Top Leg
|
||||
copyImageRelative(8, 16, 24, 48, 4, 4, true); // Bottom Leg
|
||||
copyImageRelative(0, 20, 24, 52, 4, 12, true); // Outer Leg
|
||||
copyImageRelative(4, 20, 20, 52, 4, 12, true); // Front Leg
|
||||
copyImageRelative(8, 20, 16, 52, 4, 12, true); // Inner Leg
|
||||
copyImageRelative(12, 20, 28, 52, 4, 12, true); // Back Leg
|
||||
copyImageRelative(44, 16, 36, 48, 4, 4, true); // Top Arm
|
||||
copyImageRelative(48, 16, 40, 48, 4, 4, true); // Bottom Arm
|
||||
copyImageRelative(40, 20, 40, 52, 4, 12, true); // Outer Arm
|
||||
copyImageRelative(44, 20, 36, 52, 4, 12, true); // Front Arm
|
||||
copyImageRelative(48, 20, 32, 52, 4, 12, true); // Inner Arm
|
||||
copyImageRelative(52, 20, 44, 52, 4, 12, true); // Back Arm
|
||||
}
|
||||
|
||||
private void copyImageRelative(int sx, int sy, int dx, int dy, int w, int h, boolean flipHorizontal) {
|
||||
copyImage(normalizedTexture, normalizedTexture, sx * scale, sy * scale, dx * scale, dy * scale, w * scale, h * scale, flipHorizontal);
|
||||
}
|
||||
|
||||
public BufferedImage getOriginalTexture() {
|
||||
return texture;
|
||||
}
|
||||
|
||||
public BufferedImage getNormalizedTexture() {
|
||||
return normalizedTexture;
|
||||
}
|
||||
|
||||
public int getScale() {
|
||||
return scale;
|
||||
}
|
||||
|
||||
public boolean isOldFormat() {
|
||||
return oldFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the skin is slim.
|
||||
* Note that this method doesn't guarantee the result is correct.
|
||||
*/
|
||||
public boolean isSlim() {
|
||||
return (hasTransparencyRelative(50, 16, 2, 4) ||
|
||||
hasTransparencyRelative(54, 20, 2, 12) ||
|
||||
hasTransparencyRelative(42, 48, 2, 4) ||
|
||||
hasTransparencyRelative(46, 52, 2, 12)) ||
|
||||
(isAreaBlackRelative(50, 16, 2, 4) &&
|
||||
isAreaBlackRelative(54, 20, 2, 12) &&
|
||||
isAreaBlackRelative(42, 48, 2, 4) &&
|
||||
isAreaBlackRelative(46, 52, 2, 12));
|
||||
}
|
||||
|
||||
private boolean hasTransparencyRelative(int x0, int y0, int w, int h) {
|
||||
x0 *= scale;
|
||||
y0 *= scale;
|
||||
w *= scale;
|
||||
h *= scale;
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
int pixel = normalizedTexture.getRGB(x0 + x, y0 + y);
|
||||
if (pixel >>> 24 != 0xff) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isAreaBlackRelative(int x0, int y0, int w, int h) {
|
||||
x0 *= scale;
|
||||
y0 *= scale;
|
||||
w *= scale;
|
||||
h *= scale;
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
int pixel = normalizedTexture.getRGB(x0 + x, y0 + y);
|
||||
if (pixel != 0xff000000) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user