diff --git a/pom.xml b/pom.xml index 473658a8..ca5097f9 100644 --- a/pom.xml +++ b/pom.xml @@ -191,5 +191,10 @@ jna-platform 4.1.0 + + org.apache.maven + maven-artifact + 3.0.3 + diff --git a/res/bang.png b/res/bang.png new file mode 100644 index 00000000..6f1e3274 Binary files /dev/null and b/res/bang.png differ diff --git a/res/version b/res/version index 006a1cd5..27f750ad 100644 --- a/res/version +++ b/res/version @@ -1,2 +1,3 @@ version=${pom.version} -build.date=${timestamp} +file=https://github.com/itdelatrisu/opsu/releases/download/${pom.version}/opsu-${pom.version}.jar +build.date=${timestamp} \ No newline at end of file diff --git a/src/itdelatrisu/opsu/Container.java b/src/itdelatrisu/opsu/Container.java index b56ae1e3..65e4bc17 100644 --- a/src/itdelatrisu/opsu/Container.java +++ b/src/itdelatrisu/opsu/Container.java @@ -20,6 +20,7 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.downloads.DownloadList; +import itdelatrisu.opsu.downloads.Updater; import org.lwjgl.opengl.Display; import org.newdawn.slick.AppGameContainer; @@ -76,8 +77,10 @@ public class Container extends AppGameContainer { } } - if (forceExit) - Opsu.exit(); + if (forceExit) { + Opsu.close(); + System.exit(0); + } } @Override @@ -128,8 +131,14 @@ public class Container extends AppGameContainer { @Override public void exit() { // show confirmation dialog if any downloads are active - if (forceExit && DownloadList.get().hasActiveDownloads() && DownloadList.showExitConfirmation()) - return; + if (forceExit) { + if (DownloadList.get().hasActiveDownloads() && + UI.showExitConfirmation(DownloadList.EXIT_CONFIRMATION)) + return; + if (Updater.get().getStatus() == Updater.Status.UPDATE_DOWNLOADING && + UI.showExitConfirmation(Updater.EXIT_CONFIRMATION)) + return; + } super.exit(); } diff --git a/src/itdelatrisu/opsu/ErrorHandler.java b/src/itdelatrisu/opsu/ErrorHandler.java index ef0936a3..97505e75 100644 --- a/src/itdelatrisu/opsu/ErrorHandler.java +++ b/src/itdelatrisu/opsu/ErrorHandler.java @@ -119,7 +119,7 @@ public class ErrorHandler { String issueTitle = (error != null) ? error : e.getMessage(); StringBuilder sb = new StringBuilder(); Properties props = new Properties(); - props.load(ResourceLoader.getResourceAsStream("version")); + props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE)); String version = props.getProperty("version"); if (version != null && !version.equals("${pom.version}")) { sb.append("**Version:** "); diff --git a/src/itdelatrisu/opsu/GameImage.java b/src/itdelatrisu/opsu/GameImage.java index 2740c8c1..96d6b9d4 100644 --- a/src/itdelatrisu/opsu/GameImage.java +++ b/src/itdelatrisu/opsu/GameImage.java @@ -447,6 +447,12 @@ public enum GameImage { protected Image process_sub(Image img, int w, int h) { return img.getScaledCopy((h / 17f) / img.getHeight()); } + }, + BANG ("bang", "png", false, false) { + @Override + protected Image process_sub(Image img, int w, int h) { + return REPOSITORY.process_sub(img, w, h); + } }; /** Image file types. */ diff --git a/src/itdelatrisu/opsu/Opsu.java b/src/itdelatrisu/opsu/Opsu.java index 56b9a0a5..e56df7c5 100644 --- a/src/itdelatrisu/opsu/Opsu.java +++ b/src/itdelatrisu/opsu/Opsu.java @@ -21,6 +21,7 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.db.DBController; import itdelatrisu.opsu.downloads.DownloadList; +import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.states.ButtonMenu; import itdelatrisu.opsu.states.DownloadsMenu; import itdelatrisu.opsu.states.Game; @@ -136,6 +137,18 @@ public class Opsu extends StateBasedGame { // initialize databases DBController.init(); + // check for updates + new Thread() { + @Override + public void run() { + try { + Updater.get().checkForUpdates(); + } catch (IOException e) { + Log.warn("Check for updates failed.", e); + } + } + }.start(); + // start the game try { // loop until force exit @@ -150,6 +163,13 @@ public class Opsu extends StateBasedGame { app.setForceExit(true); app.start(); + + // run update if available + if (Updater.get().getStatus() == Updater.Status.UPDATE_FINAL) { + close(); + Updater.get().runUpdate(); + break; + } } } catch (SlickException e) { // JARs will not run properly inside directories containing '!' @@ -159,8 +179,6 @@ public class Opsu extends StateBasedGame { else ErrorHandler.error("Error while creating game container.", e, true); } - - Opsu.exit(); } @Override @@ -191,16 +209,20 @@ public class Opsu extends StateBasedGame { } // show confirmation dialog if any downloads are active - if (DownloadList.get().hasActiveDownloads() && DownloadList.showExitConfirmation()) + if (DownloadList.get().hasActiveDownloads() && + UI.showExitConfirmation(DownloadList.EXIT_CONFIRMATION)) + return false; + if (Updater.get().getStatus() == Updater.Status.UPDATE_DOWNLOADING && + UI.showExitConfirmation(Updater.EXIT_CONFIRMATION)) return false; return true; } /** - * Closes all resources and exits the application. + * Closes all resources. */ - public static void exit() { + public static void close() { // close databases DBController.closeConnections(); @@ -215,7 +237,5 @@ public class Opsu extends StateBasedGame { ErrorHandler.error("Failed to close server socket.", e, false); } } - - System.exit(0); } } diff --git a/src/itdelatrisu/opsu/Options.java b/src/itdelatrisu/opsu/Options.java index ddd4ecda..9ccae2b2 100644 --- a/src/itdelatrisu/opsu/Options.java +++ b/src/itdelatrisu/opsu/Options.java @@ -69,11 +69,17 @@ public class Options { /** Font file name. */ public static final String FONT_NAME = "kochi-gothic.ttf"; + /** Version file name. */ + public static final String VERSION_FILE = "version"; + /** Repository address. */ - public static URI REPOSITORY_URI = URI.create("https://github.com/itdelatrisu/opsu"); + public static final URI REPOSITORY_URI = URI.create("https://github.com/itdelatrisu/opsu"); /** Issue reporting address. */ - public static String ISSUES_URL = "https://github.com/itdelatrisu/opsu/issues/new?title=%s&body=%s"; + public static final String ISSUES_URL = "https://github.com/itdelatrisu/opsu/issues/new?title=%s&body=%s"; + + /** Address containing the latest version file. */ + public static final String VERSION_REMOTE = "https://raw.githubusercontent.com/itdelatrisu/opsu/gh-pages/version"; /** The beatmap directory. */ private static File beatmapDir; diff --git a/src/itdelatrisu/opsu/UI.java b/src/itdelatrisu/opsu/UI.java index 56849906..31de0eff 100644 --- a/src/itdelatrisu/opsu/UI.java +++ b/src/itdelatrisu/opsu/UI.java @@ -21,6 +21,9 @@ package itdelatrisu.opsu; import java.util.Iterator; import java.util.LinkedList; +import javax.swing.JOptionPane; +import javax.swing.UIManager; + import itdelatrisu.opsu.audio.SoundController; import org.newdawn.slick.Animation; @@ -571,4 +574,20 @@ public class UI { Utils.COLOR_BLACK_ALPHA.a = oldAlphaB; Utils.COLOR_WHITE_ALPHA.a = oldAlphaW; } + + /** + * Shows a confirmation dialog (used before exiting the game). + * @param message the message to display + * @return true if user selects "yes", false otherwise + */ + public static boolean showExitConfirmation(String message) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + ErrorHandler.error("Could not set system look and feel for exit confirmation.", e, true); + } + int n = JOptionPane.showConfirmDialog(null, message, "Warning", + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + return (n != JOptionPane.YES_OPTION); + } } diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 4777ea74..ac70bf52 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -20,12 +20,19 @@ package itdelatrisu.opsu; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.downloads.Download; import itdelatrisu.opsu.downloads.DownloadNode; import java.awt.Font; import java.awt.image.BufferedImage; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.text.SimpleDateFormat; @@ -394,6 +401,9 @@ public class Utils { * @author Sarel Botha (http://stackoverflow.com/a/5626340) */ public static String cleanFileName(String badFileName, char replace) { + if (badFileName == null) + return null; + boolean doReplace = (replace > 0 && Arrays.binarySearch(illegalChars, replace) < 0); StringBuilder cleanName = new StringBuilder(); for (int i = 0, n = badFileName.length(); i < n; i++) { @@ -500,4 +510,39 @@ public class Utils { list.add(str); return list; } + + /** + * Returns a the contents of a URL as a string. + * @param url the remote URL + * @return the contents as a string, or null if any error occurred + */ + public static String readDataFromUrl(URL url) throws IOException { + // open connection + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setConnectTimeout(Download.CONNECTION_TIMEOUT); + conn.setReadTimeout(Download.READ_TIMEOUT); + conn.setUseCaches(false); + try { + conn.connect(); + } catch (SocketTimeoutException e) { + Log.warn("Connection to server timed out.", e); + throw e; + } + + if (Thread.interrupted()) + return null; + + // read contents + try (InputStream in = conn.getInputStream()) { + BufferedReader rd = new BufferedReader(new InputStreamReader(in)); + StringBuilder sb = new StringBuilder(); + int c; + while ((c = rd.read()) != -1) + sb.append((char) c); + return sb.toString(); + } catch (SocketTimeoutException e) { + Log.warn("Connection to server timed out.", e); + throw e; + } + } } diff --git a/src/itdelatrisu/opsu/downloads/BloodcatServer.java b/src/itdelatrisu/opsu/downloads/BloodcatServer.java index cdc167de..bb1cf6f5 100644 --- a/src/itdelatrisu/opsu/downloads/BloodcatServer.java +++ b/src/itdelatrisu/opsu/downloads/BloodcatServer.java @@ -19,15 +19,11 @@ package itdelatrisu.opsu.downloads; import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.Utils; -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; import java.net.MalformedURLException; -import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLEncoder; @@ -98,36 +94,15 @@ public class BloodcatServer implements DownloadServer { * @return the JSON object * @author Roland Illig (http://stackoverflow.com/a/4308662) */ - public static JSONObject readJsonFromUrl(URL url) throws IOException { - // open connection - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setConnectTimeout(Download.CONNECTION_TIMEOUT); - conn.setReadTimeout(Download.READ_TIMEOUT); - conn.setUseCaches(false); - try { - conn.connect(); - } catch (SocketTimeoutException e) { - ErrorHandler.error("Connection to server timed out.", e, false); - throw e; - } - - if (Thread.interrupted()) - return null; - - // read JSON + private static JSONObject readJsonFromUrl(URL url) throws IOException { + String s = Utils.readDataFromUrl(url); JSONObject json = null; - try (InputStream in = conn.getInputStream()) { - BufferedReader rd = new BufferedReader(new InputStreamReader(in)); - StringBuilder sb = new StringBuilder(); - int c; - while ((c = rd.read()) != -1) - sb.append((char) c); - json = new JSONObject(sb.toString()); - } catch (SocketTimeoutException e) { - ErrorHandler.error("Connection to server timed out.", e, false); - throw e; - } catch (JSONException e1) { - ErrorHandler.error("Failed to create JSON object.", e1, true); + if (s != null) { + try { + json = new JSONObject(s); + } catch (JSONException e) { + ErrorHandler.error("Failed to create JSON object.", e, true); + } } return json; } diff --git a/src/itdelatrisu/opsu/downloads/Download.java b/src/itdelatrisu/opsu/downloads/Download.java index c638173d..f8857b60 100644 --- a/src/itdelatrisu/opsu/downloads/Download.java +++ b/src/itdelatrisu/opsu/downloads/Download.java @@ -138,9 +138,19 @@ public class Download { return; } this.localPath = localPath; - this.rename = rename; + this.rename = Utils.cleanFileName(rename, '-'); } + /** + * Returns the remote download URL. + */ + public URL getRemoteURL() { return url; } + + /** + * Returns the local path to save the download (after renamed). + */ + public String getLocalPath() { return (rename != null) ? rename : localPath; } + /** * Sets the download listener. * @param listener the listener to set @@ -187,9 +197,8 @@ public class Download { rbc.close(); fos.close(); if (rename != null) { - String cleanedName = Utils.cleanFileName(rename, '-'); Path source = new File(localPath).toPath(); - Files.move(source, source.resolveSibling(cleanedName), StandardCopyOption.REPLACE_EXISTING); + Files.move(source, source.resolveSibling(rename), StandardCopyOption.REPLACE_EXISTING); } if (listener != null) listener.completed(); diff --git a/src/itdelatrisu/opsu/downloads/DownloadList.java b/src/itdelatrisu/opsu/downloads/DownloadList.java index 10cfec54..89102d69 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadList.java +++ b/src/itdelatrisu/opsu/downloads/DownloadList.java @@ -18,7 +18,6 @@ package itdelatrisu.opsu.downloads; -import itdelatrisu.opsu.ErrorHandler; import itdelatrisu.opsu.downloads.Download.Status; import java.util.ArrayList; @@ -27,9 +26,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import javax.swing.JOptionPane; -import javax.swing.UIManager; - /** * Maintains the current downloads list. */ @@ -37,6 +33,9 @@ public class DownloadList { /** The single instance of this class. */ private static DownloadList list = new DownloadList(); + /** The exit confirmation message. */ + public static final String EXIT_CONFIRMATION = "Beatmap downloads are in progress.\nAre you sure you want to quit opsu!?"; + /** Current list of downloads. */ private List nodes; @@ -160,20 +159,4 @@ public class DownloadList { } } } - - /** - * Shows a confirmation dialog (used before exiting the game). - * @return true if user selects "yes", false otherwise - */ - public static boolean showExitConfirmation() { - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (Exception e) { - ErrorHandler.error("Could not set system look and feel for DownloadList.", e, true); - } - int n = JOptionPane.showConfirmDialog(null, - "Beatmap downloads are in progress.\nAre you sure you want to quit opsu!?", - "Warning", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); - return (n != JOptionPane.YES_OPTION); - } } diff --git a/src/itdelatrisu/opsu/downloads/Updater.java b/src/itdelatrisu/opsu/downloads/Updater.java new file mode 100644 index 00000000..7e1f5560 --- /dev/null +++ b/src/itdelatrisu/opsu/downloads/Updater.java @@ -0,0 +1,219 @@ +/* + * opsu! - an open-source osu! client + * Copyright (C) 2014, 2015 Jeffrey Han + * + * opsu! 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. + * + * opsu! 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 opsu!. If not, see . + */ + +package itdelatrisu.opsu.downloads; + +import itdelatrisu.opsu.ErrorHandler; +import itdelatrisu.opsu.Options; +import itdelatrisu.opsu.UI; +import itdelatrisu.opsu.Utils; +import itdelatrisu.opsu.downloads.Download.DownloadListener; + +import java.io.File; +import java.io.IOException; +import java.io.StringReader; +import java.net.URL; +import java.util.Properties; + +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.newdawn.slick.util.ResourceLoader; + +/** + * Handles automatic program updates. + */ +public class Updater { + /** The single instance of this class. */ + private static Updater updater = new Updater(); + + /** The exit confirmation message. */ + public static final String EXIT_CONFIRMATION = "An opsu! update is being downloaded.\nAre you sure you want to quit opsu!?"; + + /** + * Returns the single instance of this class. + */ + public static Updater get() { return updater; } + + /** Updater status. */ + public enum Status { + INITIAL (""), + CHECKING ("Checking for updates..."), + CONNECTION_ERROR ("Connection error."), + INTERNAL_ERROR ("Internal error."), + UP_TO_DATE ("Up to date!"), + UPDATE_AVAILABLE ("Update available!\nClick to download."), + UPDATE_DOWNLOADING ("Downloading update...") { + @Override + public String getDescription() { + Download d = updater.download; + if (d != null && d.getStatus() == Download.Status.DOWNLOADING) { + return String.format("Downloading update...\n%.1f%% complete (%s/%s)", + d.getProgress(), Utils.bytesToString(d.readSoFar()), Utils.bytesToString(d.contentLength())); + } else + return super.getDescription(); + } + }, + UPDATE_DOWNLOADED ("Download complete.\nClick to restart."), + UPDATE_FINAL ("Update queued."); + + /** The status description. */ + private String description; + + /** + * Constructor. + * @param description the status description + */ + Status(String description) { + this.description = description; + } + + /** + * Returns the status description. + */ + public String getDescription() { return description; } + }; + + /** The current updater status. */ + private Status status; + + /** The current and latest versions. */ + private DefaultArtifactVersion currentVersion, latestVersion; + + /** The download object. */ + private Download download; + + /** + * Constructor. + */ + private Updater() { + status = Status.INITIAL; + } + + /** + * Returns the updater status. + */ + public Status getStatus() { return status; } + + /** + * Returns whether or not the updater button should be displayed. + */ + public boolean showButton() { + return (status == Status.UPDATE_AVAILABLE || status == Status.UPDATE_DOWNLOADED || status == Status.UPDATE_DOWNLOADING); + } + + /** + * Returns the version from a set of properties. + * @param props the set of properties + * @return the version, or null if not found + */ + private DefaultArtifactVersion getVersion(Properties props) { + String version = props.getProperty("version"); + if (version == null || version.equals("${pom.version}")) { + status = Status.INTERNAL_ERROR; + return null; + } else + return new DefaultArtifactVersion(version); + } + + /** + * Checks the program version against the version file on the update server. + */ + public void checkForUpdates() throws IOException { + if (status != Status.INITIAL) + return; + + status = Status.CHECKING; + + // get current version + Properties props = new Properties(); + props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE)); + if ((currentVersion = getVersion(props)) == null) + return; + + // get latest version + String s = Utils.readDataFromUrl(new URL(Options.VERSION_REMOTE)); + if (s == null) { + status = Status.CONNECTION_ERROR; + return; + } + props = new Properties(); + props.load(new StringReader(s)); + if ((latestVersion = getVersion(props)) == null) + return; + + // compare versions + if (latestVersion.compareTo(currentVersion) <= 0) + status = Status.UP_TO_DATE; + else { + String updateURL = props.getProperty("file"); + if (updateURL == null) { + status = Status.INTERNAL_ERROR; + return; + } + status = Status.UPDATE_AVAILABLE; + String localPath = String.format("%s%copsu-update-%s", + System.getProperty("user.dir"), File.separatorChar, latestVersion.toString()); + String rename = String.format("opsu-%s.jar", latestVersion.toString()); + download = new Download(updateURL, localPath, rename); + download.setListener(new DownloadListener() { + @Override + public void completed() { + status = Status.UPDATE_DOWNLOADED; + UI.sendBarNotification("Update has finished downloading."); + } + }); + } + } + + /** + * Starts the download, if available. + */ + public void startDownload() { + if (status != Status.UPDATE_AVAILABLE || download == null || download.getStatus() != Download.Status.WAITING) + return; + + status = Status.UPDATE_DOWNLOADING; + download.start(); + } + + /** + * Prepares to run the update when the application closes. + */ + public void prepareUpdate() { + if (status != Status.UPDATE_DOWNLOADED || download == null || download.getStatus() != Download.Status.COMPLETE) + return; + + status = Status.UPDATE_FINAL; + } + + /** + * Hands over execution to the updated file, if available. + */ + public void runUpdate() { + if (status != Status.UPDATE_FINAL) + return; + + try { + // TODO: it is better to wait for the process? is this portable? + ProcessBuilder pb = new ProcessBuilder("java", "-jar", download.getLocalPath()); + pb.start(); + } catch (IOException e) { + status = Status.INTERNAL_ERROR; + ErrorHandler.error("Failed to start new process.", e, true); + } + } +} diff --git a/src/itdelatrisu/opsu/states/MainMenu.java b/src/itdelatrisu/opsu/states/MainMenu.java index 14836513..0a37ea5b 100644 --- a/src/itdelatrisu/opsu/states/MainMenu.java +++ b/src/itdelatrisu/opsu/states/MainMenu.java @@ -32,6 +32,7 @@ import itdelatrisu.opsu.Utils; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; import itdelatrisu.opsu.audio.SoundEffect; +import itdelatrisu.opsu.downloads.Updater; import itdelatrisu.opsu.states.ButtonMenu.MenuState; import java.awt.Desktop; @@ -84,6 +85,9 @@ public class MainMenu extends BasicGameState { /** Button linking to repository. */ private MenuButton repoButton; + /** Button for installing updates. */ + private MenuButton updateButton; + /** Application start time, for drawing the total running time. */ private long osuStartTime; @@ -93,6 +97,9 @@ public class MainMenu extends BasicGameState { /** Background alpha level (for fade-in effect). */ private float bgAlpha = 0f; + /** Whether or not an update notification was already sent. */ + private boolean updateNotification = false; + /** Music position bar coordinates and dimensions. */ private float musicBarX, musicBarY, musicBarWidth, musicBarHeight; @@ -164,13 +171,21 @@ public class MainMenu extends BasicGameState { downloadsButton.setHoverExpand(1.03f, Expand.LEFT); // initialize repository button + float startX = width * 0.997f, startY = height * 0.997f; if (Desktop.isDesktopSupported()) { // only if a webpage can be opened Image repoImg = GameImage.REPOSITORY.getImage(); repoButton = new MenuButton(repoImg, - (width * 0.997f) - repoImg.getWidth(), (height * 0.997f) - repoImg.getHeight() + startX - repoImg.getWidth(), startY - repoImg.getHeight() ); repoButton.setHoverExpand(); - } + startX -= repoImg.getWidth() * 1.75f; + } else + startX -= width * 0.005f; + + // initialize update button + Image bangImg = GameImage.BANG.getImage(); + updateButton = new MenuButton(bangImg, startX - bangImg.getWidth(), startY - bangImg.getHeight()); + updateButton.setHoverExpand(1.15f); reset(); } @@ -232,6 +247,27 @@ public class MainMenu extends BasicGameState { if (repoButton != null) repoButton.draw(); + // draw update button + boolean showUpdateButton = Updater.get().showButton(); + if (Updater.get().showButton()) { + Color updateColor = null; + switch (Updater.get().getStatus()) { + case UPDATE_AVAILABLE: + updateColor = Color.red; + break; + case UPDATE_DOWNLOADED: + updateColor = Color.green; + break; + case UPDATE_DOWNLOADING: + updateColor = Color.yellow; + break; + default: + updateColor = Color.white; + break; + } + updateButton.draw(updateColor); + } + // draw text float marginX = width * 0.015f, marginY = height * 0.015f; g.setFont(Utils.FONT_MEDIUM); @@ -268,6 +304,8 @@ public class MainMenu extends BasicGameState { UI.drawTooltip(g, "Next track", false); else if (musicPrevious.contains(mouseX, mouseY)) UI.drawTooltip(g, "Previous track", false); + else if (showUpdateButton && updateButton.contains(mouseX, mouseY)) + UI.drawTooltip(g, Updater.get().getStatus().getDescription(), true); } @Override @@ -280,6 +318,7 @@ public class MainMenu extends BasicGameState { exitButton.hoverUpdate(delta, mouseX, mouseY, 0.25f); if (repoButton != null) repoButton.hoverUpdate(delta, mouseX, mouseY); + updateButton.hoverUpdate(delta, mouseX, mouseY); downloadsButton.hoverUpdate(delta, mouseX, mouseY); // ensure only one button is in hover state at once if (musicPositionBarContains(mouseX, mouseY)) @@ -347,6 +386,10 @@ public class MainMenu extends BasicGameState { public void enter(GameContainer container, StateBasedGame game) throws SlickException { UI.enter(); + if (!updateNotification && Updater.get().getStatus() == Updater.Status.UPDATE_AVAILABLE) { + UI.sendBarNotification("An opsu! update is available."); + updateNotification = true; + } // reset button hover states if mouse is not currently hovering over the button int mouseX = input.getMouseX(), mouseY = input.getMouseY(); @@ -366,6 +409,8 @@ public class MainMenu extends BasicGameState { musicPrevious.resetHover(); if (repoButton != null && !repoButton.contains(mouseX, mouseY)) repoButton.resetHover(); + if (!updateButton.contains(mouseX, mouseY)) + updateButton.resetHover(); if (!downloadsButton.contains(mouseX, mouseY)) downloadsButton.resetHover(); } @@ -424,6 +469,24 @@ public class MainMenu extends BasicGameState { } } + // update button actions + else if (Updater.get().showButton() && updateButton.contains(x, y)) { + switch (Updater.get().getStatus()) { + case UPDATE_AVAILABLE: + SoundController.playSound(SoundEffect.MENUHIT); + Updater.get().startDownload(); + break; + case UPDATE_DOWNLOADED: + SoundController.playSound(SoundEffect.MENUHIT); + Updater.get().prepareUpdate(); + container.setForceExit(false); + container.exit(); + break; + default: + break; + } + } + // start moving logo (if clicked) else if (!logoClicked) { if (logo.contains(x, y, 0.25f)) { @@ -519,6 +582,9 @@ public class MainMenu extends BasicGameState { musicPause.resetHover(); musicNext.resetHover(); musicPrevious.resetHover(); + if (repoButton != null) + repoButton.resetHover(); + updateButton.resetHover(); downloadsButton.resetHover(); }