diff --git a/src/hdskins/java/com/minelittlepony/gui/Button.java b/src/hdskins/java/com/minelittlepony/gui/Button.java index 9579d240..f9aa2c6c 100644 --- a/src/hdskins/java/com/minelittlepony/gui/Button.java +++ b/src/hdskins/java/com/minelittlepony/gui/Button.java @@ -36,8 +36,14 @@ public class Button extends GuiButton implements IActionable, IGuiTooltipped getTooltip() { + return tooltip; + } + @Override public void renderToolTip(Minecraft mc, int mouseX, int mouseY) { + List tooltip = getTooltip(); + if (visible && isMouseOver() && tooltip != null) { mc.currentScreen.drawHoveringText(tooltip, mouseX + tipX, mouseY + tipY); } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/SkinUploader.java b/src/hdskins/java/com/voxelmodpack/hdskins/SkinUploader.java index cfb0fa26..9fa67f10 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/SkinUploader.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/SkinUploader.java @@ -15,6 +15,7 @@ import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.minecraft.MinecraftProfileTexture.Type; import com.mumfrey.liteloader.util.log.LiteLoaderLogger; import com.voxelmodpack.hdskins.gui.EntityPlayerModel; +import com.voxelmodpack.hdskins.gui.Feature; import com.voxelmodpack.hdskins.resources.PreviewTextureManager; import com.voxelmodpack.hdskins.server.SkinServer; import com.voxelmodpack.hdskins.server.SkinUpload; @@ -102,6 +103,10 @@ public class SkinUploader implements Closeable { return gateway == null ? "" : gateway.toString(); } + public boolean supportsFeature(Feature feature) { + return gateway != null && gateway.supportsFeature(feature); + } + protected void setError(String er) { status = er; sendingSkin = false; diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/gui/Feature.java b/src/hdskins/java/com/voxelmodpack/hdskins/gui/Feature.java new file mode 100644 index 00000000..0f77d1ab --- /dev/null +++ b/src/hdskins/java/com/voxelmodpack/hdskins/gui/Feature.java @@ -0,0 +1,50 @@ +package com.voxelmodpack.hdskins.gui; + +/** + * Represents the possible features that a skin server can implement. + */ +public enum Feature { + /** + * Whether a server has write access. + * i.e. If the server allows for users to upload a new skin. + */ + UPLOAD_USER_SKIN, + /** + * Whether a server allows for downloading and saving a user's skin. + * Most servers should support this. + */ + DOWNLOAD_USER_SKIN, + /** + * Whether a server has delete access. + * i.e. If the server allows a user to deleted a previously uploaded skin. + */ + DELETE_USER_SKIN, + /** + * Whether a server can send a full list of skins for a given profile. + * Typically used for servers that keep a record of past uploads + * and/or allow for switching between past skins. + */ + FETCH_SKIN_LIST, + /** + * Whether a server supports thin (Alex) skins or just default (Steve) skins. + * Servers without this will typically fall back to using the player's uuid on the client side. + * + * (unused) + */ + MODEL_VARIANTS, + /** + * Whether a server allows for uploading alternative skin types. i.e. Cape, Elytra, Hats and wears. + */ + MODEL_TYPES, + /** + * Whether a server will accept arbitrary extra metadata values with skin uploads. + * + * (unused) + */ + MODEL_METADATA, + /** + * Whether a server can provide a link to view a user's profile online, + * typically through a web-portal. + */ + LINK_PROFILE +} diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java b/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java index fa3b4e8f..8ec65c1e 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/gui/GuiSkins.java @@ -1,7 +1,9 @@ package com.voxelmodpack.hdskins.gui; +import com.google.common.base.Splitter; import com.minelittlepony.gui.Button; import com.minelittlepony.gui.GameGui; +import com.minelittlepony.gui.IGuiAction; import com.minelittlepony.gui.IconicButton; import com.minelittlepony.gui.IconicToggle; import com.minelittlepony.gui.Label; @@ -44,15 +46,15 @@ public class GuiSkins extends GameGui implements ISkinUploadHandler { private int updateCounter = 0; private Button btnBrowse; - private Button btnUpload; - private Button btnDownload; - private Button btnClear; + private FeatureButton btnUpload; + private FeatureButton btnDownload; + private FeatureButton btnClear; - private Button btnModeSteve; - private Button btnModeAlex; + private FeatureSwitch btnModeSteve; + private FeatureSwitch btnModeAlex; - private Button btnModeSkin; - private Button btnModeElytra; + private FeatureSwitch btnModeSkin; + private FeatureSwitch btnModeElytra; protected EntityPlayerModel localPlayer; protected EntityPlayerModel remotePlayer; @@ -148,21 +150,21 @@ public class GuiSkins extends GameGui implements ISkinUploadHandler { chooser.openBrowsePNG(mc, format("hdskins.open.title")))) .setEnabled(!mc.isFullScreen()); - addButton(btnUpload = new Button(width / 2 - 24, height / 2 - 20, 48, 20, "hdskins.options.chevy", sender -> { + addButton(btnUpload = new FeatureButton(width / 2 - 24, height / 2 - 20, 48, 20, "hdskins.options.chevy", sender -> { if (uploader.canUpload()) { punchServer("hdskins.upload"); } })).setEnabled(uploader.canUpload()) .setTooltip("hdskins.options.chevy.title"); - addButton(btnDownload = new Button(width / 2 - 24, height / 2 + 20, 48, 20, "hdskins.options.download", sender -> { + addButton(btnDownload = new FeatureButton(width / 2 - 24, height / 2 + 20, 48, 20, "hdskins.options.download", sender -> { if (uploader.canClear()) { chooser.openSavePNG(mc, format("hdskins.save.title")); } })).setEnabled(uploader.canClear()) .setTooltip("hdskins.options.download.title"); - addButton(btnClear = new Button(width / 2 + 60, height - 27, 90, 20, "hdskins.options.clear", sender -> { + addButton(btnClear = new FeatureButton(width / 2 + 60, height - 27, 90, 20, "hdskins.options.clear", sender -> { if (uploader.canClear()) { punchServer("hdskins.request"); } @@ -171,26 +173,26 @@ public class GuiSkins extends GameGui implements ISkinUploadHandler { addButton(new Button(width / 2 - 50, height - 25, 100, 20, "hdskins.options.close", sender -> mc.displayGuiScreen(new GuiMainMenu()))); - addButton(btnModeSteve = new IconicButton(width - 25, 32, sender -> switchSkinMode("default")) - .setIcon(new ItemStack(Items.LEATHER_LEGGINGS), 0x3c5dcb)) + addButton(btnModeSteve = new FeatureSwitch(width - 25, 32, sender -> switchSkinMode("default"))) + .setIcon(new ItemStack(Items.LEATHER_LEGGINGS), 0x3c5dcb) .setEnabled("slim".equals(uploader.getMetadataField("model"))) .setTooltip("hdskins.mode.steve") .setTooltipOffset(0, 10); - addButton(btnModeAlex = new IconicButton(width - 25, 51, sender -> switchSkinMode("slim")) - .setIcon(new ItemStack(Items.LEATHER_LEGGINGS), 0xfff500)) + addButton(btnModeAlex = new FeatureSwitch(width - 25, 51, sender -> switchSkinMode("slim"))) + .setIcon(new ItemStack(Items.LEATHER_LEGGINGS), 0xfff500) .setEnabled("default".equals(uploader.getMetadataField("model"))) .setTooltip("hdskins.mode.alex") .setTooltipOffset(0, 10); - addButton(btnModeSkin = new IconicButton(width - 25, 75, sender -> uploader.setSkinType(Type.SKIN)) - .setIcon(new ItemStack(Items.LEATHER_CHESTPLATE))) + addButton(btnModeSkin = new FeatureSwitch(width - 25, 75, sender -> uploader.setSkinType(Type.SKIN))) + .setIcon(new ItemStack(Items.LEATHER_CHESTPLATE)) .setEnabled(uploader.getSkinType() == Type.ELYTRA) .setTooltip(format("hdskins.mode.skin", toTitleCase(Type.SKIN.name()))) .setTooltipOffset(0, 10); - addButton(btnModeElytra = new IconicButton(width - 25, 94, sender -> uploader.setSkinType(Type.ELYTRA)) - .setIcon(new ItemStack(Items.ELYTRA))) + addButton(btnModeElytra = new FeatureSwitch(width - 25, 94, sender -> uploader.setSkinType(Type.ELYTRA))) + .setIcon(new ItemStack(Items.ELYTRA)) .setEnabled(uploader.getSkinType() == Type.SKIN) .setTooltip(format("hdskins.mode.skin", toTitleCase(Type.ELYTRA.name()))) .setTooltipOffset(0, 10); @@ -506,8 +508,85 @@ public class GuiSkins extends GameGui implements ISkinUploadHandler { private void updateButtons() { btnClear.enabled = uploader.canClear(); - btnUpload.enabled = uploader.canUpload(); + btnUpload.enabled = uploader.canUpload() && uploader.supportsFeature(Feature.UPLOAD_USER_SKIN); btnDownload.enabled = uploader.canClear() && !chooser.pickingInProgress(); btnBrowse.enabled = !chooser.pickingInProgress(); + + boolean types = !uploader.supportsFeature(Feature.MODEL_TYPES); + boolean variants = !uploader.supportsFeature(Feature.MODEL_VARIANTS); + + btnModeSkin.setLocked(types); + btnModeElytra.setLocked(types); + + btnModeSteve.setLocked(variants); + btnModeAlex.setLocked(variants); + + btnClear.setLocked(!uploader.supportsFeature(Feature.DELETE_USER_SKIN)); + btnUpload.setLocked(!uploader.supportsFeature(Feature.UPLOAD_USER_SKIN)); + btnDownload.setLocked(!uploader.supportsFeature(Feature.DOWNLOAD_USER_SKIN)); + } + + protected class FeatureButton extends Button { + private List disabledTooltip = Splitter.onPattern("\r?\n|\\\\n").splitToList(format("hdskins.warning.disabled.description")); + + protected boolean locked; + + public FeatureButton(int x, int y, int width, int height, String label, IGuiAction callback) { + super(x, y, width, height, label, callback); + } + + @Override + protected List getTooltip() { + if (locked) { + return disabledTooltip; + } + return super.getTooltip(); + } + + @Override + public Button setTooltip(String tooltip) { + disabledTooltip = Splitter.onPattern("\r?\n|\\\\n").splitToList( + format("hdskins.warning.disabled.title", + format(tooltip), + format("hdskins.warning.disabled.description"))); + return super.setTooltip(tooltip); + } + + public void setLocked(boolean lock) { + locked = lock; + enabled &= !lock; + } + } + + protected class FeatureSwitch extends IconicButton { + private List disabledTooltip = null; + + protected boolean locked; + + public FeatureSwitch(int x, int y, IGuiAction callback) { + super(x, y, callback); + } + + @Override + protected List getTooltip() { + if (locked) { + return disabledTooltip; + } + return super.getTooltip(); + } + + @Override + public Button setTooltip(String tooltip) { + disabledTooltip = Splitter.onPattern("\r?\n|\\\\n").splitToList( + format("hdskins.warning.disabled.title", + format(tooltip), + format("hdskins.warning.disabled.description"))); + return super.setTooltip(tooltip); + } + + public void setLocked(boolean lock) { + locked = lock; + enabled &= !lock; + } } } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/BethlehemSkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/BethlehemSkinServer.java index 2ea0924a..0569d146 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/server/BethlehemSkinServer.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/BethlehemSkinServer.java @@ -7,6 +7,7 @@ import com.mojang.authlib.GameProfile; import com.mojang.authlib.exceptions.AuthenticationException; import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mojang.util.UUIDTypeAdapter; +import com.voxelmodpack.hdskins.gui.Feature; import com.voxelmodpack.hdskins.util.IndentedToStringStyle; import com.voxelmodpack.hdskins.util.MoreHttpResponses; import com.voxelmodpack.hdskins.util.NetClient; @@ -84,4 +85,17 @@ public class BethlehemSkinServer implements SkinServer { .append("address", address) .build(); } + + @Override + public boolean supportsFeature(Feature feature) { + switch (feature) { + case DOWNLOAD_USER_SKIN: + case UPLOAD_USER_SKIN: + case MODEL_VARIANTS: + case MODEL_TYPES: + case LINK_PROFILE: + return true; + default: return false; + } + } } diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/LegacySkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/LegacySkinServer.java index f7f051ab..f66897d3 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/server/LegacySkinServer.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/LegacySkinServer.java @@ -10,6 +10,7 @@ import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mojang.util.UUIDTypeAdapter; import com.voxelmodpack.hdskins.HDSkinManager; +import com.voxelmodpack.hdskins.gui.Feature; import com.voxelmodpack.hdskins.util.CallableFutures; import com.voxelmodpack.hdskins.util.IndentedToStringStyle; import com.voxelmodpack.hdskins.util.MoreHttpResponses; @@ -164,6 +165,17 @@ public class LegacySkinServer implements SkinServer { return !Strings.isNullOrEmpty(gateway); } + @Override + public boolean supportsFeature(Feature feature) { + switch (feature) { + case DOWNLOAD_USER_SKIN: + case UPLOAD_USER_SKIN: + case DELETE_USER_SKIN: + return true; + default: return false; + } + } + @Override public String toString() { return new IndentedToStringStyle.Builder(this) diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/ServerType.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/ServerType.java index 4c60eb7c..1509dd56 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/server/ServerType.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/ServerType.java @@ -5,6 +5,14 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * The remote server API level that this skin server implements. + * + * Current values are: + * - legacy + * - valhalla + * - bethlehem + */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ServerType { diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServer.java index 60383684..8b3552bd 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServer.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/SkinServer.java @@ -10,6 +10,7 @@ import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mojang.util.UUIDTypeAdapter; import com.mumfrey.liteloader.modconfig.Exposable; import com.voxelmodpack.hdskins.HDSkinManager; +import com.voxelmodpack.hdskins.gui.Feature; import com.voxelmodpack.hdskins.util.CallableFutures; import net.minecraft.client.Minecraft; import net.minecraft.util.Session; @@ -30,22 +31,67 @@ public interface SkinServer extends Exposable { "http://skinmanager.voxelmodpack.com") ); + /** + * Returns true for any features that this skin server supports. + */ + boolean supportsFeature(Feature feature); + + /** + * Synchronously loads texture information for the provided profile. + * + * @return The parsed server response as a textures payload. + * + * @throws IOException If any authenticaiton or network error occurs. + */ MinecraftTexturesPayload loadProfileData(GameProfile profile) throws IOException; + /** + * Synchronously uploads a skin to this server. + * + * @param upload The payload to send. + * + * @return A server response object. + * + * @throws IOException + * @throws AuthenticationException + */ SkinUploadResponse performSkinUpload(SkinUpload upload) throws IOException, AuthenticationException; + /** + * Asynchronously uploads a skin to the server. + * + * Returns an incomplete future for chaining other actions to be performed after this method completes. + * Actions are dispatched to the default skinUploadExecutor + * + * @param upload The payload to send. + */ default CompletableFuture uploadSkin(SkinUpload upload) { return CallableFutures.asyncFailableFuture(() -> performSkinUpload(upload), HDSkinManager.skinUploadExecutor); } + /** + * Asynchronously loads texture information for the provided profile. + * + * Returns an incomplete future for chaining other actions to be performed after this method completes. + * Actions are dispatched to the default skinDownloadExecutor + */ default CompletableFuture getPreviewTextures(GameProfile profile) { return CallableFutures.asyncFailableFuture(() -> loadProfileData(profile), HDSkinManager.skinDownloadExecutor); } + /** + * Called to validate this skin server's state. + * Any servers with an invalid gateway format will not be loaded and generate an exception. + */ default boolean verifyGateway() { return true; } + /** + * Joins with the Mojang API to verify the current user's session. + + * @throws AuthenticationException if authentication failed or the session is invalid. + */ static void verifyServerConnection(Session session, String serverId) throws AuthenticationException { MinecraftSessionService service = Minecraft.getMinecraft().getSessionService(); service.joinServer(session.getProfile(), session.getToken(), serverId); diff --git a/src/hdskins/java/com/voxelmodpack/hdskins/server/ValhallaSkinServer.java b/src/hdskins/java/com/voxelmodpack/hdskins/server/ValhallaSkinServer.java index c874f261..63f2b698 100644 --- a/src/hdskins/java/com/voxelmodpack/hdskins/server/ValhallaSkinServer.java +++ b/src/hdskins/java/com/voxelmodpack/hdskins/server/ValhallaSkinServer.java @@ -8,6 +8,7 @@ import com.mojang.authlib.minecraft.MinecraftProfileTexture; import com.mojang.authlib.yggdrasil.response.MinecraftTexturesPayload; import com.mojang.util.UUIDTypeAdapter; import com.voxelmodpack.hdskins.HDSkinManager; +import com.voxelmodpack.hdskins.gui.Feature; import com.voxelmodpack.hdskins.util.IndentedToStringStyle; import com.voxelmodpack.hdskins.util.MoreHttpResponses; import net.minecraft.client.Minecraft; @@ -176,6 +177,19 @@ public class ValhallaSkinServer implements SkinServer { return URI.create(String.format("%s/auth/response", this.address)); } + @Override + public boolean supportsFeature(Feature feature) { + switch (feature) { + case DOWNLOAD_USER_SKIN: + case UPLOAD_USER_SKIN: + case DELETE_USER_SKIN: + case MODEL_VARIANTS: + case MODEL_TYPES: + return true; + default: return false; + } + } + @Override public String toString() { return new IndentedToStringStyle.Builder(this) diff --git a/src/hdskins/resources/assets/hdskins/lang/en_us.lang b/src/hdskins/resources/assets/hdskins/lang/en_us.lang index 8c67297e..ef5121e3 100644 --- a/src/hdskins/resources/assets/hdskins/lang/en_us.lang +++ b/src/hdskins/resources/assets/hdskins/lang/en_us.lang @@ -25,7 +25,7 @@ hdskins.server=Server Skin hdskins.mode.steve=Steve Model hdskins.mode.alex=Alex Model -hdskins.mode.skin=%s +hdskins.mode.skin=%s Texture hdskins.mode.stand=Standing hdskins.mode.sleep=Sleeping @@ -43,4 +43,6 @@ hdskins.options.browse=Browse hdskins.options.skindrops=Experimental Skin Drop hdskins.options.cache=Clear Skin Cache -hdskins.warning.experimental=§6WARNING: This feature is §4experimental§6, meaning things may break or derp or even slurp. Enabling this means you accept responsibility for what may happen to your chickens. \ No newline at end of file +hdskins.warning.experimental=§6WARNING: This feature is §4experimental§6, meaning things may break or derp or even slurp. Enabling this means you accept responsibility for what may happen to your chickens. +hdskins.warning.disabled.title=§c%s %s +hdskins.warning.disabled.description=§4(DISABLED)\n§7This feature is not supported by your current skin server.\n§7Please choose a different one to proceed. \ No newline at end of file