diff --git a/res/readme.txt b/res/readme.txt new file mode 100644 index 00000000..f30e36dd --- /dev/null +++ b/res/readme.txt @@ -0,0 +1,45 @@ +opsu! + +opsu! is an unofficial open-source client for the rhythm game osu!, written in Java using Slick2D and LWJGL (wrappers around OpenGL and OpenAL). + +opsu! runs on Windows, OS X, and Linux. A libGDX port also supports Android devices. + +Getting Started + +opsu! requires "beatmaps" to run, which contain the songs and gameplay data. These can be downloaded directly through opsu! in the downloads menu, or manually from the osu! website (requires registration) and mirror sites like Bloodcat. Place any manually downloaded beatmaps (in .osz format) in the Import/ directory for opsu! to unpack them automatically. + +The beatmap directory can be changed by setting the "BeatmapDirectory" value in the generated configuration file. + +First Run + +opsu! will parse all beatmaps when launched, which can take a while for the first time. If no beatmaps are found, the game will prompt you to download some to get started. + +Game settings can be changed in the options menu, accessed by clicking the "Other Options" button in the song menu. The "Music Offset" value will likely need to be adjusted initially, or whenever hit objects are out of sync with the music. + +Directory Structure + +The following files and folders will be created by opsu! as needed: + + opsu.cfg: The configuration file. Most (but not all) of the settings can be changed through the options menu. + opsu.log: The error log. All critical errors displayed in-game are also logged to this file, and other warnings not shown are logged as well. + Songs/: The beatmap directory. The parser searches all of its subdirectories for .osu files to load. + Skins/: The skins directory. Each skin must be placed in a folder within this directory. Any game resource (in res/) can be skinned by placing a file with the same name in a skin folder. Skins can be selected in the options menu. + Replays/: The replay directory. Replays of each completed game are saved as .osr files, and can be viewed at a later time or shared with others. + Import/: The import directory. All beatmap packs (.osz) and skin packs (.osk) are unpacked to the proper location. All replays (.osr) are moved to the replay directory, and their scores saved to the scores database. + + +opsu! - an open-source osu! client +Copyright (C) 2014-2017 Jeffrey Han + fluddokt + +This program 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. + +This program 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 this program. If not, see . \ No newline at end of file diff --git a/src/fluddokt/ex/DeviceInfo.java b/src/fluddokt/ex/DeviceInfo.java index dd4e20d0..9d08b78d 100644 --- a/src/fluddokt/ex/DeviceInfo.java +++ b/src/fluddokt/ex/DeviceInfo.java @@ -1,8 +1,13 @@ package fluddokt.ex; +import fluddokt.opsu.fake.File; + public class DeviceInfo { public static DeviceInfo info = new DeviceInfo(); public String getInfo() { return ""; } + public File getDownloadDir() { + return null; + } } diff --git a/src/fluddokt/opsu/fake/DynamicFreeTypeFont.java b/src/fluddokt/opsu/fake/DynamicFreeTypeFont.java index 93d3aadf..e36ea626 100644 --- a/src/fluddokt/opsu/fake/DynamicFreeTypeFont.java +++ b/src/fluddokt/opsu/fake/DynamicFreeTypeFont.java @@ -22,6 +22,7 @@ import com.badlogic.gdx.utils.IntMap; public class DynamicFreeTypeFont { + static int PAD = 1; FileHandle handle; Face face; Face backupface; @@ -236,7 +237,7 @@ public CharInfo addChar(char c) { // cant fit width, go to next line if (x + pixMapWidth > curPixmap.getWidth()) { x = 0; - y += maxHeight; + y += maxHeight + PAD; maxHeight = 0; } // find the max Height of the this line @@ -260,7 +261,7 @@ public CharInfo addChar(char c) { TextureRegion tr = new TextureRegion(curTexture, x, y, pixMapWidth, pixmap.getHeight()); tr.flip(false, true); - x += pixMapWidth; + x += pixMapWidth + PAD; GlyphMetrics metrics = slot.getMetrics(); CharInfo ci = new CharInfo(); diff --git a/src/fluddokt/opsu/fake/File.java b/src/fluddokt/opsu/fake/File.java index 57bc26ed..d2cff8af 100644 --- a/src/fluddokt/opsu/fake/File.java +++ b/src/fluddokt/opsu/fake/File.java @@ -70,7 +70,7 @@ public File(String name) { } - private File(FileHandle nfh) { + public File(FileHandle nfh) { this.fh = nfh; } @@ -222,4 +222,8 @@ public boolean isExternal() { ); } + public long length() { + return fh.length(); + } + } diff --git a/src/fluddokt/opsu/fake/GameOpsu.java b/src/fluddokt/opsu/fake/GameOpsu.java index 82e8a903..c591b3ab 100644 --- a/src/fluddokt/opsu/fake/GameOpsu.java +++ b/src/fluddokt/opsu/fake/GameOpsu.java @@ -2,6 +2,8 @@ import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; @@ -23,7 +25,7 @@ public class GameOpsu extends com.badlogic.gdx.Game { - public final static String VERSION = "0.16.0a"; + public final static String VERSION = "0.16.0b"; public StateBasedGame sbg; Stage stage; @@ -108,6 +110,25 @@ public void render() { File nomediafile = new File(dataDir, ".nomedia"); if(!nomediafile.exists()) new FileOutputStream(nomediafile.getIOFile()).close(); + + File readmefile = new File(new File(Gdx.files.internal("res")), "readme.txt"); + File readmefilecpyto = new File(dataDir, "readme.txt"); + System.out.println("readmeexist: "+readmefile.exists()+" "+readmefilecpyto.exists()+" "+readmefile.length()+" "+readmefilecpyto.length()); + if (readmefile.exists() && !readmefilecpyto.exists() || readmefile.length() != readmefilecpyto.length()) { + try( + InputStream in = new FileInputStream(readmefile); + OutputStream out = new fluddokt.opsu.fake.FileOutputStream(readmefilecpyto); + ) { + byte[] buf = new byte[512]; + while (true) { + int read = in.read(buf); + if (read < 0) + break; + out.write(buf, 0, read); + } + } + } + } System.out.println("Local Dir:"+Gdx.files.getLocalStoragePath()); Gdx.input.setInputProcessor(new InputMultiplexer(stage, sbg)); diff --git a/src/fluddokt/opsu/fake/Image.java b/src/fluddokt/opsu/fake/Image.java index 3a46a651..8024db38 100644 --- a/src/fluddokt/opsu/fake/Image.java +++ b/src/fluddokt/opsu/fake/Image.java @@ -3,6 +3,7 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.LinkedList; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Pixmap; @@ -10,6 +11,7 @@ import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.Texture.TextureFilter; import com.badlogic.gdx.graphics.TextureData; +import com.badlogic.gdx.graphics.g2d.PixmapPacker; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.graphics.glutils.FrameBuffer; import com.badlogic.gdx.utils.GdxRuntimeException; @@ -404,7 +406,14 @@ public Image getScaledCopy(float f) { } public void setAlpha(float f) { - this.alpha = f; + this.alpha = clamp(f, 0, 1); + } + public float clamp(float val, float low, float high) { + if (val < low) + return low; + if (val > high) + return high; + return val; } public boolean isDestroyed() { diff --git a/src/fluddokt/opsu/fake/TextField.java b/src/fluddokt/opsu/fake/TextField.java index ea053b90..287c1d01 100644 --- a/src/fluddokt/opsu/fake/TextField.java +++ b/src/fluddokt/opsu/fake/TextField.java @@ -11,7 +11,7 @@ public class TextField extends GInputAdapter { UnicodeFont font; int x, y, w, h; - StringBuilder str = new StringBuilder(); + String str = new String(); Color bgColor = Color.green, textColor = Color.blue, borderColor = Color.red; GameContainer container; @@ -61,6 +61,7 @@ public void render(GUIContext container2, Graphics g) { g.fillRect(x, y, w, h); g.setColor(borderColor); g.drawRect(x, y, w, h); + g.setColor(textColor); g.drawString(font, str.toString(), x, y); } @@ -74,7 +75,7 @@ public String getText() { } public void setText(String string) { - str = new StringBuilder(string); + str = string; } public int getWidth() { @@ -89,9 +90,9 @@ public int getHeight() { public void keyType(char character) { if (hasFocus) { if (character == KeyEvent.VK_BACK_SPACE) - str.setLength(Math.max(str.length() - 1, 0)); + str = str.substring(0, Math.max(str.length() - 1, 0)); else if (!Character.isISOControl(character)) - str.append(character); + str += character; consumeEvent(); } } diff --git a/src/itdelatrisu/opsu/GameData.java b/src/itdelatrisu/opsu/GameData.java index 440abc4f..0be92f36 100644 --- a/src/itdelatrisu/opsu/GameData.java +++ b/src/itdelatrisu/opsu/GameData.java @@ -19,7 +19,6 @@ package itdelatrisu.opsu; import fluddokt.opsu.fake.*; - import itdelatrisu.opsu.audio.HitSound; import itdelatrisu.opsu.audio.MusicController; import itdelatrisu.opsu.audio.SoundController; diff --git a/src/itdelatrisu/opsu/Utils.java b/src/itdelatrisu/opsu/Utils.java index 6c93097d..6494a23f 100644 --- a/src/itdelatrisu/opsu/Utils.java +++ b/src/itdelatrisu/opsu/Utils.java @@ -439,14 +439,16 @@ else if (doReplace) * @param file the ZIP archive * @param dest the destination directory */ - public static void unzip(File file, File dest) { + public static boolean unzip(File file, File dest) { try { ZipFile zipFile = new ZipFile(file.getIOFile()); zipFile.extractAll(dest.getAbsolutePath()); + return true; } catch (ZipException e) { - ErrorHandler.error(String.format("Failed to unzip file %s to dest %s.", - file.getAbsolutePath(), dest.getAbsolutePath()), e, false); + //ErrorHandler.error(String.format("Failed to unzip file %s to dest %s.", + // file.getAbsolutePath(), dest.getAbsolutePath()), e, false); } + return false; } /** @@ -757,6 +759,10 @@ public static void moveFile(File src, File dst) throws IOException { if(src.getIOFile().renameTo(dst.getIOFile())){ return; } + copyFile(src, dst); + src.delete(); + } + public static void copyFile(File src, File dst) throws IOException { FileInputStream instream = new FileInputStream(src.getIOFile()); FileOutputStream outstream = new FileOutputStream(dst.getIOFile()); FileChannel inChannel = instream.getChannel(); @@ -780,7 +786,6 @@ public static void moveFile(File src, File dst) throws IOException { if (outChannel != null) outChannel.close(); } - src.delete(); } /** diff --git a/src/itdelatrisu/opsu/beatmap/BeatmapSet.java b/src/itdelatrisu/opsu/beatmap/BeatmapSet.java index 365c9983..12291de1 100644 --- a/src/itdelatrisu/opsu/beatmap/BeatmapSet.java +++ b/src/itdelatrisu/opsu/beatmap/BeatmapSet.java @@ -131,6 +131,7 @@ public boolean matches(String query) { beatmap.creator.toLowerCase().contains(query) || beatmap.source.toLowerCase().contains(query) || beatmap.version.toLowerCase().contains(query) || + Integer.toString(beatmap.beatmapSetID).contains(query) || beatmap.tags.contains(query)) return true; diff --git a/src/itdelatrisu/opsu/beatmap/OszUnpacker.java b/src/itdelatrisu/opsu/beatmap/OszUnpacker.java index 09bce7ad..75f0c930 100644 --- a/src/itdelatrisu/opsu/beatmap/OszUnpacker.java +++ b/src/itdelatrisu/opsu/beatmap/OszUnpacker.java @@ -76,9 +76,12 @@ public boolean accept(java.io.File dir, String name) { File songDir = new File(dest, dirName); if (!songDir.isDirectory()) { songDir.mkdir(); - Utils.unzip(file, songDir); - file.delete(); // delete the OSZ when finished - dirs.add(songDir); + if (Utils.unzip(file, songDir)) { + file.delete(); // delete the OSZ when finished + dirs.add(songDir); + } else { + songDir.delete(); + } } } if (ws != null) diff --git a/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java b/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java index 80b2ca6c..8a77b5ab 100644 --- a/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java +++ b/src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java @@ -74,7 +74,7 @@ public DownloadNode[] resultList(String query, int page, boolean rankedOnly) thr try { // read HTML String queryString = (query.isEmpty()) ? "-" : query; - String search = String.format(SEARCH_URL, URLEncoder.encode(queryString, "UTF-8")); + String search = String.format(SEARCH_URL, URLEncoder.encode(queryString, "UTF-8").replace("+", "%20")); String html = Utils.readDataFromUrl(new URL(search)); if (html == null) { this.totalResults = -1; diff --git a/src/itdelatrisu/opsu/options/Options.java b/src/itdelatrisu/opsu/options/Options.java index f489bfce..ba6a1c53 100644 --- a/src/itdelatrisu/opsu/options/Options.java +++ b/src/itdelatrisu/opsu/options/Options.java @@ -533,7 +533,7 @@ public void setValue(int value) { }, EFFECT_VOLUME ("Effects", "VolumeEffect", "Menu and game sound effects volume.", 70, 0, 100), HITSOUND_VOLUME ("Hit sounds", "VolumeHitSound", "Hit sounds volume.", 30, 0, 100), - MUSIC_OFFSET ("Universal offset", "Offset", "Adjust this value if hit objects are out of sync.", -75, -500, 500) { + MUSIC_OFFSET ("Universal offset", "Offset", "Adjust this value if hit objects are out of sync.", -200, -500, 500) { @Override public String getValueString() { return String.format("%dms", val); } }, diff --git a/src/itdelatrisu/opsu/options/OptionsOverlay.java b/src/itdelatrisu/opsu/options/OptionsOverlay.java index d955cb21..35e3f273 100644 --- a/src/itdelatrisu/opsu/options/OptionsOverlay.java +++ b/src/itdelatrisu/opsu/options/OptionsOverlay.java @@ -1403,6 +1403,8 @@ public void keyType(char c){ * @param mouseY the mouse y coordinate */ private void adjustSlider(int mouseX, int mouseY) { + if (hoverOption == null) + return; int oldSliderValue = hoverOption.getIntegerValue(); // set new value diff --git a/src/itdelatrisu/opsu/replay/Replay.java b/src/itdelatrisu/opsu/replay/Replay.java index fd2018e1..e6251b42 100644 --- a/src/itdelatrisu/opsu/replay/Replay.java +++ b/src/itdelatrisu/opsu/replay/Replay.java @@ -34,14 +34,18 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; //import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.text.NumberFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Locale; + //import org.newdawn.slick.util.Log; import org.tukaani.xz.LZMA2Options; @@ -327,7 +331,9 @@ public void run() { // LZMA-encoded replay data if (frames != null && frames.length > 0) { // build full frame string - NumberFormat nf = new DecimalFormat("###.#####"); + NumberFormat nf = new DecimalFormat("###.#####" + ,DecimalFormatSymbols.getInstance(Locale.forLanguageTag("en_US")) + ); sb = new StringBuilder(); for (int i = 0; i < frames.length; i++) { ReplayFrame frame = frames[i]; diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index 55d951c7..b3cfed43 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -291,6 +291,11 @@ public void run() { private void importBeatmaps() { // invoke unpacker and parser File[] dirs = OszUnpacker.unpackAllFiles(Options.getImportDir(), Options.getBeatmapDir()); + if (dirs == null || dirs.length <=0) { + File dlDir = fluddokt.ex.DeviceInfo.info.getDownloadDir(); + if (dlDir != null) + dirs = OszUnpacker.unpackAllFiles(dlDir, Options.getBeatmapDir()); + } if (dirs != null && dirs.length > 0) { this.importedNode = BeatmapParser.parseDirectories(dirs); if (importedNode == null) diff --git a/src/itdelatrisu/opsu/states/Game.java b/src/itdelatrisu/opsu/states/Game.java index e2c4e21e..40f2b0a2 100644 --- a/src/itdelatrisu/opsu/states/Game.java +++ b/src/itdelatrisu/opsu/states/Game.java @@ -272,10 +272,10 @@ public enum PlayState { private LinkedList lifeFrames; /** The offscreen image rendered to. */ - private Image offscreen; + //private Image offscreen; /** The offscreen graphics. */ - private Graphics gOffscreen; + //private Graphics gOffscreen; /** The current flashlight area radius. */ private int flashlightRadius; @@ -576,7 +576,7 @@ else if (breakIndex > 1) { } if (GameMod.FLASHLIGHT.isActive()) { - drawHitObjects(gOffscreen, trackPos); + drawHitObjects(g, trackPos); GameImage.ALPHA_MAP.getImage().draw(alphaX, alphaY, alphaRadius, alphaRadius); g.clearClip(); diff --git a/src/itdelatrisu/opsu/ui/KineticScrolling.java b/src/itdelatrisu/opsu/ui/KineticScrolling.java index 915d383b..0b251189 100644 --- a/src/itdelatrisu/opsu/ui/KineticScrolling.java +++ b/src/itdelatrisu/opsu/ui/KineticScrolling.java @@ -111,8 +111,13 @@ public void update(float delta) { totalDelta += delta; position = target + (float) (-amplitude * Math.exp(-totalDelta / (TIME_CONST / speedMultiplier))); } else { - avgVelocity = (ONE_MINUS_AVG_CONST * avgVelocity + AVG_CONST * (deltaPosition * 1000f / delta)); - + //avgVelocity = (ONE_MINUS_AVG_CONST * avgVelocity + AVG_CONST * (deltaPosition * 1000f / delta)); + if (delta>60) + delta = 60; + float delta2 = delta/60f; + float minusdelta2 = 1 - delta2; + avgVelocity = (minusdelta2 * avgVelocity + delta2 * (deltaPosition * 1000f / delta)); + position += deltaPosition; target = position; deltaPosition = 0; @@ -185,6 +190,7 @@ public void pressed() { return; pressed = true; avgVelocity = 0; + deltaPosition = 0; } /** diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index 6007fa7b..39a2ceea 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -386,7 +386,7 @@ public static void drawTooltip(Graphics g) { return; int containerWidth = container.getWidth(), containerHeight = container.getHeight(); - int margin = containerWidth / 100, textMarginX = 2; + int margin = containerHeight / 100, textMarginX = 2; int offset = GameImage.CURSOR_MIDDLE.getImage().getWidth() / 2; int lineHeight = Fonts.SMALL.getLineHeight(); int textWidth = textMarginX * 2, textHeight = lineHeight; @@ -404,7 +404,7 @@ public static void drawTooltip(Graphics g) { textWidth += Fonts.SMALL.getWidth(tooltip); // get drawing coordinates - int x = input.getMouseX() + offset, y = input.getMouseY() + offset; + int x = input.getMouseX() + offset - textWidth/2, y = input.getMouseY() - offset - textHeight - margin*5; if (x + textWidth > containerWidth - margin) x = containerWidth - margin - textWidth; else if (x < margin)