opsu-dance/src/itdelatrisu/opsu/Utils.java
Jeffrey Han 553f091693 Added ErrorHandler class to handle all critical errors.
This mostly replaces the Slick2D Log class for error reporting.  Most game errors will now trigger the error popup.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
2015-01-15 21:55:26 -05:00

589 lines
17 KiB
Java

/*
* opsu! - an open-source osu! client
* Copyright (C) 2014 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 <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.states.Options;
import java.awt.Font;
import java.io.File;
import java.nio.IntBuffer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Cursor;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.font.effects.ColorEffect;
import org.newdawn.slick.imageout.ImageOut;
import org.newdawn.slick.state.StateBasedGame;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
/**
* Contains miscellaneous utilities.
*/
public class Utils {
/**
* Game colors.
*/
public static final Color
COLOR_BLACK_ALPHA = new Color(0, 0, 0, 0.5f),
COLOR_WHITE_ALPHA = new Color(255, 255, 255, 0.5f),
COLOR_BLUE_DIVIDER = new Color(49, 94, 237),
COLOR_BLUE_BACKGROUND = new Color(74, 130, 255),
COLOR_BLUE_BUTTON = new Color(50, 189, 237),
COLOR_ORANGE_BUTTON = new Color(230, 151, 87),
COLOR_GREEN_OBJECT = new Color(26, 207, 26),
COLOR_BLUE_OBJECT = new Color(46, 136, 248),
COLOR_RED_OBJECT = new Color(243, 48, 77),
COLOR_ORANGE_OBJECT = new Color(255, 200, 32),
COLOR_YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
COLOR_WHITE_FADE = new Color(255, 255, 255, 1f),
COLOR_RED_HOVER = new Color(255, 112, 112);
/**
* The default map colors, used when a map does not provide custom colors.
*/
public static final Color[] DEFAULT_COMBO = {
COLOR_GREEN_OBJECT, COLOR_BLUE_OBJECT,
COLOR_RED_OBJECT, COLOR_ORANGE_OBJECT
};
/**
* Game fonts.
*/
public static UnicodeFont
FONT_DEFAULT, FONT_BOLD,
FONT_XLARGE, FONT_LARGE, FONT_MEDIUM, FONT_SMALL;
/**
* Back button (shared by other states).
*/
private static MenuButton backButton;
/**
* Cursor image and trail.
*/
private static Image cursor, cursorTrail, cursorMiddle;
/**
* Last cursor coordinates.
*/
private static int lastX = -1, lastY = -1;
/**
* Stores all previous cursor locations to display a trail.
*/
private static LinkedList<Integer>
cursorX = new LinkedList<Integer>(),
cursorY = new LinkedList<Integer>();
/**
* Set of all Unicode strings already loaded.
*/
private static HashSet<String> loadedGlyphs = new HashSet<String>();
// game-related variables
private static GameContainer container;
private static StateBasedGame game;
private static Input input;
// This class should not be instantiated.
private Utils() {}
/**
* Initializes game settings and class data.
* @param container the game container
* @param game the game object
* @throws SlickException
*/
public static void init(GameContainer container, StateBasedGame game)
throws SlickException {
Utils.container = container;
Utils.game = game;
Utils.input = container.getInput();
// game settings
container.setTargetFrameRate(Options.getTargetFPS());
container.setMusicVolume(Options.getMusicVolume());
container.setShowFPS(false);
container.getInput().enableKeyRepeat();
container.setAlwaysRender(true);
int width = container.getWidth();
int height = container.getHeight();
// set the cursor
try {
// hide the native cursor
int min = Cursor.getMinCursorSize();
IntBuffer tmp = BufferUtils.createIntBuffer(min * min);
Cursor emptyCursor = new Cursor(min, min, min/2, min/2, 1, tmp, null);
container.setMouseCursor(emptyCursor, 0, 0);
} catch (LWJGLException e) {
ErrorHandler.error("Failed to set the cursor.", e, true);
}
loadCursor();
// create fonts
float fontBase;
if (height <= 600)
fontBase = 10f;
else if (height < 800)
fontBase = 11f;
else if (height <= 900)
fontBase = 13f;
else
fontBase = 15f;
try {
Font javaFont = Font.createFont(Font.TRUETYPE_FONT, ResourceLoader.getResourceAsStream(Options.FONT_NAME));
Font font = javaFont.deriveFont(Font.PLAIN, (int) (fontBase * 4 / 3));
FONT_DEFAULT = new UnicodeFont(font);
FONT_BOLD = new UnicodeFont(font.deriveFont(Font.BOLD));
FONT_XLARGE = new UnicodeFont(font.deriveFont(fontBase * 4));
FONT_LARGE = new UnicodeFont(font.deriveFont(fontBase * 2));
FONT_MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2));
FONT_SMALL = new UnicodeFont(font.deriveFont(fontBase));
ColorEffect colorEffect = new ColorEffect();
loadFont(FONT_DEFAULT, 2, colorEffect);
loadFont(FONT_BOLD, 2, colorEffect);
loadFont(FONT_XLARGE, 4, colorEffect);
loadFont(FONT_LARGE, 4, colorEffect);
loadFont(FONT_MEDIUM, 3, colorEffect);
loadFont(FONT_SMALL, 1, colorEffect);
} catch (Exception e) {
ErrorHandler.error("Failed to load fonts.", e, true);
}
// initialize game images
GameImage.init(width, height);
GameImage.MENU_LOGO.setDefaultImage();
GameImage.MENU_BG.setDefaultImage();
// initialize game mods
for (GameMod mod : GameMod.values())
mod.init(width, height);
// initialize sorts
for (SongSort sort : SongSort.values())
sort.init(width, height);
// back button
Image back = GameImage.MENU_BACK.getImage();
backButton = new MenuButton(back,
back.getWidth() / 2f,
height - (back.getHeight() / 2f));
backButton.setHoverDir(MenuButton.Expand.UP_RIGHT);
}
/**
* Returns the 'menu-back' MenuButton.
*/
public static MenuButton getBackButton() { return backButton; }
/**
* Draws a tab image and text centered at a location.
* @param x the center x coordinate
* @param y the center y coordinate
* @param text the text to draw inside the tab
* @param selected whether the tab is selected (white) or not (red)
* @param isHover whether to include a hover effect (unselected only)
*/
public static void drawTab(float x, float y, String text, boolean selected, boolean isHover) {
Image tabImage = GameImage.MENU_TAB.getImage();
float tabTextX = x - (Utils.FONT_MEDIUM.getWidth(text) / 2);
float tabTextY = y - (tabImage.getHeight() / 2f) +
Math.max((tabImage.getHeight() - Utils.FONT_MEDIUM.getLineHeight()) / 1.5f, 0);
Color filter, textColor;
if (selected) {
filter = Color.white;
textColor = Color.black;
} else {
filter = (isHover) ? Utils.COLOR_RED_HOVER : Color.red;
textColor = Color.white;
}
Utils.drawCentered(tabImage, x, y, filter);
Utils.FONT_MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
}
/**
* Draws an image based on its center with a color filter.
* @param img the image to draw
* @param x the center x coordinate
* @param y the center y coordinate
* @param color the color filter to apply
*/
public static void drawCentered(Image img, float x, float y, Color color) {
img.draw(x - (img.getWidth() / 2f), y - (img.getHeight() / 2f), color);
}
/**
* Draws an animation based on its center.
* @param anim the animation to draw
* @param x the center x coordinate
* @param y the center y coordinate
*/
public static void drawCentered(Animation anim, float x, float y) {
anim.draw(x - (anim.getWidth() / 2f), y - (anim.getHeight() / 2f));
}
/**
* Returns a bounded value for a base value and displacement.
* @param base the initial value
* @param diff the value change
* @param min the minimum value
* @param max the maximum value
* @return the bounded value
*/
public static int getBoundedValue(int base, int diff, int min, int max) {
int val = base + diff;
if (val < min)
val = min;
else if (val > max)
val = max;
return val;
}
/**
* Returns a bounded value for a base value and displacement.
* @param base the initial value
* @param diff the value change
* @param min the minimum value
* @param max the maximum value
* @return the bounded value
*/
public static float getBoundedValue(float base, float diff, float min, float max) {
float val = base + diff;
if (val < min)
val = min;
else if (val > max)
val = max;
return val;
}
/**
* Loads the cursor images.
* @throws SlickException
*/
public static void loadCursor() throws SlickException {
// destroy old cursors, if they exist
if (cursor != null)
cursor.destroy();
if (cursorTrail != null)
cursorTrail.destroy();
if (cursorMiddle != null)
cursorMiddle.destroy();
cursor = cursorTrail = cursorMiddle = null;
// TODO: cleanup
boolean skinCursor = new File(Options.getSkinDir(), "cursor.png").isFile();
if (Options.isNewCursorEnabled()) {
// load new cursor type
// if skin cursor exists but middle part does not, don't load default middle
if (skinCursor && !new File(Options.getSkinDir(), "cursormiddle.png").isFile())
;
else {
cursorMiddle = new Image("cursormiddle.png");
cursor = new Image("cursor.png");
cursorTrail = new Image("cursortrail.png");
}
}
if (cursorMiddle == null) {
// load old cursor type
// default is stored as *2.png, but load skin cursor if it exists
if (skinCursor)
cursor = new Image("cursor.png");
else
cursor = new Image("cursor2.png");
if (new File(Options.getSkinDir(), "cursortrail.png").isFile())
cursorTrail = new Image("cursortrail.png");
else
cursorTrail = new Image("cursortrail2.png");
}
// scale the cursor
float scale = 1 + ((container.getHeight() - 600) / 1000f);
cursor = cursor.getScaledCopy(scale);
cursorTrail = cursorTrail.getScaledCopy(scale);
if (cursorMiddle != null)
cursorMiddle = cursorMiddle.getScaledCopy(scale);
}
/**
* Draws the cursor.
*/
public static void drawCursor() {
// TODO: use an image buffer
int x = input.getMouseX();
int y = input.getMouseY();
int removeCount = 0;
int FPSmod = (Options.getTargetFPS() / 60);
// if middle exists, add all points between cursor movements
if (cursorMiddle != null) {
if (lastX < 0) {
lastX = x;
lastY = y;
return;
}
addCursorPoints(lastX, lastY, x, y);
lastX = x;
lastY = y;
removeCount = (cursorX.size() / (6 * FPSmod)) + 1;
}
// else, sample one point at a time
else {
cursorX.add(x);
cursorY.add(y);
int max = 10 * FPSmod;
if (cursorX.size() > max)
removeCount = cursorX.size() - max;
}
// remove points from the lists
for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) {
cursorX.remove();
cursorY.remove();
}
// draw a fading trail
float alpha = 0f;
float t = 2f / cursorX.size();
Iterator<Integer> iterX = cursorX.iterator();
Iterator<Integer> iterY = cursorY.iterator();
while (iterX.hasNext()) {
int cx = iterX.next();
int cy = iterY.next();
alpha += t;
cursorTrail.setAlpha(alpha);
// if (cx != x || cy != y)
cursorTrail.drawCentered(cx, cy);
}
cursorTrail.drawCentered(x, y);
// increase the cursor size if pressed
int state = game.getCurrentStateID();
float scale = 1f;
if (((state == Opsu.STATE_GAME || state == Opsu.STATE_GAMEPAUSEMENU) && isGameKeyPressed()) ||
(input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) || input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON)))
scale = 1.25f;
// draw the other components
Image cursorScaled = cursor.getScaledCopy(scale);
cursorScaled.setRotation(cursor.getRotation());
cursorScaled.drawCentered(x, y);
if (cursorMiddle != null) {
Image cursorMiddleScaled = cursorMiddle.getScaledCopy(scale);
cursorMiddleScaled.setRotation(cursorMiddle.getRotation());
cursorMiddleScaled.drawCentered(x, y);
}
}
/**
* Adds all points between (x1, y1) and (x2, y2) to the cursor point lists.
* @author http://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm#Java
*/
private static void addCursorPoints(int x1, int y1, int x2, int y2) {
// delta of exact value and rounded value of the dependent variable
int d = 0;
int dy = Math.abs(y2 - y1);
int dx = Math.abs(x2 - x1);
int dy2 = (dy << 1); // slope scaling factors to avoid floating
int dx2 = (dx << 1); // point
int ix = x1 < x2 ? 1 : -1; // increment direction
int iy = y1 < y2 ? 1 : -1;
int k = 5; // sample size
if (dy <= dx) {
for (int i = 0; ; i++) {
if (i == k) {
cursorX.add(x1);
cursorY.add(y1);
i = 0;
}
if (x1 == x2)
break;
x1 += ix;
d += dy2;
if (d > dx) {
y1 += iy;
d -= dx2;
}
}
} else {
for (int i = 0; ; i++) {
if (i == k) {
cursorX.add(x1);
cursorY.add(y1);
i = 0;
}
if (y1 == y2)
break;
y1 += iy;
d += dx2;
if (d > dy) {
x1 += ix;
d -= dy2;
}
}
}
}
/**
* Rotates the cursor by a degree determined by a delta interval.
* If the old style cursor is being used, this will do nothing.
* @param delta the delta interval since the last call
*/
public static void updateCursor(int delta) {
if (cursorMiddle == null)
return;
cursor.rotate(delta / 40f);
}
/**
* Returns true if a game input key is pressed (mouse/keyboard left/right).
* @return true if pressed
*/
public static boolean isGameKeyPressed() {
return (input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON) ||
input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON) ||
input.isKeyDown(Options.getGameKeyLeft()) ||
input.isKeyDown(Options.getGameKeyRight()));
}
/**
* Draws the FPS at the bottom-right corner of the game container.
* If the option is not activated, this will do nothing.
*/
public static void drawFPS() {
if (!Options.isFPSCounterEnabled())
return;
String fps = String.format("FPS: %d", container.getFPS());
FONT_DEFAULT.drawString(
container.getWidth() - 15 - FONT_DEFAULT.getWidth(fps),
container.getHeight() - 15 - FONT_DEFAULT.getHeight(fps),
fps, Color.white
);
}
/**
* Takes a screenshot.
* @return true if successful
*/
public static boolean takeScreenShot() {
// TODO: should this be threaded?
try {
// create the screenshot directory
File dir = Options.getScreenshotDir();
if (!dir.isDirectory()) {
if (!dir.mkdir())
return false;
}
// create file name
SimpleDateFormat date = new SimpleDateFormat("yyyyMMdd_HHmmss");
File file = new File(dir, String.format("screenshot_%s.%s",
date.format(new Date()), Options.getScreenshotFormat()));
SoundController.playSound(SoundEffect.SHUTTER);
// copy the screen
Image screen = new Image(container.getWidth(), container.getHeight());
container.getGraphics().copyArea(screen, 0, 0);
ImageOut.write(screen, file.getAbsolutePath(), false);
screen.destroy();
} catch (SlickException e) {
Log.warn("Failed to take a screenshot.", e);
return false;
}
return true;
}
/**
* Loads a Unicode font.
* @param font the font to load
* @param padding the top and bottom padding
* @param colorEffect the ColorEffect
* @throws SlickException
*/
@SuppressWarnings("unchecked")
private static void loadFont(UnicodeFont font, int padding,
ColorEffect colorEffect) throws SlickException {
font.setPaddingTop(padding);
font.setPaddingBottom(padding);
font.addAsciiGlyphs();
font.getEffects().add(colorEffect);
font.loadGlyphs();
}
/**
* Adds and loads glyphs for an OsuFile's Unicode title and artist strings.
* @param osu the OsuFile
*/
public static void loadGlyphs(OsuFile osu) {
if (!Options.useUnicodeMetadata())
return;
boolean glyphsAdded = false;
if (!osu.titleUnicode.isEmpty() && !loadedGlyphs.contains(osu.titleUnicode)) {
Utils.FONT_LARGE.addGlyphs(osu.titleUnicode);
Utils.FONT_MEDIUM.addGlyphs(osu.titleUnicode);
Utils.FONT_DEFAULT.addGlyphs(osu.titleUnicode);
loadedGlyphs.add(osu.titleUnicode);
glyphsAdded = true;
}
if (!osu.artistUnicode.isEmpty() && !loadedGlyphs.contains(osu.artistUnicode)) {
Utils.FONT_LARGE.addGlyphs(osu.artistUnicode);
Utils.FONT_MEDIUM.addGlyphs(osu.artistUnicode);
Utils.FONT_DEFAULT.addGlyphs(osu.artistUnicode);
loadedGlyphs.add(osu.artistUnicode);
glyphsAdded = true;
}
if (glyphsAdded) {
try {
Utils.FONT_LARGE.loadGlyphs();
Utils.FONT_MEDIUM.loadGlyphs();
Utils.FONT_DEFAULT.loadGlyphs();
} catch (SlickException e) {
Log.warn("Failed to load glyphs.", e);
}
}
}
}