feat: support change skin of offline accounts.

This commit is contained in:
huanghongxun 2021-09-25 02:04:25 +08:00
parent 01893b053d
commit cd030c1de0
24 changed files with 672 additions and 192 deletions

View File

@ -1,12 +1,9 @@
package moe.mickey.minecraft.skin.fx;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.PerspectiveCamera;
import javafx.scene.SceneAntialiasing;
import javafx.scene.SubScene;
import javafx.scene.*;
import javafx.scene.image.Image;
import javafx.scene.paint.Color;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.paint.Material;
import javafx.scene.paint.PhongMaterial;
import javafx.scene.shape.Shape3D;
@ -16,9 +13,8 @@ import javafx.scene.transform.Translate;
public class SkinCanvas extends Group {
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/alex.png"));
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/steve.png"));
public static final Image CHOCOLATE = new Image(SkinCanvas.class.getResourceAsStream("/chocolate.png"));
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/alex.png"));
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img//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);
@ -208,7 +204,6 @@ public class SkinCanvas extends Group {
subScene = new SubScene(group, preW, preH, true,
msaa ? SceneAntialiasing.BALANCED : SceneAntialiasing.DISABLED);
subScene.setFill(Color.ALICEBLUE);
subScene.setCamera(camera);
return subScene;
@ -218,4 +213,45 @@ public class SkinCanvas extends Group {
getChildren().add(createSubScene());
}
private double lastX, lastY;
public void enableRotation(double sensitivity) {
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> {
lastX = -1;
lastY = -1;
});
addEventHandler(MouseEvent.MOUSE_DRAGGED, e -> {
if (!(lastX == -1 || lastY == -1)) {
if (e.isAltDown() || e.isControlDown() || e.isShiftDown()) {
if (e.isShiftDown())
zRotate.setAngle(zRotate.getAngle() - (e.getSceneY() - lastY) * sensitivity);
if (e.isAltDown())
yRotate.setAngle(yRotate.getAngle() + (e.getSceneX() - lastX) * sensitivity);
if (e.isControlDown())
xRotate.setAngle(xRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity);
} else {
double yaw = yRotate.getAngle() + (e.getSceneX() - lastX) * sensitivity;
yaw %= 360;
if (yaw < 0)
yaw += 360;
int flagX = yaw < 90 || yaw > 270 ? 1 : -1;
int flagZ = yaw < 180 ? -1 : 1;
double kx = Math.abs(90 - yaw % 180) / 90 * flagX, kz = Math.abs(90 - (yaw + 90) % 180) / 90 * flagZ;
xRotate.setAngle(xRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity * kx);
yRotate.setAngle(yaw);
zRotate.setAngle(zRotate.getAngle() + (e.getSceneY() - lastY) * sensitivity * kz);
}
}
lastX = e.getSceneX();
lastY = e.getSceneY();
});
addEventHandler(ScrollEvent.SCROLL, e -> {
double delta = (e.getDeltaY() > 0 ? 1 : e.getDeltaY() == 0 ? 0 : -1) / 10D * sensitivity;
scale.setX(Math.min(Math.max(scale.getX() - delta, 0.1), 10));
scale.setY(Math.min(Math.max(scale.getY() - delta, 0.1), 10));
});
}
}

View File

