Added support for Unicode (non-English) metadata.

- Setting is off by default, and can be switched on in the options menu.
- Changed default font to "Arial Unicode MS" (CJK), with fallback to "Lucida Sans Console" (non-CJK).  Cross-platform support may be added later.

Signed-off-by: Jeffrey Han <itdelatrisu@gmail.com>
This commit is contained in:
Jeffrey Han 2014-08-24 23:48:52 -04:00
parent 9a94c03b4e
commit 904a54df26
9 changed files with 130 additions and 21 deletions

View File

@ -177,7 +177,7 @@ public class MusicController {
public static String getTrackName() { public static String getTrackName() {
if (!trackExists() || lastOsu == null) if (!trackExists() || lastOsu == null)
return null; return null;
return lastOsu.title; return lastOsu.getTitle();
} }
/** /**
@ -186,7 +186,7 @@ public class MusicController {
public static String getArtistName() { public static String getArtistName() {
if (!trackExists() || lastOsu == null) if (!trackExists() || lastOsu == null)
return null; return null;
return lastOsu.artist; return lastOsu.getArtist();
} }
/** /**

View File

@ -18,6 +18,8 @@
package itdelatrisu.opsu; package itdelatrisu.opsu;
import itdelatrisu.opsu.states.Options;
import java.io.File; import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
@ -108,9 +110,28 @@ public class OsuFile implements Comparable<OsuFile> {
/** /**
* Returns the associated file object. * Returns the associated file object.
* @return the File object
*/ */
public File getFile() { return file; } public File getFile() { return file; }
/**
* Returns the song title.
* If configured, the Unicode string will be returned instead.
* @return the song title
*/
public String getTitle() {
return (Options.useUnicodeMetadata() && !titleUnicode.isEmpty()) ? titleUnicode : title;
}
/**
* Returns the song artist.
* If configured, the Unicode string will be returned instead.
* @return the song artist
*/
public String getArtist() {
return (Options.useUnicodeMetadata() && !artistUnicode.isEmpty()) ? artistUnicode : artist;
}
/** /**
* Draws the background associated with the OsuFile. * Draws the background associated with the OsuFile.
* @param width the container width * @param width the container width
@ -154,6 +175,6 @@ public class OsuFile implements Comparable<OsuFile> {
*/ */
@Override @Override
public String toString() { public String toString() {
return String.format("%s - %s [%s]", artist, title, version); return String.format("%s - %s [%s]", getArtist(), getTitle(), version);
} }
} }

View File

