Added multi-image support to GameImage.

- Allows loading an undetermined number of files using a format string (e.g. combo bursts, slider balls).
- Fixes bug with those images not being properly reloaded.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2015-01-21 17:10:31 -05:00
parent 131c8a5637
commit e02cf60312
3 changed files with 178 additions and 81 deletions

View File

@ -104,6 +104,7 @@ public enum GameImage {
APPROACHCIRCLE ("approachcircle", "png"), APPROACHCIRCLE ("approachcircle", "png"),
// Slider // Slider
SLIDER_BALL ("sliderb", "sliderb%d", "png"),
SLIDER_FOLLOWCIRCLE ("sliderfollowcircle", "png"), SLIDER_FOLLOWCIRCLE ("sliderfollowcircle", "png"),
REVERSEARROW ("reversearrow", "png"), REVERSEARROW ("reversearrow", "png"),
SLIDER_TICK ("sliderscorepoint", "png"), SLIDER_TICK ("sliderscorepoint", "png"),
@ -117,6 +118,7 @@ public enum GameImage {
SPINNER_OSU ("spinner-osu", "png"), SPINNER_OSU ("spinner-osu", "png"),
// Game Score // Game Score
COMBO_BURST ("comboburst", "comboburst-%d", "png"),
SCOREBAR_BG ("scorebar-bg", "png") { SCOREBAR_BG ("scorebar-bg", "png") {
@Override @Override
protected Image process_sub(Image img, int w, int h) { protected Image process_sub(Image img, int w, int h) {
@ -395,6 +397,11 @@ public enum GameImage {
*/ */
private String filename; private String filename;
/**
* The formatted file name string (for loading multiple images).
*/
private String filenameFormat;
/** /**
* Image file type. * Image file type.
*/ */
@ -416,11 +423,21 @@ public enum GameImage {
*/ */
private Image defaultImage; private Image defaultImage;
/**
* The default image array.
*/
private Image[] defaultImages;
/** /**
* The beatmap skin image (optional, temporary). * The beatmap skin image (optional, temporary).
*/ */
private Image skinImage; private Image skinImage;
/**
* The beatmap skin image array (optional, temporary).
*/
private Image[] skinImages;
/** /**
* Container dimensions. * Container dimensions.
*/ */
@ -446,8 +463,10 @@ public enum GameImage {
* This does NOT destroy images, so be careful of memory leaks! * This does NOT destroy images, so be careful of memory leaks!
*/ */
public static void clearReferences() { public static void clearReferences() {
for (GameImage img : GameImage.values()) for (GameImage img : GameImage.values()) {
img.defaultImage = img.skinImage = null; img.defaultImage = img.skinImage = null;
img.defaultImages = img.skinImages = null;
}
} }
/** /**
@ -480,6 +499,19 @@ public enum GameImage {
return b; return b;
} }
/**
* Returns a list of possible filenames (with extensions).
* @return filename list
*/
private static List<String> getFileNames(String filename, byte type) {
List<String> list = new ArrayList<String>(2);
if ((type & IMG_PNG) != 0)
list.add(String.format("%s.png", filename));
if ((type & IMG_JPG) != 0)
list.add(String.format("%s.jpg", filename));
return list;
}
/** /**
* Constructor for game-related images (skinnable and preloaded). * Constructor for game-related images (skinnable and preloaded).
* @param filename the image file name * @param filename the image file name
@ -492,6 +524,20 @@ public enum GameImage {
this.preload = true; this.preload = true;
} }
/**
* Constructor for an array of game-related images (skinnable and preloaded).
* @param filename the image file name
* @param filenameFormat the formatted file name string (for loading multiple images)
* @param type the file types (separated by '|')
*/
GameImage(String filename, String filenameFormat, String type) {
this.filename = filename;
this.filenameFormat = filenameFormat;
this.type = getType(type);
this.skinnable = true;
this.preload = true;
}
/** /**
* Constructor for general images. * Constructor for general images.
* @param filename the image file name * @param filename the image file name
@ -523,11 +569,19 @@ public enum GameImage {
* The skin image takes priority over the default image. * The skin image takes priority over the default image.
*/ */
public Image getImage() { public Image getImage() {
if (defaultImage == null) setDefaultImage();
setDefaultImage();
return (skinImage != null) ? skinImage : defaultImage; return (skinImage != null) ? skinImage : defaultImage;
} }
/**
* Returns the image array associated with this resource.
* The skin images takes priority over the default images.
*/
public Image[] getImages() {
setDefaultImage();
return (skinImages != null) ? skinImages : defaultImages;
}
/** /**
* Sets the image associated with this resource to another image. * Sets the image associated with this resource to another image.
* The skin image takes priority over the default image. * The skin image takes priority over the default image.
@ -540,16 +594,17 @@ public enum GameImage {
} }
/** /**
* Returns a list of possible filenames (with extensions). * Sets an image associated with this resource to another image.
* @return filename list * The skin image takes priority over the default image.
*/ */
private List<String> getFileNames() { public void setImage(Image img, int index) {
List<String> list = new ArrayList<String>(2); if (skinImages != null) {
if ((type & IMG_PNG) != 0) if (index < skinImages.length)
list.add(String.format("%s.png", filename)); this.skinImages[index] = img;
if ((type & IMG_JPG) != 0) } else {
list.add(String.format("%s.jpg", filename)); if (index < defaultImages.length)
return list; this.defaultImages[index] = img;
}
} }
/** /**
@ -557,9 +612,40 @@ public enum GameImage {
* If the default image has already been loaded, this will do nothing. * If the default image has already been loaded, this will do nothing.
*/ */
public void setDefaultImage() { public void setDefaultImage() {
if (defaultImage != null) if (defaultImage != null || defaultImages != null)
return; return;
for (String name : getFileNames()) {
// load image array
if (filenameFormat != null) {
List<Image> list = new ArrayList<Image>();
int i = 0;
boolean loaded;
do {
loaded = false;
for (String name : getFileNames(String.format(filenameFormat, i), type)) {
try {
// try loading the image
Image img = new Image(name);
// image successfully loaded
list.add(img);
loaded = true;
break;
} catch (SlickException | RuntimeException e) {
continue;
}
}
i++;
} while (loaded);
if (!list.isEmpty()) {
this.defaultImages = list.toArray(new Image[list.size()]);
process();
return;
}
}
// load single image
for (String name : getFileNames(filename, type)) {
try { try {
// try loading the image // try loading the image
Image img = new Image(name); Image img = new Image(name);
@ -584,16 +670,49 @@ public enum GameImage {
if (dir == null) if (dir == null)
return false; return false;
// destroy the existing image, if any // destroy the existing images, if any
destroySkinImage(); destroySkinImage();
// beatmap skins disabled // beatmap skins disabled
if (Options.isBeatmapSkinIgnored()) if (Options.isBeatmapSkinIgnored())
return false; return false;
// look for multiple skin images
if (filenameFormat != null) {
List<Image> list = new ArrayList<Image>();
int i = 0;
boolean loaded;
do {
loaded = false;
for (String name : getFileNames(String.format(filenameFormat, i), type)) {
File file = new File(dir, name);
if (!file.isFile())
continue;
try {
// try loading the image
Image img = new Image(file.getAbsolutePath());
// image successfully loaded
list.add(img);
loaded = true;
break;
} catch (SlickException | RuntimeException e) {
continue;
}
}
i++;
} while (loaded);
if (!list.isEmpty()) {
this.skinImages = list.toArray(new Image[list.size()]);
process();
skinImageLoaded = true;
return true;
}
}
// look for a skin image // look for a skin image
String errorFile = null; String errorFile = null;
for (String name : getFileNames()) { for (String name : getFileNames(filename, type)) {
File file = new File(dir, name); File file = new File(dir, name);
if (!file.isFile()) if (!file.isFile())
continue; continue;
@ -624,17 +743,32 @@ public enum GameImage {
public boolean hasSkinImage() { return (skinImage != null && !skinImage.isDestroyed()); } public boolean hasSkinImage() { return (skinImage != null && !skinImage.isDestroyed()); }
/** /**
* Destroys the associated skin image, if any. * Returns whether skin images are currently loaded.
* @return true if any skin image exists
*/
public boolean hasSkinImages() { return (skinImages != null); }
/**
* Destroys the associated skin image(s), if any.
*/ */
private void destroySkinImage() { private void destroySkinImage() {
if (skinImage == null) if (skinImage == null && skinImages == null)
return; return;
try { try {
if (!skinImage.isDestroyed()) if (skinImage != null) {
skinImage.destroy(); if (!skinImage.isDestroyed())
skinImage = null; skinImage.destroy();
skinImage = null;
}
if (skinImages != null) {
for (int i = 0; i < skinImages.length; i++) {
if (!skinImages[i].isDestroyed())
skinImages[i].destroy();
}
skinImages = null;
}
} catch (SlickException e) { } catch (SlickException e) {
ErrorHandler.error(String.format("Failed to destroy skin image for '%s'.", this.name()), e, true); ErrorHandler.error(String.format("Failed to destroy skin images for '%s'.", this.name()), e, true);
} }
} }
@ -653,6 +787,13 @@ public enum GameImage {
* Performs individual post-loading actions on the image. * Performs individual post-loading actions on the image.
*/ */
private void process() { private void process() {
setImage(process_sub(getImage(), containerWidth, containerHeight)); if (skinImages != null) {
for (int i = 0; i < skinImages.length; i++)
setImage(process_sub(getImages()[i], containerWidth, containerHeight), i);
} else if (defaultImages != null && skinImage == null) {
for (int i = 0; i < defaultImages.length; i++)
setImage(process_sub(getImages()[i], containerWidth, containerHeight), i);
} else
setImage(process_sub(getImage(), containerWidth, containerHeight));
} }
} }

View File

@ -249,35 +249,11 @@ public class GameScore {
*/ */
public void loadImages(File dir) throws SlickException { public void loadImages(File dir) throws SlickException {
// combo burst images // combo burst images
if (comboBurstImages != null) { if (GameImage.COMBO_BURST.hasSkinImages() ||
for (int i = 0; i < comboBurstImages.length; i++) { (!GameImage.COMBO_BURST.hasSkinImage() && GameImage.COMBO_BURST.getImages() != null))
if (!comboBurstImages[i].isDestroyed()) comboBurstImages = GameImage.COMBO_BURST.getImages();
comboBurstImages[i].destroy(); else
} comboBurstImages = new Image[]{ GameImage.COMBO_BURST.getImage() };
}
LinkedList<Image> comboBurst = new LinkedList<Image>();
String comboFormat = "comboburst-%d.png";
int comboIndex = 0;
File comboFile = new File(dir, "comboburst.png");
File comboFileN = new File(dir, String.format(comboFormat, comboIndex));
if (comboFileN.isFile()) { // beatmap provides images
do {
comboBurst.add(new Image(comboFileN.getAbsolutePath()));
comboFileN = new File(dir, String.format(comboFormat, ++comboIndex));
} while (comboFileN.isFile());
} else if (comboFile.isFile()) // beatmap provides single image
comboBurst.add(new Image(comboFile.getAbsolutePath()));
else { // load default images
while (true) {
try {
Image comboImage = new Image(String.format(comboFormat, comboIndex++));
comboBurst.add(comboImage);
} catch (Exception e) {
break;
}
}
}
comboBurstImages = comboBurst.toArray(new Image[comboBurst.size()]);
// default symbol images // default symbol images
defaultSymbols = new Image[10]; defaultSymbols = new Image[10];

View File

@ -27,8 +27,6 @@ import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.states.Game; import itdelatrisu.opsu.states.Game;
import java.io.File;
import org.newdawn.slick.Animation; import org.newdawn.slick.Animation;
import org.newdawn.slick.Color; import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer; import org.newdawn.slick.GameContainer;
@ -282,33 +280,15 @@ public class Slider implements HitObject {
diameter = diameter * container.getWidth() / 640; // convert from Osupixels (640x480) diameter = diameter * container.getWidth() / 640; // convert from Osupixels (640x480)
// slider ball // slider ball
if (sliderBall != null) { Image[] sliderBallImages;
for (int i = 0; i < sliderBall.getFrameCount(); i++) { if (GameImage.SLIDER_BALL.hasSkinImages() ||
Image img = sliderBall.getImage(i); (!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
if (!img.isDestroyed()) sliderBallImages = GameImage.SLIDER_BALL.getImages();
img.destroy(); else
} sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
} for (int i = 0; i < sliderBallImages.length; i++)
sliderBall = new Animation(); sliderBallImages[i] = sliderBallImages[i].getScaledCopy(diameter * 118 / 128, diameter * 118 / 128);
String sliderFormat = "sliderb%d.png"; sliderBall = new Animation(sliderBallImages, 60);
int sliderIndex = 0;
File dir = MusicController.getOsuFile().getFile().getParentFile();
File slider = new File(dir, String.format(sliderFormat, sliderIndex));
if (slider.isFile()) {
do {
sliderBall.addFrame(new Image(slider.getAbsolutePath()).getScaledCopy(diameter * 118 / 128, diameter * 118 / 128), 60);
slider = new File(dir, String.format(sliderFormat, ++sliderIndex));
} while (slider.isFile());
} else {
while (true) {
try {
Image sliderFrame = new Image(String.format(sliderFormat, sliderIndex++));
sliderBall.addFrame(sliderFrame.getScaledCopy(diameter * 118 / 128, diameter * 118 / 128), 60);
} catch (Exception e) {
break;
}
}
}
GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameter * 259 / 128, diameter * 259 / 128)); GameImage.SLIDER_FOLLOWCIRCLE.setImage(GameImage.SLIDER_FOLLOWCIRCLE.getImage().getScaledCopy(diameter * 259 / 128, diameter * 259 / 128));
GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameter, diameter)); GameImage.REVERSEARROW.setImage(GameImage.REVERSEARROW.getImage().getScaledCopy(diameter, diameter));