@ -20,7 +20,10 @@ package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.*;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
@ -46,6 +49,7 @@ import org.jackhuang.hmcl.util.skin.InvalidSkinException;
import org.jackhuang.hmcl.util.skin.NormalizedSkin;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@ -54,8 +58,6 @@ 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;
@ -135,6 +137,8 @@ public class AccountListItem extends RadioButton {
} else {
return createBooleanBinding(() -> true);
}
} else if (account instanceof OfflineAccount) {
return createBooleanBinding(() -> true);
} else {
return createBooleanBinding(() -> false);
}
@ -145,6 +149,10 @@ public class AccountListItem extends RadioButton {
*/
@Nullable
public Task<?> uploadSkin() {
if (account instanceof OfflineAccount) {
Controllers.dialog(new OfflineAccountSkinPane((OfflineAccount) account));
return null;
}
if (!(account instanceof YggdrasilAccount)) {
return null;
}

View File

@ -523,7 +523,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
return getAuthServer();
} else if (factory instanceof OfflineAccountFactory) {
UUID uuid = txtUUID == null ? null : StringUtils.isBlank(txtUUID.getText()) ? null : UUIDTypeAdapter.fromString(txtUUID.getText());
return new OfflineAccountFactory.AdditionalData(uuid, null, null);
return new OfflineAccountFactory.AdditionalData(uuid, null);
} else {
return null;
}

View File

@ -17,19 +17,184 @@
*/
package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import javafx.scene.layout.StackPane;
import com.jfoenix.controls.JFXTextField;
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.*;
import moe.mickey.minecraft.skin.fx.SkinCanvas;
import moe.mickey.minecraft.skin.fx.animation.SkinAniRunning;
import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.auth.offline.Skin;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*;
import java.io.File;
import java.util.Arrays;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class OfflineAccountSkinPane extends StackPane {
private final OfflineAccount account;
private final MultiFileItem<Skin.Type> skinItem = new MultiFileItem<>();
private final JFXTextField cslApiField = new JFXTextField();
private final FileSelector skinSelector = new FileSelector();
private final FileSelector capeSelector = new FileSelector();
private final InvalidationListener skinBinding;
public OfflineAccountSkinPane(OfflineAccount account) {
this.account = account;
getStyleClass().add("skin-pane");
JFXDialogLayout layout = new JFXDialogLayout();
getChildren().setAll(layout);
layout.setHeading(new Label(i18n("account.skin")));
MultiFileItem<>
BorderPane pane = new BorderPane();
SkinCanvas canvas = new SkinCanvas(SkinCanvas.STEVE, 300, 300, true);
StackPane canvasPane = new StackPane(canvas);
canvasPane.setPrefWidth(300);
canvasPane.setPrefHeight(300);
pane.setCenter(canvas);
canvas.getAnimationplayer().addSkinAnimation(new SkinAniWavingArms(100, 2000, 7.5, canvas), new SkinAniRunning(100, 100, 30, canvas));
canvas.enableRotation(.5);
canvas.addEventHandler(DragEvent.DRAG_OVER, e -> {
if (e.getDragboard().hasFiles()) {
File file = e.getDragboard().getFiles().get(0);
if (file.getAbsolutePath().endsWith(".png"))
e.acceptTransferModes(TransferMode.COPY);
}
});
canvas.addEventHandler(DragEvent.DRAG_DROPPED, e -> {
if (e.isAccepted()) {
File skin = e.getDragboard().getFiles().get(0);
Platform.runLater(() -> {
skinSelector.setValue(skin.getAbsolutePath());
skinItem.setSelectedData(Skin.Type.LOCAL_FILE);
});
}
});
TransitionPane skinOptionPane = new TransitionPane();
skinOptionPane.setMaxWidth(300);
VBox optionPane = new VBox(skinItem, skinOptionPane);
pane.setRight(optionPane);
layout.setBody(pane);
cslApiField.setPromptText(i18n("account.skin.type.csl_api.location.hint"));
cslApiField.setValidators(new URLValidator());
skinItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("message.default"), Skin.Type.DEFAULT),
new MultiFileItem.Option<>("Steve", Skin.Type.STEVE),
new MultiFileItem.Option<>("Alex", Skin.Type.ALEX),
new MultiFileItem.Option<>(i18n("account.skin.type.local_file"), Skin.Type.LOCAL_FILE),
new MultiFileItem.Option<>("LittleSkin", Skin.Type.LITTLE_SKIN),
new MultiFileItem.Option<>(i18n("account.skin.type.csl_api"), Skin.Type.CUSTOM_SKIN_LOADER_API)
));
if (account.getSkin() == null) {
skinItem.setSelectedData(Skin.Type.DEFAULT);
} else {
skinItem.setSelectedData(account.getSkin().getType());
cslApiField.setText(account.getSkin().getCslApi());
skinSelector.setValue(account.getSkin().getLocalSkinPath());
capeSelector.setValue(account.getSkin().getLocalCapePath());
}
skinBinding = FXUtils.observeWeak(() -> {
getSkin().load(account.getUsername())
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
LOG.log(Level.WARNING, "Failed to load skin", exception);
Controllers.showToast(i18n("message.failed"));
} else {
if (result == null || result.getSkin() == null) {
canvas.updateSkin(getDefaultTexture(), isDefaultSlim());
return;
}
canvas.updateSkin(new Image(result.getSkin().getInputStream()), result.getModel() == TextureModel.ALEX);
}
}).start();
}, skinItem.selectedDataProperty(), cslApiField.textProperty(), skinSelector.valueProperty(), capeSelector.valueProperty());
FXUtils.onChangeAndOperate(skinItem.selectedDataProperty(), selectedData -> {
GridPane gridPane = new GridPane();
gridPane.setPadding(new Insets(0, 0, 0, 10));
gridPane.setHgap(16);
gridPane.setVgap(8);
gridPane.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
switch (selectedData) {
case DEFAULT:
case STEVE:
case ALEX:
case LITTLE_SKIN:
break;
case LOCAL_FILE:
gridPane.addRow(0, new Label(i18n("account.skin")), skinSelector);
gridPane.addRow(1, new Label(i18n("account.cape")), capeSelector);
break;
case CUSTOM_SKIN_LOADER_API:
gridPane.addRow(0, new Label(i18n("account.skin.type.csl_api.location")), cslApiField);
break;
}
skinOptionPane.setContent(gridPane, ContainerAnimations.NONE.getAnimationProducer());
});
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.getStyleClass().add("dialog-accept");
acceptButton.setOnAction(e -> {
account.setSkin(getSkin());
fireEvent(new DialogCloseEvent());
});
JFXHyperlink littleSkinLink = new JFXHyperlink(i18n("account.skin.type.little_skin.hint"));
littleSkinLink.setOnAction(e -> FXUtils.openLink("https://mcskin.littleservice.cn/"));
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("dialog-cancel");
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
onEscPressed(this, cancelButton::fire);
layout.setActions(littleSkinLink, acceptButton, cancelButton);
}
private Skin getSkin() {
return new Skin(skinItem.getSelectedData(), cslApiField.getText(), 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;
}
}
}

View File