@ -95,9 +95,9 @@ public class OsuGroupNode {
float cx = x + (bg.getWidth() * 0.05f) - xOffset; float cx = x + (bg.getWidth() * 0.05f) - xOffset;
float cy = y + (bg.getHeight() * 0.2f) - 3; float cy = y + (bg.getHeight() * 0.2f) - 3;
Utils.FONT_MEDIUM.drawString(cx, cy, osu.title, textColor); Utils.FONT_MEDIUM.drawString(cx, cy, osu.getTitle(), textColor);
Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 4, Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 4,
String.format("%s // %s", osu.artist, osu.creator), textColor); String.format("%s // %s", osu.getArtist(), osu.creator), textColor);
if (expanded || osuFiles.size() == 1) if (expanded || osuFiles.size() == 1)
Utils.FONT_BOLD.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 8, Utils.FONT_BOLD.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 8,
osu.version, textColor); osu.version, textColor);
@ -144,7 +144,7 @@ public class OsuGroupNode {
@Override @Override
public String toString() { public String toString() {
if (osuFileIndex == -1) if (osuFileIndex == -1)
return String.format("%s - %s", osuFiles.get(0).artist, osuFiles.get(0).title); return String.format("%s - %s", osuFiles.get(0).getArtist(), osuFiles.get(0).getTitle());
else else
return osuFiles.get(osuFileIndex).toString(); return osuFiles.get(osuFileIndex).toString();
} }

View File

@ -20,9 +20,11 @@ package itdelatrisu.opsu;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader; import java.io.FileReader;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedList; import java.util.LinkedList;
@ -110,7 +112,7 @@ public class OsuParser {
private static OsuFile parseFile(File file, ArrayList<OsuFile> osuFiles, boolean parseObjects) { private static OsuFile parseFile(File file, ArrayList<OsuFile> osuFiles, boolean parseObjects) {
OsuFile osu = new OsuFile(file); OsuFile osu = new OsuFile(file);
try (BufferedReader in = new BufferedReader(new FileReader(file))) { try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) {
// initialize timing point list // initialize timing point list
osu.timingPoints = new ArrayList<OsuTimingPoint>(); osu.timingPoints = new ArrayList<OsuTimingPoint>();

View File

@ -21,9 +21,12 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.states.Options; import itdelatrisu.opsu.states.Options;
import java.awt.Font; import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.io.File; import java.io.File;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
@ -104,6 +107,11 @@ public class Utils {
cursorX = new LinkedList<Integer>(), cursorX = new LinkedList<Integer>(),
cursorY = 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 // game-related variables
private static GameContainer container; private static GameContainer container;
private static StateBasedGame game; private static StateBasedGame game;
@ -148,15 +156,23 @@ public class Utils {
// create fonts // create fonts
float fontBase; float fontBase;
if (height <= 600) if (height <= 600)
fontBase = 9f;
else if (height < 800)
fontBase = 10f; fontBase = 10f;
else if (height < 800)
fontBase = 11f;
else if (height <= 900) else if (height <= 900)
fontBase = 12f; fontBase = 13f;
else else
fontBase = 14f; fontBase = 15f;
Font font = new Font("Lucida Sans Unicode", Font.PLAIN, (int) (fontBase * 4 / 3)); // TODO: cross-platform multilingual support
String fontName = "";
String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
if (Arrays.asList(fontNames).contains("Arial Unicode MS"))
fontName = "Arial Unicode MS";
else
fontName = "Lucida Sans Console";
Font font = new Font(fontName, Font.PLAIN, (int) (fontBase * 4 / 3));
FONT_DEFAULT = new UnicodeFont(font); FONT_DEFAULT = new UnicodeFont(font);
FONT_BOLD = new UnicodeFont(font.deriveFont(Font.BOLD)); FONT_BOLD = new UnicodeFont(font.deriveFont(Font.BOLD));
FONT_XLARGE = new UnicodeFont(font.deriveFont(fontBase * 4)); FONT_XLARGE = new UnicodeFont(font.deriveFont(fontBase * 4));
@ -485,4 +501,35 @@ public class Utils {
} }
return true; return true;
} }
/**
* Adds and loads glyphs for an OsuFile's Unicode title and artist strings.
* @param osu the OsuFile
*/
public static void loadGlyphs(OsuFile osu) {
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 && Options.useUnicodeMetadata()) {
try {
Utils.FONT_LARGE.loadGlyphs();
Utils.FONT_MEDIUM.loadGlyphs();
Utils.FONT_DEFAULT.loadGlyphs();
} catch (SlickException e) {
Log.warn("Failed to load glyphs.", e);
}
}
}
} }

View File

@ -121,7 +121,7 @@ public class GameRanking extends BasicGameState {
// header text // header text
g.setColor(Color.white); g.setColor(Color.white);
Utils.FONT_LARGE.drawString(10, 0, Utils.FONT_LARGE.drawString(10, 0,
String.format("%s - %s [%s]", osu.artist, osu.title, osu.version)); String.format("%s - %s [%s]", osu.getArtist(), osu.getTitle(), osu.version));
Utils.FONT_MEDIUM.drawString(10, Utils.FONT_LARGE.getLineHeight() - 6, Utils.FONT_MEDIUM.drawString(10, Utils.FONT_LARGE.getLineHeight() - 6,
String.format("Beatmap by %s", osu.creator)); String.format("Beatmap by %s", osu.creator));

View File

@ -128,7 +128,8 @@ public class Options extends BasicGameState {
CHECKPOINT, CHECKPOINT,
DISABLE_SOUNDS, DISABLE_SOUNDS,
KEY_LEFT, KEY_LEFT,
KEY_RIGHT; KEY_RIGHT,
SHOW_UNICODE;
}; };
/** /**
@ -169,6 +170,7 @@ public class Options extends BasicGameState {
// GameOption.FULLSCREEN, // GameOption.FULLSCREEN,
GameOption.TARGET_FPS, GameOption.TARGET_FPS,
GameOption.SHOW_FPS, GameOption.SHOW_FPS,
GameOption.SHOW_UNICODE,
GameOption.SCREENSHOT_FORMAT, GameOption.SCREENSHOT_FORMAT,
GameOption.NEW_CURSOR, GameOption.NEW_CURSOR,
GameOption.DYNAMIC_BACKGROUND, GameOption.DYNAMIC_BACKGROUND,
@ -363,6 +365,11 @@ public class Options extends BasicGameState {
private static boolean disableSound = private static boolean disableSound =
(System.getProperty("os.name").toLowerCase().indexOf("linux") > -1); (System.getProperty("os.name").toLowerCase().indexOf("linux") > -1);
/**
* Whether or not to display non-English metadata.
*/
private static boolean showUnicode = false;
/** /**
* Left and right game keys. * Left and right game keys.
*/ */
@ -607,6 +614,18 @@ public class Options extends BasicGameState {
keyEntryLeft = false; keyEntryLeft = false;
keyEntryRight = true; keyEntryRight = true;
break; break;
case SHOW_UNICODE:
showUnicode = !showUnicode;
if (showUnicode) {
try {
Utils.FONT_LARGE.loadGlyphs();
Utils.FONT_MEDIUM.loadGlyphs();
Utils.FONT_DEFAULT.loadGlyphs();
} catch (SlickException e) {
Log.warn("Failed to load glyphs.", e);
}
}
break;
default: default:
break; break;
} }
@ -780,6 +799,12 @@ public class Options extends BasicGameState {
"Show an FPS counter in the bottom-right hand corner." "Show an FPS counter in the bottom-right hand corner."
); );
break; break;
case SHOW_UNICODE:
drawOption(pos, "Prefer Non-English Metadata",
showUnicode ? "Yes" : "No",
"Where available, song titles will be shown in their native language."
);
break;
case NEW_CURSOR: case NEW_CURSOR:
drawOption(pos, "Enable New Cursor", drawOption(pos, "Enable New Cursor",
newCursor ? "Yes" : "No", newCursor ? "Yes" : "No",
@ -1132,6 +1157,12 @@ public class Options extends BasicGameState {
*/ */
public static boolean isSoundDisabled() { return disableSound; } public static boolean isSoundDisabled() { return disableSound; }
/**
* Returns whether or not to use non-English metadata where available.
* @return true if Unicode preferred
*/
public static boolean useUnicodeMetadata() { return showUnicode; }
/** /**
* Sets the track checkpoint time, if within bounds. * Sets the track checkpoint time, if within bounds.
* @param time the track position (in ms) * @param time the track position (in ms)
@ -1288,6 +1319,9 @@ public class Options extends BasicGameState {
case "FpsCounter": case "FpsCounter":
showFPS = Boolean.parseBoolean(value); showFPS = Boolean.parseBoolean(value);
break; break;
case "ShowUnicode":
showUnicode = Boolean.parseBoolean(value);
break;
case "NewCursor": case "NewCursor":
newCursor = Boolean.parseBoolean(value); newCursor = Boolean.parseBoolean(value);
break; break;
@ -1416,6 +1450,8 @@ public class Options extends BasicGameState {
writer.newLine(); writer.newLine();
writer.write(String.format("FpsCounter = %b", showFPS)); writer.write(String.format("FpsCounter = %b", showFPS));
writer.newLine(); writer.newLine();
writer.write(String.format("ShowUnicode = %b", showUnicode));
writer.newLine();
writer.write(String.format("ScreenshotFormat = %d", screenshotFormatIndex)); writer.write(String.format("ScreenshotFormat = %d", screenshotFormatIndex));
writer.newLine(); writer.newLine();
writer.write(String.format("NewCursor = %b", newCursor)); writer.write(String.format("NewCursor = %b", newCursor));

View File

@ -240,6 +240,7 @@ public class SongMenu extends BasicGameState {
OsuGroupNode node = startNode; OsuGroupNode node = startNode;
for (int i = 0; i < MAX_BUTTONS && node != null; i++) { for (int i = 0; i < MAX_BUTTONS && node != null; i++) {
node.draw(buttonX, buttonY + (i*buttonOffset), (node == focusNode)); node.draw(buttonX, buttonY + (i*buttonOffset), (node == focusNode));
Utils.loadGlyphs(node.osuFiles.get(0));
node = node.next; node = node.next;
} }
@ -569,7 +570,9 @@ public class SongMenu extends BasicGameState {
if (flag || (startNode.index == 0 && startNode.osuFileIndex == -1 && startNode.prev == null)) if (flag || (startNode.index == 0 && startNode.osuFileIndex == -1 && startNode.prev == null))
startNode = node; startNode = node;
focusNode = Opsu.groups.getNode(node, pos); focusNode = Opsu.groups.getNode(node, pos);
MusicController.play(focusNode.osuFiles.get(focusNode.osuFileIndex), true); OsuFile osu = focusNode.osuFiles.get(focusNode.osuFileIndex);
MusicController.play(osu, true);
Utils.loadGlyphs(osu);
// check startNode bounds // check startNode bounds
while (startNode.index >= Opsu.groups.size() + length - MAX_BUTTONS && startNode.prev != null) while (startNode.index >= Opsu.groups.size() + length - MAX_BUTTONS && startNode.prev != null)

View File

@ -110,7 +110,6 @@ public class Splash extends BasicGameState {
// load other resources in a new thread // load other resources in a new thread
final int width = container.getWidth(); final int width = container.getWidth();
final int height = container.getHeight(); final int height = container.getHeight();
final SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
new Thread() { new Thread() {
@Override @Override
public void run() { public void run() {
@ -122,10 +121,6 @@ public class Splash extends BasicGameState {
// parse song directory // parse song directory
OsuParser.parseAllFiles(beatmapDir, width, height); OsuParser.parseAllFiles(beatmapDir, width, height);
// initialize song list
Opsu.groups.init();
menu.setFocus(Opsu.groups.getRandomNode(), -1, true);
// load sounds // load sounds
SoundController.init(); SoundController.init();
@ -140,9 +135,14 @@ public class Splash extends BasicGameState {
logo.setAlpha(alpha + (delta / 400f)); logo.setAlpha(alpha + (delta / 400f));
// change states when loading complete // change states when loading complete
if (finished && alpha >= 1f) if (finished && alpha >= 1f) {
// initialize song list
Opsu.groups.init();
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).setFocus(Opsu.groups.getRandomNode(), -1, true);
game.enterState(Opsu.STATE_MAINMENU); game.enterState(Opsu.STATE_MAINMENU);
} }
}
@Override @Override
public int getID() { return state; } public int getID() { return state; }