diff --git a/build.gradle b/build.gradle index a0e2bf2..3eaa8b9 100644 --- a/build.gradle +++ b/build.gradle @@ -42,4 +42,9 @@ options.addStringOption('Xdoclint:none', '-quiet') } } + + tasks.withType(JavaExec) { + workingDir = new File(rootDir, "run/") + workingDir.mkdirs() + } } diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/NewAccountList.java b/launcher/src/main/java/com/skcraft/launcher/auth/NewAccountList.java index c88419f..6081252 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/NewAccountList.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/NewAccountList.java @@ -5,7 +5,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.skcraft.launcher.dialog.component.ListListenerReducer; import com.skcraft.launcher.persistence.Scrambled; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; import org.apache.commons.lang.RandomStringUtils; import javax.swing.*; @@ -17,7 +19,9 @@ * Persisted account list */ @Scrambled("ACCOUNT_LIST_NOT_SECURITY!") -@Data +@Getter +@Setter +@ToString @JsonIgnoreProperties(ignoreUnknown = true) public class NewAccountList implements ListModel { private List accounts = Lists.newArrayList(); diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java b/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java index 0431b2c..4570ae9 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/OfflineSession.java @@ -63,6 +63,11 @@ } @Override + public String getAvatarImage() { + return null; + } + + @Override public boolean isOnline() { return false; } diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/SavedSession.java b/launcher/src/main/java/com/skcraft/launcher/auth/SavedSession.java index 5dffd88..33430ff 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/SavedSession.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/SavedSession.java @@ -1,9 +1,12 @@ package com.skcraft.launcher.auth; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Data; import org.apache.commons.lang.builder.HashCodeBuilder; +import java.util.Base64; + /** * Represents a session saved to disk. */ @@ -15,6 +18,12 @@ private String username; private String accessToken; private String refreshToken; + private String avatarImage; + + @JsonIgnore + public byte[] getAvatarBytes() { + return Base64.getDecoder().decode(avatarImage); + } @Override public boolean equals(Object o) { diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/Session.java b/launcher/src/main/java/com/skcraft/launcher/auth/Session.java index 00d75a5..326dd93 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/Session.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/Session.java @@ -65,6 +65,13 @@ UserType getUserType(); /** + * Get the user's avatar + * + * @return User's avatar as a base64 string. + */ + String getAvatarImage(); + + /** * Return true if the user is in an online session. * * @return true if online @@ -82,6 +89,7 @@ savedSession.setUsername(getName()); savedSession.setUuid(getUuid()); savedSession.setAccessToken(getAccessToken()); + savedSession.setAvatarImage(getAvatarImage()); return savedSession; } diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/VisageSkinService.java b/launcher/src/main/java/com/skcraft/launcher/auth/VisageSkinService.java new file mode 100644 index 0000000..a1b9136 --- /dev/null +++ b/launcher/src/main/java/com/skcraft/launcher/auth/VisageSkinService.java @@ -0,0 +1,22 @@ +package com.skcraft.launcher.auth; + +import com.skcraft.launcher.util.HttpRequest; + +import java.io.IOException; +import java.util.Base64; + +import static com.skcraft.launcher.util.HttpRequest.url; + +public class VisageSkinService { + public static String fetchSkinHead(String uuid) throws IOException, InterruptedException { + String skinUrl = String.format("https://visage.surgeplay.com/face/32/%s.png", uuid); + + byte[] skinBytes = HttpRequest.get(url(skinUrl)) + .execute() + .expectResponseCode(200) + .returnContent() + .asBytes(); + + return Base64.getEncoder().encodeToString(skinBytes); + } +} diff --git a/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java b/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java index 43bf72c..35911b8 100644 --- a/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java +++ b/launcher/src/main/java/com/skcraft/launcher/auth/YggdrasilLoginService.java @@ -9,7 +9,7 @@ import com.fasterxml.jackson.annotation.*; import com.skcraft.launcher.util.HttpRequest; import lombok.Data; -import lombok.NonNull; +import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.extern.java.Log; @@ -22,27 +22,17 @@ * Creates authenticated sessions using the Mojang Yggdrasil login protocol. */ @Log +@RequiredArgsConstructor public class YggdrasilLoginService implements LoginService { private final URL authUrl; private final String clientId; - /** - * Create a new login service with the given authentication URL. - * - * @param authUrl the authentication URL - * @param clientId - */ - public YggdrasilLoginService(@NonNull URL authUrl, String clientId) { - this.authUrl = authUrl; - this.clientId = clientId; - } - public Session login(String agent, String id, String password) throws IOException, InterruptedException, AuthenticationException { AuthenticatePayload payload = new AuthenticatePayload(new Agent(agent), id, password, clientId); - return call(this.authUrl, payload); + return call(this.authUrl, payload, null); } @Override @@ -50,10 +40,10 @@ throws IOException, InterruptedException, AuthenticationException { RefreshPayload payload = new RefreshPayload(savedSession.getAccessToken(), clientId); - return call(new URL(this.authUrl, "/refresh"), payload); + return call(new URL(this.authUrl, "/refresh"), payload, savedSession); } - private Session call(URL url, Object payload) + private Session call(URL url, Object payload, SavedSession previous) throws IOException, InterruptedException, AuthenticationException { HttpRequest req = HttpRequest .post(url) @@ -66,8 +56,15 @@ throw new AuthenticationException(error.getErrorMessage(), error.getErrorMessage()); } else { AuthenticateResponse response = req.returnContent().asJson(AuthenticateResponse.class); + Profile profile = response.getSelectedProfile(); - return response.getSelectedProfile(); + if (previous == null || previous.getAvatarImage() == null) { + profile.setAvatarImage(VisageSkinService.fetchSkinHead(profile.getUuid())); + } else { + profile.setAvatarImage(previous.getAvatarImage()); + } + + return profile; } } @@ -117,6 +114,7 @@ @JsonProperty("id") private String uuid; private String name; private boolean legacy; + private String avatarImage; @JsonIgnore private final Map userProperties = Collections.emptyMap(); @JsonBackReference private AuthenticateResponse response; diff --git a/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java b/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java index b43a625..3199ec8 100644 --- a/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java +++ b/launcher/src/main/java/com/skcraft/launcher/dialog/AccountSelectDialog.java @@ -17,16 +17,13 @@ import javax.swing.*; import java.awt.*; -import java.net.URL; import java.util.concurrent.Callable; -import static com.skcraft.launcher.util.HttpRequest.url; - public class AccountSelectDialog extends JDialog { private final JList accountList; private final JButton loginButton = new JButton(SharedLocale.tr("login.login")); private final JButton cancelButton = new JButton(SharedLocale.tr("button.cancel")); - private final JButton addAccountButton = new JButton(SharedLocale.tr("accounts.addNew")); + private final JButton addAccountButton = new JButton(SharedLocale.tr("accounts.addMojang")); private final LinedBoxPanel buttonsPanel = new LinedBoxPanel(true); private final Launcher launcher; @@ -82,6 +79,7 @@ if (newSession != null) { launcher.getAccounts().add(newSession.toSavedSession()); + setResult(newSession); } }); } @@ -117,7 +115,6 @@ Futures.addCallback(future, new FutureCallback() { @Override public void onSuccess(Session result) { -// session.setAccessToken(result.getAccessToken()); setResult(result); } @@ -161,9 +158,11 @@ @Override public Component getListCellRendererComponent(JList list, SavedSession value, int index, boolean isSelected, boolean cellHasFocus) { setText(value.getUsername()); - - URL avatarUrl = url("https://visage.surgeplay.com/face/24/" + value.getUuid() + ".png"); - setIcon(new ImageIcon(avatarUrl)); + if (value.getAvatarImage() != null) { + setIcon(new ImageIcon(value.getAvatarBytes())); + } else { + setIcon(SwingHelper.createIcon(Launcher.class, "default_skin.png", 32, 32)); + } if (isSelected) { setOpaque(true); diff --git a/launcher/src/main/resources/com/skcraft/launcher/default_skin.png b/launcher/src/main/resources/com/skcraft/launcher/default_skin.png new file mode 100644 index 0000000..4c885f1 --- /dev/null +++ b/launcher/src/main/resources/com/skcraft/launcher/default_skin.png Binary files differ diff --git a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties index 18e99bc..2554685 100644 --- a/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties +++ b/launcher/src/main/resources/com/skcraft/launcher/lang/Launcher.properties @@ -89,7 +89,8 @@ accounts.title=Select the account to play with accounts.refreshingStatus=Refreshing login session... -accounts.addNew=Add a new account... +accounts.addMojang=Log in via Mojang +accounts.addMicrosoft=Log in via Microsoft login.rememberId=Remember my account in the list login.rememberPassword=Remember my password