@ -0,0 +1,105 @@
/*
* 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.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.SVG;
import java.io.File;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class FileSelector extends HBox {
private StringProperty value = new SimpleStringProperty();
private String chooserTitle = i18n("selector.choose_file");
private boolean directory = false;
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
public String getValue() {
return value.get();
}
public StringProperty valueProperty() {
return value;
}
public void setValue(String value) {
this.value.set(value);
}
public String getChooserTitle() {
return chooserTitle;
}
public FileSelector setChooserTitle(String chooserTitle) {
this.chooserTitle = chooserTitle;
return this;
}
public boolean isDirectory() {
return directory;
}
public FileSelector setDirectory(boolean directory) {
this.directory = directory;
return this;
}
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return extensionFilters;
}
public FileSelector() {
JFXTextField customField = new JFXTextField();
customField.textProperty().bindBidirectional(valueProperty());
JFXButton selectButton = new JFXButton();
selectButton.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
selectButton.setOnMouseClicked(e -> {
if (directory) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(chooserTitle);
File dir = chooser.showDialog(Controllers.getStage());
if (dir != null)
customField.setText(dir.getAbsolutePath());
} else {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(chooserTitle);
File file = chooser.showOpenDialog(Controllers.getStage());
if (file != null)
customField.setText(file.getAbsolutePath());
}
});
setAlignment(Pos.CENTER_LEFT);
setSpacing(3);
getChildren().addAll(customField, selectButton);
}
}

View File

@ -17,11 +17,10 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -31,39 +30,28 @@ import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.File;
import java.util.Collection;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiFileItem<T> extends ComponentSublist {
public class MultiFileItem<T> extends VBox {
private final ObjectProperty<T> selectedData = new SimpleObjectProperty<>(this, "selectedData");
private final ObjectProperty<T> fallbackData = new SimpleObjectProperty<>(this, "fallbackData");
private final ToggleGroup group = new ToggleGroup();
private final VBox pane = new VBox();
private Consumer<Toggle> toggleSelectedListener;
@SuppressWarnings("unchecked")
public MultiFileItem() {
pane.setStyle("-fx-padding: 0 0 10 0;");
pane.setSpacing(8);
getContent().add(pane);
setPadding(new Insets(0, 0, 10, 0));
setSpacing(8);
group.selectedToggleProperty().addListener((a, b, newValue) -> {
if (toggleSelectedListener != null)
@ -86,7 +74,7 @@ public class MultiFileItem<T> extends ComponentSublist {
}
public void loadChildren(Collection<Option<T>> options) {
pane.getChildren().setAll(options.stream()
getChildren().setAll(options.stream()
.map(option -> option.createItem(group))
.collect(Collectors.toList()));
}
@ -183,6 +171,7 @@ public class MultiFileItem<T> extends ComponentSublist {
public static class StringOption<T> extends Option<T> {
private StringProperty value = new SimpleStringProperty();
private ValidatorBase[] validators;
public StringOption(String title, T data) {
super(title, data);
@ -205,6 +194,11 @@ public class MultiFileItem<T> extends ComponentSublist {
return this;
}
public StringOption<T> setValidators(ValidatorBase... validators) {
this.validators = validators;
return this;
}
@Override
protected Node createItem(ToggleGroup group) {
BorderPane pane = new BorderPane();
@ -221,6 +215,9 @@ public class MultiFileItem<T> extends ComponentSublist {
BorderPane.setAlignment(customField, Pos.CENTER_RIGHT);
customField.textProperty().bindBidirectional(valueProperty());
customField.disableProperty().bind(left.selectedProperty().not());
if (validators != null) {
customField.setValidators(validators);
}
pane.setRight(customField);
return pane;
@ -228,44 +225,41 @@ public class MultiFileItem<T> extends ComponentSublist {
}
public static class FileOption<T> extends Option<T> {
private StringProperty value = new SimpleStringProperty();
private String chooserTitle = i18n("selector.choose_file");
private boolean directory = false;
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
private FileSelector selector = new FileSelector();
public FileOption(String title, T data) {
super(title, data);
}
public String getValue() {
return value.get();
return selector.getValue();
}
public StringProperty valueProperty() {
return value;
return selector.valueProperty();
}
public void setValue(String value) {
this.value.set(value);
selector.setValue(value);
}
public FileOption<T> setDirectory(boolean directory) {
this.directory = directory;
selector.setDirectory(directory);
return this;
}
public FileOption<T> bindBidirectional(Property<String> property) {
this.value.bindBidirectional(property);
selector.valueProperty().bindBidirectional(property);
return this;
}
public FileOption<T> setChooserTitle(String chooserTitle) {
this.chooserTitle = chooserTitle;
selector.setChooserTitle(chooserTitle);
return this;
}
public ObservableList<FileChooser.ExtensionFilter> getExtensionFilters() {
return extensionFilters;
return selector.getExtensionFilters();
}
@Override
@ -280,36 +274,9 @@ public class MultiFileItem<T> extends ComponentSublist {
left.setUserData(data);
pane.setLeft(left);
JFXTextField customField = new JFXTextField();
customField.textProperty().bindBidirectional(valueProperty());
customField.disableProperty().bind(left.selectedProperty().not());
JFXButton selectButton = new JFXButton();
selectButton.disableProperty().bind(left.selectedProperty().not());
selectButton.setGraphic(SVG.folderOpen(Theme.blackFillBinding(), 15, 15));
selectButton.setOnMouseClicked(e -> {
if (directory) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(chooserTitle);
File dir = chooser.showDialog(Controllers.getStage());
if (dir != null)
customField.setText(dir.getAbsolutePath());
} else {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(chooserTitle);
File file = chooser.showOpenDialog(Controllers.getStage());
if (file != null)
customField.setText(file.getAbsolutePath());
}
});
HBox right = new HBox();
right.setAlignment(Pos.CENTER_RIGHT);
BorderPane.setAlignment(right, Pos.CENTER_RIGHT);
right.setSpacing(3);
right.getChildren().addAll(customField, selectButton);
pane.setRight(right);
selector.disableProperty().bind(left.selectedProperty().not());
BorderPane.setAlignment(selector, Pos.CENTER_RIGHT);
pane.setRight(selector);
return pane;
}
}

View File

@ -0,0 +1,51 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.scene.control.TextInputControl;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class URLValidator extends ValidatorBase {
private final boolean nullable;
public URLValidator() {
this(false);
}
public URLValidator(@NamedArg("nullable") boolean nullable) {
this(i18n("input.url"), nullable);
}
public URLValidator(@NamedArg("message") String message, @NamedArg("nullable") boolean nullable) {
super(message);
this.nullable = nullable;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
evalTextInputField();
}
}
private void evalTextInputField() {
TextInputControl textField = ((TextInputControl) srcControl.get());
if (StringUtils.isBlank(textField.getText()))
hasErrors.set(!nullable);
else {
try {
new URL(textField.getText()).toURI();
hasErrors.set(false);
} catch (IOException | URISyntaxException e) {
hasErrors.set(true);
}
}
}
}

View File

@ -46,9 +46,6 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
@ -243,17 +240,7 @@ public final class ModpackInfoPage extends Control implements WizardPage {
txtModpackFileApi.getValidators().add(new RequiredValidator());
}
txtModpackFileApi.getValidators().add(new Validator(s -> {
if (s.isEmpty()) {
return true;
}
try {
new URL(s).toURI();
return true;
} catch (IOException | URISyntaxException e) {
return false;
}
}));
txtModpackFileApi.getValidators().add(new URLValidator());
pane.addRow(rowIndex++, new Label(i18n("modpack.file_api")), txtModpackFileApi);
}

View File

@ -86,11 +86,13 @@ public class PersonalizationPage extends StackPane {
}
{
StackPane componentList = new StackPane();
ComponentList componentList = new ComponentList();
MultiFileItem<EnumBackgroundImage> backgroundItem = new MultiFileItem<>();
backgroundItem.setTitle(i18n("launcher.background"));
backgroundItem.setHasSubtitle(true);
ComponentSublist backgroundSublist = new ComponentSublist();
backgroundSublist.getContent().add(backgroundItem);
backgroundSublist.setTitle(i18n("launcher.background"));
backgroundSublist.setHasSubtitle(true);
backgroundItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT),
@ -102,12 +104,12 @@ public class PersonalizationPage extends StackPane {
.bindBidirectional(config().backgroundImageUrlProperty())
));
backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
backgroundItem.subtitleProperty().bind(
backgroundSublist.subtitleProperty().bind(
new When(backgroundItem.selectedDataProperty().isEqualTo(EnumBackgroundImage.DEFAULT))
.then(i18n("launcher.background.default"))
.otherwise(config().backgroundImageProperty()));
componentList.getChildren().add(backgroundItem);
componentList.getContent().add(backgroundItem);
content.getChildren().addAll(ComponentList.createComponentListTitle(i18n("launcher.background")), componentList);
}

View File

@ -64,7 +64,7 @@ public final class SettingsPage extends SettingsView {
// ====
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
fileCommonLocation.subtitleProperty().bind(
fileCommonLocationSublist.subtitleProperty().bind(
Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory())
.orElse(i18n("launcher.cache_directory.disabled")),
config().commonDirectoryProperty(), config().commonDirTypeProperty()));

View File

@ -48,6 +48,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class SettingsView extends StackPane {
protected final JFXComboBox<SupportedLocale> cboLanguage;
protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation;
protected final ComponentSublist fileCommonLocationSublist;
protected final Label lblUpdate;
protected final Label lblUpdateSub;
protected final JFXRadioButton chkUpdateStable;
@ -144,8 +145,10 @@ public abstract class SettingsView extends StackPane {
{
fileCommonLocation = new MultiFileItem<>();
fileCommonLocation.setTitle(i18n("launcher.cache_directory"));
fileCommonLocation.setHasSubtitle(true);
fileCommonLocationSublist = new ComponentSublist();
fileCommonLocationSublist.getContent().add(fileCommonLocation);
fileCommonLocationSublist.setTitle(i18n("launcher.cache_directory"));
fileCommonLocationSublist.setHasSubtitle(true);
fileCommonLocation.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
@ -159,10 +162,10 @@ public abstract class SettingsView extends StackPane {
cleanButton.setOnMouseClicked(e -> clearCacheDirectory());
cleanButton.getStyleClass().add("jfx-button-border");
fileCommonLocation.setHeaderRight(cleanButton);
fileCommonLocationSublist.setHeaderRight(cleanButton);
}
settingsPane.getContent().add(fileCommonLocation);
settingsPane.getContent().add(fileCommonLocationSublist);
}
{

View File

@ -99,10 +99,13 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
private final OptionToggleButton noJVMCheckPane;
private final OptionToggleButton useNativeGLFWPane;
private final OptionToggleButton useNativeOpenALPane;
private final ComponentSublist javaSublist;
private final MultiFileItem<JavaVersion> javaItem;
private final MultiFileItem.FileOption<JavaVersion> javaCustomOption;
private final ComponentSublist gameDirSublist;
private final MultiFileItem<GameDirectoryType> gameDirItem;
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
private final ComponentSublist nativesDirSublist;
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
private final JFXComboBox<ProcessPriority> cboProcessPriority;
@ -192,14 +195,18 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
componentList.setDepth(1);
javaItem = new MultiFileItem<>();
javaItem.setTitle(i18n("settings.game.java_directory"));
javaItem.setHasSubtitle(true);
javaSublist = new ComponentSublist();
javaSublist.getContent().add(javaItem);
javaSublist.setTitle(i18n("settings.game.java_directory"));
javaSublist.setHasSubtitle(true);
javaCustomOption = new MultiFileItem.FileOption<JavaVersion>(i18n("settings.custom"), null)
.setChooserTitle(i18n("settings.game.java_directory.choose"));
gameDirItem = new MultiFileItem<>();
gameDirItem.setTitle(i18n("settings.game.working_directory"));
gameDirItem.setHasSubtitle(true);
gameDirSublist = new ComponentSublist();
gameDirSublist.getContent().add(gameDirItem);
gameDirSublist.setTitle(i18n("settings.game.working_directory"));
gameDirSublist.setHasSubtitle(true);
gameDirItem.disableProperty().bind(modpack);
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
.setChooserTitle(i18n("settings.game.working_directory.choose"))
@ -395,7 +402,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP);
}
componentList.getContent().setAll(javaItem, gameDirItem, maxMemoryPane, launcherVisibilityPane, dimensionPane, showLogsPane, processPriorityPane, serverPane);
componentList.getContent().setAll(javaSublist, gameDirSublist, maxMemoryPane, launcherVisibilityPane, dimensionPane, showLogsPane, processPriorityPane, serverPane);
}
HBox advancedHintPane = new HBox();
@ -474,8 +481,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
workaroundPane.disableProperty().bind(enableSpecificSettings.not());
{
nativesDirItem = new MultiFileItem<>();
nativesDirItem.setTitle(i18n("settings.advanced.natives_directory"));
nativesDirItem.setHasSubtitle(true);
nativesDirSublist = new ComponentSublist();
nativesDirSublist.getContent().add(nativesDirItem);
nativesDirSublist.setTitle(i18n("settings.advanced.natives_directory"));
nativesDirSublist.setHasSubtitle(true);
nativesDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), NativesDirectoryType.CUSTOM)
.setChooserTitle(i18n("settings.advanced.natives_directory.choose"))
.setDirectory(true);
@ -499,7 +508,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
useNativeOpenALPane = new OptionToggleButton();
useNativeOpenALPane.setTitle(i18n("settings.advanced.use_native_openal"));
workaroundPane.getContent().setAll(nativesDirItem, noJVMArgsPane, noGameCheckPane, noJVMCheckPane, useNativeGLFWPane, useNativeOpenALPane);
workaroundPane.getContent().setAll(nativesDirSublist, noJVMArgsPane, noGameCheckPane, noJVMCheckPane, useNativeGLFWPane, useNativeOpenALPane);
}
rootPane.getChildren().addAll(componentList,
@ -613,10 +622,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
lastVersionSetting.javaProperty().removeListener(javaListener);
gameDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().unbind();
gameDirSublist.subtitleProperty().unbind();
nativesDirItem.selectedDataProperty().unbindBidirectional(lastVersionSetting.nativesDirTypeProperty());
nativesDirItem.subtitleProperty().unbind();
nativesDirSublist.subtitleProperty().unbind();
}
// unbind data fields
@ -663,11 +672,11 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
versionSetting.javaProperty().addListener(javaListener);
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
gameDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
nativesDirItem.selectedDataProperty().bindBidirectional(versionSetting.nativesDirTypeProperty());
nativesDirItem.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
nativesDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
lastVersionSetting = versionSetting;
@ -703,7 +712,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
if (versionSetting == null)
return;
Task.supplyAsync(versionSetting::getJavaVersion)
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaItem.setSubtitle(Optional.ofNullable(javaVersion)
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion)
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path")))
.start();
}

View File

@ -91,6 +91,10 @@
-fx-fill: #856404;
}
.skin-pane .jfx-text-field {
-fx-pref-width: 200;
}
.memory-label {
}

View File

@ -50,6 +50,7 @@ about.open_source=Open Source
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=Accounts
account.cape=Cape
account.character=character
account.choose=Choose a character
account.create=Create a new account
@ -110,7 +111,13 @@ account.missing=No Account
account.missing.add=Click here to add
account.not_logged_in=Not logged in
account.password=Password
account.skin.file=Skin file
account.skin=Skin
account.skin.file=Skin image file
account.skin.type.csl_api=Blessing Skin
account.skin.type.csl_api.location=Address
account.skin.type.csl_api.location.hint=CustomSkinAPI
account.skin.type.little_skin.hint=LittleSkin
account.skin.type.local_file=Local skin image file
account.skin.upload=Upload skin
account.skin.upload.failed=Failed to upload skin
account.skin.invalid_skin=Unrecognized skin file
@ -459,6 +466,7 @@ main_page=Home
message.cancelled=Operation was cancelled
message.confirm=Confirm
message.copied=Copied to clipboard
message.default=Default
message.doing=Please wait
message.downloading=Downloading...
message.error=Error

View File

@ -35,14 +35,14 @@ about.thanks_to=鳴謝
about.thanks_to.bangbang93.statement=提供 BMCLAPI 下載源,請贊助支持 BMCLAPI
about.thanks_to.contributors=所有通過 Issues、Pull Requests 等管道參與本項目的貢獻者
about.thanks_to.contributors.statement=沒有開源社區的支持Hello Minecraft! Launcher 無法走到今天
about.thanks_to.gamerteam.statement=提供默認背景圖
about.thanks_to.gamerteam.statement=提供預設背景圖
about.thanks_to.mcbbs=MCBBS 我的世界中文論壇
about.thanks_to.mcbbs.statement=提供 MCBBS 下載源
about.thanks_to.mcmod=MC 百科
about.thanks_to.mcmod.statement=提供 Mod 中文名映射表與 Mod 百科
about.thanks_to.noin=這裡 (noin.cn)
about.thanks_to.noin.statement=提供多人聯機服務 (cato - ioi 系列作品)
about.thanks_to.red_lnn.statement=提供默認背景圖
about.thanks_to.red_lnn.statement=提供預設背景圖
about.thanks_to.users=HMCL 用戶群成員
about.thanks_to.users.statement=感謝用戶群成員贊助充電、積極催更、迴響問題、出謀劃策
about.thanks_to.yushijinhun.statement=authlib-injector 相关支援
@ -50,6 +50,7 @@ about.open_source=開放原始碼
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=帳戶
account.cape=披風
account.character=角色
account.choose=請選擇角色
account.create=建立帳戶
@ -110,7 +111,13 @@ account.missing=沒有遊戲帳戶
account.missing.add=按一下此處加入帳戶
account.not_logged_in=未登入
account.password=密碼
account.skin=皮膚
account.skin.file=皮膚圖片檔案
account.skin.type.csl_api=Blessing Skin 伺服器
account.skin.type.csl_api.location=伺服器位址
account.skin.type.csl_api.location.hint=CustomSkinAPI 位址
account.skin.type.little_skin.hint=LittleSkin 皮膚站
account.skin.type.local_file=本地皮膚圖片檔案
account.skin.upload=上傳皮膚
account.skin.upload.failed=皮膚上傳失敗
account.skin.invalid_skin=無法識別的皮膚檔案
@ -459,6 +466,7 @@ main_page=首頁
message.cancelled=操作被取消
message.confirm=提示
message.copied=已複製到剪貼板
message.default=預設
message.doing=請耐心等待
message.downloading=正在下載…
message.error=錯誤
@ -708,14 +716,14 @@ settings.advanced.java_permanent_generation_space=記憶體永久儲存區域
settings.advanced.java_permanent_generation_space.prompt=格式: MB
settings.advanced.jvm=Java 虛擬機設定
settings.advanced.jvm_args=Java 虛擬機參數
settings.advanced.jvm_args.prompt=填寫此處可以覆蓋默認設定
settings.advanced.jvm_args.prompt=填寫此處可以覆蓋預設設定
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
settings.advanced.launcher_visibility.hide_and_reopen=隱藏啟動器並在遊戲結束後重新開啟
settings.advanced.launcher_visibility.keep=不隱藏啟動器
settings.advanced.launcher_visible=啟動器可見性
settings.advanced.minecraft_arguments=Minecraft 額外參數
settings.advanced.minecraft_arguments.prompt=默認
settings.advanced.minecraft_arguments.prompt=預設
settings.advanced.natives_directory=本地庫路徑LWJGL
settings.advanced.natives_directory.choose=選擇本地庫路徑
settings.advanced.natives_directory.default=預設(.minecraft/versions/<版本名>/natives/
@ -731,7 +739,7 @@ settings.advanced.process_priority.high=高(優先保證遊戲運行,但可
settings.advanced.post_exit_command=遊戲結束後執行命令
settings.advanced.post_exit_command.prompt=將在遊戲結束後呼叫使用
settings.advanced.server_ip=伺服器位址
settings.advanced.server_ip.prompt=默認,啟動遊戲後直接進入對應伺服器
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
settings.advanced.use_native_glfw=[Linux] 使用系統 GLFW
settings.advanced.use_native_openal=[Linux] 使用系統 OpenAL
settings.advanced.workaround=除錯選項

View File

@ -50,6 +50,7 @@ about.open_source=开源
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL/)
account=帐户
account.cape=披风
account.character=角色
account.choose=选择一个角色
account.create=添加帐户
@ -110,7 +111,13 @@ account.missing=没有游戏帐户
account.missing.add=点击此处添加帐户
account.not_logged_in=未登录
account.password=密码
account.skin=皮肤
account.skin.file=皮肤图片文件
account.skin.type.csl_api=Blessing Skin 服务器
account.skin.type.csl_api.location=服务器地址
account.skin.type.csl_api.location.hint=CustomSkinAPI 地址
account.skin.type.little_skin.hint=LittleSkin 皮肤站
account.skin.type.local_file=本地皮肤图片文件
account.skin.upload=上传皮肤
account.skin.upload.failed=皮肤上传失败
account.skin.invalid_skin=无法识别的皮肤文件
@ -459,6 +466,7 @@ main_page=主页
message.cancelled=操作被取消
message.confirm=提示
message.copied=已复制到剪贴板
message.default=默认
message.doing=请耐心等待
message.downloading=正在下载
message.error=错误

View File

@ -1,7 +1,5 @@
package moe.mickey.minecraft.skin.fx.test;
import java.util.function.Consumer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
@ -11,12 +9,14 @@ import moe.mickey.minecraft.skin.fx.SkinCanvasSupport;
import moe.mickey.minecraft.skin.fx.animation.SkinAniRunning;
import moe.mickey.minecraft.skin.fx.animation.SkinAniWavingArms;
import java.util.function.Consumer;
public class Test extends Application {
public static final String TITLE = "FX - Minecraft skin preview";
public static SkinCanvas createSkinCanvas() {
SkinCanvas canvas = new SkinCanvas(SkinCanvas.CHOCOLATE, 400, 400, true);
SkinCanvas canvas = new SkinCanvas(SkinCanvas.STEVE, 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;

View File

@ -17,13 +17,14 @@
*/
package org.jackhuang.hmcl.auth.offline;
import javafx.beans.binding.ObjectBinding;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.AuthenticationException;
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.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.StringUtils;
@ -51,13 +52,13 @@ public class OfflineAccount extends Account {
private final AuthlibInjectorArtifactProvider downloader;
private final String username;
private final UUID uuid;
private final Map<TextureType, Texture> textures;
private Skin skin;
protected OfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, Map<TextureType, Texture> textures) {
protected OfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, Skin skin) {
this.downloader = requireNonNull(downloader);
this.username = requireNonNull(username);
this.uuid = requireNonNull(uuid);
this.textures = textures;
this.skin = skin;
if (StringUtils.isBlank(username)) {
throw new IllegalArgumentException("Username cannot be blank");
@ -79,11 +80,20 @@ public class OfflineAccount extends Account {
return username;
}
public Skin getSkin() {
return skin;
}
public void setSkin(Skin skin) {
this.skin = skin;
invalidate();
}
@Override
public AuthInfo logIn() throws AuthenticationException {
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
if (skin != null || cape != null) {
if (skin != null) {
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
try {
return downloader.getArtifactInfo();
@ -109,18 +119,14 @@ public class OfflineAccount extends Account {
try {
YggdrasilServer server = new YggdrasilServer(0);
server.start();
server.addCharacter(new YggdrasilServer.Character(uuid, username, TextureModel.STEVE,
mapOf(
pair(TextureType.SKIN, server.loadTexture(skin)),
pair(TextureType.CAPE, server.loadTexture(cape))
)));
server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run()));
return authInfo.withArguments(new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + "http://localhost:" + server.getListeningPort(),
"-Dauthlibinjector.side=client"
))
.withCloseable(server::stop);
} catch (IOException e) {
} catch (Exception e) {
throw new AuthenticationException(e);
}
} else {
@ -143,18 +149,20 @@ public class OfflineAccount extends Account {
return mapOf(
pair("uuid", UUIDTypeAdapter.fromUUID(uuid)),
pair("username", username),
pair("skin", skin),
pair("cape", cape)
pair("skin", skin.toStorage())
);
}
@Override
public ObjectBinding<Optional<Map<TextureType, Texture>>> getTextures() {
return super.getTextures();
}
@Override
public String toString() {
return new ToStringBuilder(this)
.append("username", username)
.append("uuid", uuid)
.append("skin", skin)
.append("cape", cape)
.toString();
}

View File

@ -45,25 +45,23 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
}
public OfflineAccount create(String username, UUID uuid) {
return new OfflineAccount(downloader, username, uuid, null, null);
return new OfflineAccount(downloader, username, uuid, null);
}
@Override
public OfflineAccount create(CharacterSelector selector, String username, String password, ProgressCallback progressCallback, Object additionalData) {
AdditionalData data;
UUID uuid;
String skin;
String cape;
Skin skin;
if (additionalData != null) {
data = (AdditionalData) additionalData;
uuid = data.uuid == null ? getUUIDFromUserName(username) : data.uuid;
skin = data.skin;
cape = data.cape;
} else {
uuid = getUUIDFromUserName(username);
skin = cape = null;
skin = null;
}
return new OfflineAccount(downloader, username, uuid, skin, cape);
return new OfflineAccount(downloader, username, uuid, skin);
}
@Override
@ -73,10 +71,9 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
UUID uuid = tryCast(storage.get("uuid"), String.class)
.map(UUIDTypeAdapter::fromString)
.orElse(getUUIDFromUserName(username));
String skin = tryCast(storage.get("skin"), String.class).orElse(null);
String cape = tryCast(storage.get("cape"), String.class).orElse(null);
Skin skin = Skin.fromStorage(tryCast(storage.get("skin"), Map.class).orElse(null));
return new OfflineAccount(downloader, username, uuid, skin, cape);
return new OfflineAccount(downloader, username, uuid, skin);
}
public static UUID getUUIDFromUserName(String username) {
@ -85,13 +82,11 @@ public final class OfflineAccountFactory extends AccountFactory<OfflineAccount>
public static class AdditionalData {
private final UUID uuid;
private final String skin;
private final String cape;
private final Skin skin;
public AdditionalData(UUID uuid, String skin, String cape) {
public AdditionalData(UUID uuid, Skin skin) {
this.uuid = uuid;
this.skin = skin;
this.cape = cape;
}
}

View File

@ -18,26 +18,31 @@
package org.jackhuang.hmcl.auth.offline;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.task.GetTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.tryCast;
import static org.jackhuang.hmcl.util.Pair.pair;
public class Skin {
@ -46,33 +51,81 @@ public class Skin {
STEVE,
ALEX,
LOCAL_FILE,
LITTLE_SKIN,
CUSTOM_SKIN_LOADER_API,
YGGDRASIL_API
YGGDRASIL_API;
public static Type fromStorage(String type) {
switch (type) {
case "default":
return DEFAULT;
case "steve":
return STEVE;
case "alex":
return ALEX;
case "local_file":
return LOCAL_FILE;
case "little_skin":
return LITTLE_SKIN;
case "custom_skin_loader_api":
return CUSTOM_SKIN_LOADER_API;
case "yggdrasil_api":
return YGGDRASIL_API;
default:
return null;
}
}
}
private Type type;
private String value;
private final Type type;
private final String cslApi;
private final String localSkinPath;
private final String localCapePath;
public Skin(Type type, String cslApi, String localSkinPath, String localCapePath) {
this.type = type;
this.cslApi = cslApi;
this.localSkinPath = localSkinPath;
this.localCapePath = localCapePath;
}
public Type getType() {
return type;
}
public String getValue() {
return value;
public String getCslApi() {
return cslApi;
}
public Task<Texture> toTexture(String username) {
public String getLocalSkinPath() {
return localSkinPath;
}
public String getLocalCapePath() {
return localCapePath;
}
public Task<LoadedSkin> load(String username) {
switch (type) {
case DEFAULT:
return Task.supplyAsync(() -> null);
case STEVE:
return Task.supplyAsync(() -> Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/steve.png")));
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/steve.png")), null));
case ALEX:
return Task.supplyAsync(() -> Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/alex.png")));
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.ALEX, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/alex.png")), null));
case LOCAL_FILE:
return Task.supplyAsync(() -> Texture.loadTexture(Files.newInputStream(Paths.get(value))));
return Task.supplyAsync(() -> {
Texture skin = null, cape = null;
Optional<Path> skinPath = FileUtils.tryGetPath(localSkinPath);
Optional<Path> capePath = FileUtils.tryGetPath(localCapePath);
if (skinPath.isPresent()) skin = Texture.loadTexture(Files.newInputStream(skinPath.get()));
if (capePath.isPresent()) cape = Texture.loadTexture(Files.newInputStream(capePath.get()));
return new LoadedSkin(TextureModel.STEVE, skin, cape);
});
case LITTLE_SKIN:
case CUSTOM_SKIN_LOADER_API:
return Task.composeAsync(() -> new GetTask(new URL(String.format("%s/%s.json", value, username))))
String realCslApi = type == Type.LITTLE_SKIN ? "http://mcskin.littleservice.cn" : cslApi;
return Task.composeAsync(() -> new GetTask(new URL(String.format("%s/%s.json", realCslApi, username))))
.thenComposeAsync(json -> {
SkinJson result = JsonUtils.GSON.fromJson(json, SkinJson.class);
@ -80,13 +133,57 @@ public class Skin {
return Task.supplyAsync(() -> null);
}
return new FetchBytesTask(new URL(String.format("%s/textures/%s", value, result.getHash())), 3);
}).thenApplyAsync(Texture::loadTexture);
return Task.allOf(
Task.supplyAsync(result::getModel),
result.getHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(String.format("%s/textures/%s", realCslApi, result.getHash())), 3),
result.getCapeHash() == null ? Task.supplyAsync(() -> null) : new FetchBytesTask(new URL(String.format("%s/textures/%s", realCslApi, result.getCapeHash())), 3)
);
}).thenApplyAsync(result -> {
if (result == null) {
return null;
}
Texture skin, cape;
if (result.get(1) != null) {
skin = Texture.loadTexture((InputStream) result.get(1));
} else {
skin = null;
}
if (result.get(2) != null) {
cape = Texture.loadTexture((InputStream) result.get(2));
} else {
cape = null;
}
return new LoadedSkin((TextureModel) result.get(0), skin, cape);
});
default:
throw new UnsupportedOperationException();
}
}
public Map<?, ?> toStorage() {
return mapOf(
pair("type", type.name().toLowerCase(Locale.ROOT)),
pair("cslApi", cslApi),
pair("localSkinPath", localSkinPath),
pair("localCapePath", localCapePath)
);
}
public static Skin fromStorage(Map<?, ?> storage) {
if (storage == null) return null;
Type type = tryCast(storage.get("type"), String.class).flatMap(t -> Optional.ofNullable(Type.fromStorage(t)))
.orElse(Type.DEFAULT);
String cslApi = tryCast(storage.get("cslApi"), String.class).orElse(null);
String localSkinPath = tryCast(storage.get("localSkinPath"), String.class).orElse(null);
String localCapePath = tryCast(storage.get("localCapePath"), String.class).orElse(null);
return new Skin(type, cslApi, localSkinPath, localCapePath);
}
private static class FetchBytesTask extends FetchTask<InputStream> {
public FetchBytesTask(URL url, int retry) {
@ -127,6 +224,30 @@ public class Skin {
}
}
public static class LoadedSkin {
private final TextureModel model;
private final Texture skin;
private final Texture cape;
public LoadedSkin(TextureModel model, Texture skin, Texture cape) {
this.model = model;
this.skin = skin;
this.cape = cape;
}
public TextureModel getModel() {
return model;
}
public Texture getSkin() {
return skin;
}
public Texture getCape() {
return cape;
}
}
private static class SkinJson {
private final String username;
private final String skin;
@ -183,6 +304,12 @@ public class Skin {
return null;
}
public String getCapeHash() {
if (textures != null && textures.cape != null) {
return textures.cape;
} else return cape;
}
public static class TextureJson {
@SerializedName("default")
private final String defaultSkin;

View File

@ -42,6 +42,10 @@ public class Texture {
this.data = requireNonNull(data);
}
public byte[] getData() {
return data;
}
public String getHash() {
return hash;
}

View File

@ -19,8 +19,6 @@ package org.jackhuang.hmcl.auth.offline;
import com.google.gson.reflect.TypeToken;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
import org.jackhuang.hmcl.util.KeyUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@ -36,7 +34,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
@ -143,14 +140,12 @@ public class YggdrasilServer extends HttpServer {
public static class Character {
private final UUID uuid;
private final String name;
private final TextureModel model;
private final Map<TextureType, Texture> textures;
private final Skin.LoadedSkin skin;
public Character(UUID uuid, String name, TextureModel model, Map<TextureType, Texture> textures) {
public Character(UUID uuid, String name, Skin.LoadedSkin skin) {
this.uuid = uuid;
this.name = name;
this.model = model;
this.textures = textures;
this.skin = Objects.requireNonNull(skin);
}
public UUID getUUID() {
@ -161,30 +156,17 @@ public class YggdrasilServer extends HttpServer {
return name;
}
public TextureModel getModel() {
return model;
}
public Map<TextureType, Texture> getTextures() {
return textures;
}
private Map<String, Object> createKeyValue(String key, String value) {
return mapOf(
pair("name", key),
pair("value", value)
);
}
public GameProfile toSimpleResponse() {
return new GameProfile(uuid, name);
}
public Object toCompleteResponse(String rootUrl) {
Map<String, Object> realTextures = new HashMap<>();
for (Map.Entry<TextureType, Texture> textureEntry : textures.entrySet()) {
if (textureEntry.getValue() == null) continue;
realTextures.put(textureEntry.getKey().name(), mapOf(pair("url", rootUrl + "/textures/" + textureEntry.getValue().getHash())));
if (skin.getSkin() != null) {
realTextures.put("SKIN", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash())));
}
if (skin.getCape() != null) {
realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash())));
}
Map<String, Object> textureResponse = mapOf(

View File

@ -340,7 +340,7 @@ public abstract class Task<T> {
messageUpdate.accept(newMessage);
}
public final void run() throws Exception {
public final T run() throws Exception {
if (getSignificance().shouldLog())
Logging.LOG.log(Level.FINE, "Executing task: " + getName());
@ -350,6 +350,8 @@ public abstract class Task<T> {
for (Task<?> task : getDependencies())
doSubTask(task);
onDone.fireEvent(new TaskEvent(this, this, false));
return getResult();
}
private void doSubTask(Task<?> task) throws Exception {

View File

@ -413,6 +413,7 @@ public final class FileUtils {
}
public static Optional<Path> tryGetPath(String first, String... more) {
if (first == null) return Optional.empty();
try {
return Optional.of(Paths.get(first, more));
} catch (InvalidPathException e) {