Initial
This commit is contained in:
commit
c49fa2800f
53
build.xml
Normal file
53
build.xml
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!-- You may freely edit this file. See commented blocks below for --><!-- some examples of how to customize the build. --><!-- (If you delete it and reopen the project it will be recreated.) --><!-- By default, only the Clean and Build commands use this build script. --><project name="OsuReplayDiff" default="default" basedir="." xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<description>Builds, tests, and runs the project OsuReplayDiff.</description>
|
||||
<import file="nbproject/build-impl.xml"/>
|
||||
<!--
|
||||
|
||||
There exist several targets which are by default empty and which can be
|
||||
used for execution of your tasks. These targets are usually executed
|
||||
before and after some main targets. Those of them relevant for JavaFX project are:
|
||||
|
||||
-pre-init: called before initialization of project properties
|
||||
-post-init: called after initialization of project properties
|
||||
-pre-compile: called before javac compilation
|
||||
-post-compile: called after javac compilation
|
||||
-pre-compile-test: called before javac compilation of JUnit tests
|
||||
-post-compile-test: called after javac compilation of JUnit tests
|
||||
-pre-jfx-jar: called before FX SDK specific <fx:jar> task
|
||||
-post-jfx-jar: called after FX SDK specific <fx:jar> task
|
||||
-pre-jfx-deploy: called before FX SDK specific <fx:deploy> task
|
||||
-post-jfx-deploy: called after FX SDK specific <fx:deploy> task
|
||||
-pre-jfx-native: called just after -pre-jfx-deploy if <fx:deploy> runs in native packaging mode
|
||||
-post-jfx-native: called just after -post-jfx-deploy if <fx:deploy> runs in native packaging mode
|
||||
-post-clean: called after cleaning build products
|
||||
|
||||
(Targets beginning with '-' are not intended to be called on their own.)
|
||||
|
||||
Example of inserting a HTML postprocessor after javaFX SDK deployment:
|
||||
|
||||
<target name="-post-jfx-deploy">
|
||||
<basename property="jfx.deployment.base" file="${jfx.deployment.jar}" suffix=".jar"/>
|
||||
<property name="jfx.deployment.html" location="${jfx.deployment.dir}${file.separator}${jfx.deployment.base}.html"/>
|
||||
<custompostprocess>
|
||||
<fileset dir="${jfx.deployment.html}"/>
|
||||
</custompostprocess>
|
||||
</target>
|
||||
|
||||
Example of calling an Ant task from JavaFX SDK. Note that access to JavaFX SDK Ant tasks must be
|
||||
initialized; to ensure this is done add the dependence on -check-jfx-sdk-version target:
|
||||
|
||||
<target name="-post-jfx-jar" depends="-check-jfx-sdk-version">
|
||||
<echo message="Calling jar task from JavaFX SDK"/>
|
||||
<fx:jar ...>
|
||||
...
|
||||
</fx:jar>
|
||||
</target>
|
||||
|
||||
For more details about JavaFX SDK Ant tasks go to
|
||||
http://docs.oracle.com/javafx/2/deployment/jfxpub-deployment.htm
|
||||
|
||||
For list of available properties check the files
|
||||
nbproject/build-impl.xml and nbproject/jfx-impl.xml.
|
||||
|
||||
-->
|
||||
</project>
|
1420
nbproject/build-impl.xml
Normal file
1420
nbproject/build-impl.xml
Normal file
File diff suppressed because it is too large
Load Diff
2
nbproject/configs/Run_as_WebStart.properties
Normal file
2
nbproject/configs/Run_as_WebStart.properties
Normal file
@ -0,0 +1,2 @@
|
||||
# Do not modify this property in this configuration. It can be re-generated.
|
||||
$label=Run as WebStart
|
2
nbproject/configs/Run_in_Browser.properties
Normal file
2
nbproject/configs/Run_in_Browser.properties
Normal file
@ -0,0 +1,2 @@
|
||||
# Do not modify this property in this configuration. It can be re-generated.
|
||||
$label=Run in Browser
|
8
nbproject/genfiles.properties
Normal file
8
nbproject/genfiles.properties
Normal file
@ -0,0 +1,8 @@
|
||||
build.xml.data.CRC32=d513bdc6
|
||||
build.xml.script.CRC32=94a4c251
|
||||
build.xml.stylesheet.CRC32=8064a381@1.78.0.48
|
||||
# This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
|
||||
# Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
|
||||
nbproject/build-impl.xml.data.CRC32=d513bdc6
|
||||
nbproject/build-impl.xml.script.CRC32=f86c884f
|
||||
nbproject/build-impl.xml.stylesheet.CRC32=05530350@1.79.0.48
|
4007
nbproject/jfx-impl.xml
Normal file
4007
nbproject/jfx-impl.xml
Normal file
File diff suppressed because it is too large
Load Diff
2
nbproject/private/configs/Run_as_WebStart.properties
Normal file
2
nbproject/private/configs/Run_as_WebStart.properties
Normal file
@ -0,0 +1,2 @@
|
||||
# Do not modify this property in this configuration. It can be re-generated.
|
||||
javafx.run.as=webstart
|
2
nbproject/private/configs/Run_in_Browser.properties
Normal file
2
nbproject/private/configs/Run_in_Browser.properties
Normal file
@ -0,0 +1,2 @@
|
||||
# Do not modify this property in this configuration. It can be re-generated.
|
||||
javafx.run.as=embedded
|
11
nbproject/private/private.properties
Normal file
11
nbproject/private/private.properties
Normal file
@ -0,0 +1,11 @@
|
||||
auxiliary.org-netbeans-modules-projectapi.issue214819_5f_fx_5f_enabled=true
|
||||
compile.on.save=true
|
||||
do.depend=false
|
||||
do.jar=true
|
||||
# No need to modify this property unless customizing JavaFX Ant task infrastructure
|
||||
endorsed.javafx.ant.classpath=.
|
||||
javac.debug=true
|
||||
javadoc.preview=true
|
||||
javafx.run.inbrowser=<Default System Browser>
|
||||
javafx.run.inbrowser.path=C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe
|
||||
user.properties.file=C:\\Users\\aNNiMON\\AppData\\Roaming\\NetBeans\\dev\\build.properties
|
7
nbproject/private/private.xml
Normal file
7
nbproject/private/private.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project-private xmlns="http://www.netbeans.org/ns/project-private/1">
|
||||
<editor-bookmarks xmlns="http://www.netbeans.org/ns/editor-bookmarks/2" lastBookmarkId="0"/>
|
||||
<open-files xmlns="http://www.netbeans.org/ns/projectui-open-files/2">
|
||||
<group/>
|
||||
</open-files>
|
||||
</project-private>
|
4
nbproject/private/retriever/catalog.xml
Normal file
4
nbproject/private/retriever/catalog.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog" prefer="system">
|
||||
<system systemId="http://javafx.com/javafx/8" uri="www.oracle.com/technetwork/java/javase/overview/index.html"/>
|
||||
</catalog>
|
File diff suppressed because one or more lines are too long
118
nbproject/project.properties
Normal file
118
nbproject/project.properties
Normal file
@ -0,0 +1,118 @@
|
||||
annotation.processing.enabled=true
|
||||
annotation.processing.enabled.in.editor=false
|
||||
annotation.processing.processors.list=
|
||||
annotation.processing.run.all.processors=true
|
||||
annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output
|
||||
application.title=OsuReplayDiff
|
||||
application.vendor=aNNiMON
|
||||
build.classes.dir=${build.dir}/classes
|
||||
build.classes.excludes=**/*.java,**/*.form
|
||||
# This directory is removed when the project is cleaned:
|
||||
build.dir=build
|
||||
build.generated.dir=${build.dir}/generated
|
||||
build.generated.sources.dir=${build.dir}/generated-sources
|
||||
# Only compile against the classpath explicitly listed here:
|
||||
build.sysclasspath=ignore
|
||||
build.test.classes.dir=${build.dir}/test/classes
|
||||
build.test.results.dir=${build.dir}/test/results
|
||||
compile.on.save=true
|
||||
compile.on.save.unsupported.javafx=true
|
||||
# Uncomment to specify the preferred debugger connection transport:
|
||||
#debug.transport=dt_socket
|
||||
debug.classpath=\
|
||||
${run.classpath}
|
||||
debug.test.classpath=\
|
||||
${run.test.classpath}
|
||||
# This directory is removed when the project is cleaned:
|
||||
dist.dir=dist
|
||||
dist.jar=${dist.dir}/OsuReplayDiff.jar
|
||||
dist.javadoc.dir=${dist.dir}/javadoc
|
||||
endorsed.classpath=
|
||||
excludes=
|
||||
file.reference.commons-compress-1.9.jar=commons-compress-1.9.jar
|
||||
file.reference.xz-1.5.jar=xz-1.5.jar
|
||||
includes=**
|
||||
# Non-JavaFX jar file creation is deactivated in JavaFX 2.0+ projects
|
||||
jar.archive.disabled=true
|
||||
jar.compress=false
|
||||
javac.classpath=\
|
||||
${javafx.classpath.extension}:\
|
||||
${file.reference.commons-compress-1.9.jar}:\
|
||||
${file.reference.xz-1.5.jar}
|
||||
# Space-separated list of extra javac options
|
||||
javac.compilerargs=
|
||||
javac.deprecation=false
|
||||
javac.external.vm=false
|
||||
javac.processorpath=\
|
||||
${javac.classpath}
|
||||
javac.source=1.8
|
||||
javac.target=1.8
|
||||
javac.test.classpath=\
|
||||
${javac.classpath}:\
|
||||
${build.classes.dir}
|
||||
javac.test.processorpath=\
|
||||
${javac.test.classpath}
|
||||
javadoc.additionalparam=
|
||||
javadoc.author=false
|
||||
javadoc.encoding=${source.encoding}
|
||||
javadoc.noindex=false
|
||||
javadoc.nonavbar=false
|
||||
javadoc.notree=false
|
||||
javadoc.private=false
|
||||
javadoc.splitindex=true
|
||||
javadoc.use=true
|
||||
javadoc.version=false
|
||||
javadoc.windowtitle=
|
||||
javafx.application.implementation.version=1.0
|
||||
javafx.binarycss=false
|
||||
javafx.classpath.extension=\
|
||||
${java.home}/lib/javaws.jar:\
|
||||
${java.home}/lib/deploy.jar:\
|
||||
${java.home}/lib/plugin.jar
|
||||
javafx.deploy.allowoffline=true
|
||||
# If true, application update mode is set to 'background', if false, update mode is set to 'eager'
|
||||
javafx.deploy.backgroundupdate=false
|
||||
javafx.deploy.embedJNLP=true
|
||||
javafx.deploy.includeDT=true
|
||||
# Set true to prevent creation of temporary copy of deployment artifacts before each run (disables concurrent runs)
|
||||
javafx.disable.concurrent.runs=false
|
||||
# Set true to enable multiple concurrent runs of the same WebStart or Run-in-Browser project
|
||||
javafx.enable.concurrent.external.runs=false
|
||||
# This is a JavaFX project
|
||||
javafx.enabled=true
|
||||
javafx.fallback.class=com.javafx.main.NoJavaFXFallback
|
||||
# Main class for JavaFX
|
||||
javafx.main.class=com.annimon.osureplaydiff.Main
|
||||
javafx.preloader.class=
|
||||
# This project does not use Preloader
|
||||
javafx.preloader.enabled=false
|
||||
javafx.preloader.jar.filename=
|
||||
javafx.preloader.jar.path=
|
||||
javafx.preloader.project.path=
|
||||
javafx.preloader.type=none
|
||||
# Set true for GlassFish only. Rebases manifest classpaths of JARs in lib dir. Not usable with signed JARs.
|
||||
javafx.rebase.libs=false
|
||||
javafx.run.height=600
|
||||
javafx.run.width=800
|
||||
# Pre-JavaFX 2.0 WebStart is deactivated in JavaFX 2.0+ projects
|
||||
jnlp.enabled=false
|
||||
# Main class for Java launcher
|
||||
main.class=com.javafx.main.Main
|
||||
# For improved security specify narrower Codebase manifest attribute to prevent RIAs from being repurposed
|
||||
manifest.custom.codebase=*
|
||||
# Specify Permissions manifest attribute to override default (choices: sandbox, all-permissions)
|
||||
manifest.custom.permissions=
|
||||
manifest.file=manifest.mf
|
||||
meta.inf.dir=${src.dir}/META-INF
|
||||
mkdist.disabled=false
|
||||
platform.active=default_platform
|
||||
run.classpath=\
|
||||
${dist.jar}:\
|
||||
${javac.classpath}:\
|
||||
${build.classes.dir}
|
||||
run.test.classpath=\
|
||||
${javac.test.classpath}:\
|
||||
${build.test.classes.dir}
|
||||
source.encoding=UTF-8
|
||||
src.dir=src
|
||||
test.src.dir=test
|
25
nbproject/project.xml
Normal file
25
nbproject/project.xml
Normal file
@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://www.netbeans.org/ns/project/1">
|
||||
<type>org.netbeans.modules.java.j2seproject</type>
|
||||
<configuration>
|
||||
<buildExtensions xmlns="http://www.netbeans.org/ns/ant-build-extender/1">
|
||||
<extension file="jfx-impl.xml" id="jfx3">
|
||||
<dependency dependsOn="-jfx-copylibs" target="-post-jar"/>
|
||||
<dependency dependsOn="-rebase-libs" target="-post-jar"/>
|
||||
<dependency dependsOn="jfx-deployment" target="-post-jar"/>
|
||||
<dependency dependsOn="jar" target="debug"/>
|
||||
<dependency dependsOn="jar" target="profile"/>
|
||||
<dependency dependsOn="jar" target="run"/>
|
||||
</extension>
|
||||
</buildExtensions>
|
||||
<data xmlns="http://www.netbeans.org/ns/j2se-project/3">
|
||||
<name>OsuReplayDiff</name>
|
||||
<source-roots>
|
||||
<root id="src.dir"/>
|
||||
</source-roots>
|
||||
<test-roots>
|
||||
<root id="test.src.dir"/>
|
||||
</test-roots>
|
||||
</data>
|
||||
</configuration>
|
||||
</project>
|
237
src/com/annimon/osureplaydiff/FXMLDocumentController.java
Normal file
237
src/com/annimon/osureplaydiff/FXMLDocumentController.java
Normal file
@ -0,0 +1,237 @@
|
||||
/*
|
||||
*/
|
||||
package com.annimon.osureplaydiff;
|
||||
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
import itdelatrisu.opsu.replay.Replay;
|
||||
import itdelatrisu.opsu.replay.ReplayFrame;
|
||||
import itdelatrisu.opsu.skins.Skin;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author aNNiMON
|
||||
*/
|
||||
public class FXMLDocumentController implements Initializable {
|
||||
|
||||
private static final int WIDTH = 512, HEIGHT = 384;
|
||||
|
||||
@FXML
|
||||
private Slider slider;
|
||||
|
||||
@FXML
|
||||
private Canvas canvas;
|
||||
|
||||
private GraphicsContext gc;
|
||||
private Beatmap beatmap;
|
||||
private Skin skin;
|
||||
|
||||
private int[][] indices;
|
||||
private HitObject[] hits;
|
||||
private ReplayFrame[] framesA, framesB;
|
||||
|
||||
@FXML
|
||||
private void handleButtonAction(ActionEvent event) {
|
||||
final String songDir = "D:\\GAMES\\osu\\Songs\\332340 Loz Contreras - Sarajevo (Blackmill Remix)";
|
||||
beatmap = BeatmapParser.parseFile(
|
||||
new File(songDir + "\\Loz Contreras - Sarajevo (Blackmill Remix) (aNNiMON) [Hard].osu"),
|
||||
new File(songDir), true);
|
||||
|
||||
skin = Options.getSkin();
|
||||
|
||||
|
||||
Replay replayA = new Replay(new File("E://MierivaL - Loz Contreras - Sarajevo (Blackmill Remix) [Hard] (2015-07-09) Osu.osr"));
|
||||
Replay replayB = new Replay(new File("E://aNNiMON - Loz Contreras - Sarajevo (Blackmill Remix) [Hard] (2015-07-10) Osu.osr"));
|
||||
try {
|
||||
replayA.load();
|
||||
replayB.load();
|
||||
//System.out.println(replay.toString());
|
||||
} catch (IOException ex) { }
|
||||
|
||||
final HitObject[] hits = beatmap.objects;
|
||||
final ReplayFrame[] framesA = replayA.frames;
|
||||
final ReplayFrame[] framesB = replayB.frames;
|
||||
Arrays.sort(framesA, (a, b) -> Integer.compare(a.getTime(), b.getTime()));
|
||||
Arrays.sort(framesB, (a, b) -> Integer.compare(a.getTime(), b.getTime()));
|
||||
|
||||
indices = new int[3][beatmap.endTime];
|
||||
int hitIndex = 0;
|
||||
int frameIndexA1 = 0;
|
||||
int frameIndexB1 = 0;
|
||||
final int HIT_TIME = 200;
|
||||
while (hits[hitIndex].getTime() < HIT_TIME) frameIndexA1++;
|
||||
while (framesA[frameIndexA1].getTime() < 0) frameIndexA1++;
|
||||
while (framesB[frameIndexB1].getTime() < 0) frameIndexB1++;
|
||||
for (int time = 0; time < beatmap.endTime; time++) {
|
||||
if ( (hitIndex < hits.length) && (time == hits[hitIndex].getTime() - HIT_TIME) ) hitIndex++;
|
||||
|
||||
if (frameIndexA1 < framesA.length - 1) {
|
||||
if (time == framesA[frameIndexA1].getTime()) {
|
||||
frameIndexA1++;
|
||||
while ( (frameIndexA1 < framesA.length) &&
|
||||
(framesA[frameIndexA1 - 1].getTime() == framesA[frameIndexA1].getTime()) ) frameIndexA1++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (frameIndexB1 < framesB.length - 1) {
|
||||
if (time == framesB[frameIndexB1].getTime()) {
|
||||
frameIndexB1++;
|
||||
while ( (frameIndexB1 < framesB.length - 1) &&
|
||||
(framesB[frameIndexB1 - 1].getTime() == framesB[frameIndexB1].getTime()) ) frameIndexB1++;
|
||||
}
|
||||
}
|
||||
|
||||
indices[0][time] = hitIndex - 1;
|
||||
indices[1][time] = frameIndexA1;
|
||||
indices[2][time] = frameIndexB1;
|
||||
}
|
||||
|
||||
this.hits = hits;
|
||||
this.framesA = framesA;
|
||||
this.framesB = framesB;
|
||||
|
||||
final int DIV = 1;
|
||||
slider.setMax(beatmap.endTime / DIV);
|
||||
// slider.setMax(frames[frames.length - 1].getTime());
|
||||
slider.valueProperty().addListener((ov, oldValue, newValue) -> {
|
||||
final int time = newValue.intValue();
|
||||
draw(time * DIV);
|
||||
});
|
||||
draw(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
gc = canvas.getGraphicsContext2D();
|
||||
}
|
||||
|
||||
private void draw(int time) {
|
||||
final int hitIndex = indices[0][time], indexA = indices[1][time], indexB = indices[2][time];
|
||||
final HitObject hit = (hitIndex == -1) ? null : hits[hitIndex];
|
||||
final ReplayFrame frameA1 = framesA[indexA];
|
||||
final ReplayFrame frameA2 = (indexA == framesA.length - 1) ? framesA[indexA] : framesA[indexA + 1];
|
||||
final ReplayFrame frameB1 = framesB[indexB];
|
||||
final ReplayFrame frameB2 = (indexB == framesB.length - 1) ? framesB[indexB] : framesB[indexB + 1];
|
||||
|
||||
final int playerSize = 10, hitSize = 25;
|
||||
gc.setFill(Color.WHITE);
|
||||
gc.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
if (hit != null) {
|
||||
gc.setStroke(skin.getComboColors()[hit.getComboIndex()]);
|
||||
gc.setFill(skin.getComboColors()[hit.getComboIndex()]);
|
||||
if (hit.isCircle()) {
|
||||
circle(hit.getX(), hit.getY(), hitSize);
|
||||
} else if (hit.isSpinner()) {
|
||||
circle(WIDTH / 2, HEIGHT / 2, HEIGHT / 2);
|
||||
} else if (hit.isSlider()) {
|
||||
float[] x = hit.getSliderX();
|
||||
float[] y = hit.getSliderY();
|
||||
final int length = x.length;
|
||||
gc.setLineWidth(hitSize);
|
||||
|
||||
gc.beginPath();
|
||||
gc.moveTo(hit.getX(), hit.getY());
|
||||
for (int i = 1; i < length - 1; i+=2) {
|
||||
gc.bezierCurveTo(x[i-1], y[i-1], x[i], y[i], x[i+1], y[i+1]);
|
||||
}
|
||||
gc.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
final float ax = (frameA1.getX() + frameA2.getX()) / 2;
|
||||
final float ay = (frameA1.getY() + frameA2.getY()) / 2;
|
||||
final float bx = (frameB1.getX() + frameB2.getX()) / 2;
|
||||
final float by = (frameB1.getY() + frameB2.getY()) / 2;
|
||||
gc.setFill(Color.BLUE);
|
||||
circle(ax, ay, playerSize);
|
||||
gc.setFill(Color.GREEN);
|
||||
circle(bx, by, playerSize);
|
||||
|
||||
gc.setStroke(Color.BLACK);
|
||||
gc.setLineWidth(1);
|
||||
if (frameA1.isKeyPressed()) {
|
||||
gc.strokeText(frameA1.keyAsString(), frameA1.getX(), frameA1.getY());
|
||||
}
|
||||
if (frameB1.isKeyPressed()) {
|
||||
gc.strokeText(frameB1.keyAsString(), frameB1.getX(), frameB1.getY());
|
||||
}
|
||||
}
|
||||
|
||||
private void circle(double cx, double cy, double size) {
|
||||
final double rad = size / 2;
|
||||
gc.fillOval(cx - rad, cy - rad, size, size);
|
||||
}
|
||||
|
||||
/*private ReplayFrame findByTime(int time, ReplayFrame[] frames) {
|
||||
final int length = frames.length;
|
||||
|
||||
int indexA = 0, indexB = length - 1;
|
||||
while (indexA < indexB) {
|
||||
final int midIndex = indexA + (indexB - indexA) / 2;
|
||||
final ReplayFrame midFrame = frames[midIndex];
|
||||
final int midTime = midFrame.getTime();
|
||||
if (time <= midTime) indexB = midIndex;
|
||||
else indexA = midIndex + 1;
|
||||
}
|
||||
|
||||
if (indexB >= length - 1) return frames[indexB];
|
||||
|
||||
final ReplayFrame frameA = frames[indexB];
|
||||
final ReplayFrame frameB = frames[indexB + 1];
|
||||
|
||||
final int timeDiff = (frameA.getTimeDiff() + frameB.getTimeDiff()) / 2;
|
||||
final float x = (frameA.getX() + frameB.getX()) / 2;
|
||||
final float y = (frameA.getY() + frameB.getY()) / 2;
|
||||
final int keys = frameA.getKeys();
|
||||
return new ReplayFrame(timeDiff, time, x, y, keys);
|
||||
}
|
||||
|
||||
private HitObject findHitObjectByTime(int time) {
|
||||
final int length = beatmap.objects.length;
|
||||
|
||||
int indexA = 0, indexB = length - 1;
|
||||
while (indexA < indexB) {
|
||||
final int midIndex = indexA + (indexB - indexA) / 2;
|
||||
final HitObject midObject = beatmap.objects[midIndex];
|
||||
if (midObject == null) {
|
||||
int index = midIndex;
|
||||
while (index > 0 && beatmap.objects[index] == null) {
|
||||
index--;
|
||||
}
|
||||
return beatmap.objects[index];
|
||||
}
|
||||
if (midObject.isCircle()) {
|
||||
if (time <= midObject.getTime()) indexB = midIndex;
|
||||
else indexA = midIndex + 1;
|
||||
} else {
|
||||
if (time < midObject.getTime()) indexB = midIndex;
|
||||
else if (time > midObject.getEndTime()) indexA = midIndex + 1;
|
||||
else {
|
||||
indexB = midIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return beatmap.objects[indexB];
|
||||
}*/
|
||||
}
|
32
src/com/annimon/osureplaydiff/Main.java
Normal file
32
src/com/annimon/osureplaydiff/Main.java
Normal file
@ -0,0 +1,32 @@
|
||||
package com.annimon.osureplaydiff;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author aNNiMON
|
||||
*/
|
||||
public class Main extends Application {
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) throws Exception {
|
||||
Parent root = FXMLLoader.load(getClass().getResource("main.fxml"));
|
||||
|
||||
Scene scene = new Scene(root);
|
||||
|
||||
stage.setScene(scene);
|
||||
stage.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param args the command line arguments
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
launch(args);
|
||||
}
|
||||
|
||||
}
|
26
src/com/annimon/osureplaydiff/main.fxml
Normal file
26
src/com/annimon/osureplaydiff/main.fxml
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.canvas.*?>
|
||||
<?import java.lang.*?>
|
||||
<?import java.util.*?>
|
||||
<?import javafx.scene.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
|
||||
<BorderPane prefHeight="243.0" prefWidth="423.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.annimon.osureplaydiff.FXMLDocumentController">
|
||||
<top>
|
||||
<Button fx:id="button" onAction="#handleButtonAction" text="Click Me!" BorderPane.alignment="CENTER" />
|
||||
</top>
|
||||
<bottom>
|
||||
<Slider fx:id="slider" BorderPane.alignment="CENTER">
|
||||
<BorderPane.margin>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</BorderPane.margin>
|
||||
</Slider>
|
||||
</bottom>
|
||||
<center>
|
||||
<Canvas fx:id="canvas" width="512" height="384" BorderPane.alignment="CENTER" />
|
||||
</center>
|
||||
</BorderPane>
|
28
src/itdelatrisu/opsu/Log.java
Normal file
28
src/itdelatrisu/opsu/Log.java
Normal file
@ -0,0 +1,28 @@
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author aNNiMON
|
||||
*/
|
||||
public final class Log {
|
||||
|
||||
public static void error(String text, Exception e, boolean b) {
|
||||
System.err.println(text + " " + e.toString());
|
||||
}
|
||||
|
||||
public static void warn(String text, Exception e) {
|
||||
warn(text + " " + e.toString());
|
||||
}
|
||||
|
||||
public static void debug(String text) {
|
||||
System.err.println(text);
|
||||
}
|
||||
|
||||
public static void warn(String text) {
|
||||
System.err.println(text);
|
||||
}
|
||||
|
||||
public static void error(String text) {
|
||||
System.err.println(text);
|
||||
}
|
||||
}
|
17
src/itdelatrisu/opsu/Options.java
Normal file
17
src/itdelatrisu/opsu/Options.java
Normal file
@ -0,0 +1,17 @@
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.skins.Skin;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author aNNiMON
|
||||
*/
|
||||
public final class Options {
|
||||
|
||||
private static final Skin DEFAULT_SKIN = new Skin(null);
|
||||
|
||||
public static Skin getSkin() {
|
||||
return DEFAULT_SKIN;
|
||||
}
|
||||
|
||||
}
|
225
src/itdelatrisu/opsu/Utils.java
Normal file
225
src/itdelatrisu/opsu/Utils.java
Normal file
@ -0,0 +1,225 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import java.awt.Font;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
/**
|
||||
* Contains miscellaneous utilities.
|
||||
*/
|
||||
public class Utils {
|
||||
|
||||
// This class should not be instantiated.
|
||||
private Utils() {}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps a value between a lower and upper bound.
|
||||
* @param val the value to clamp
|
||||
* @param low the lower bound
|
||||
* @param high the upper bound
|
||||
* @return the clamped value
|
||||
* @author fluddokt
|
||||
*/
|
||||
public static float clamp(float val, float low, float high) {
|
||||
if (val < low)
|
||||
return low;
|
||||
if (val > high)
|
||||
return high;
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance between two points.
|
||||
* @param x1 the x-component of the first point
|
||||
* @param y1 the y-component of the first point
|
||||
* @param x2 the x-component of the second point
|
||||
* @param y2 the y-component of the second point
|
||||
* @return the Euclidean distance between points (x1,y1) and (x2,y2)
|
||||
*/
|
||||
public static float distance(float x1, float y1, float x2, float y2) {
|
||||
float v1 = Math.abs(x1 - x2);
|
||||
float v2 = Math.abs(y1 - y2);
|
||||
return (float) Math.sqrt((v1 * v1) + (v2 * v2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable representation of a given number of bytes.
|
||||
* @param bytes the number of bytes
|
||||
* @return the string representation
|
||||
* @author aioobe (http://stackoverflow.com/a/3758880)
|
||||
*/
|
||||
public static String bytesToString(long bytes) {
|
||||
if (bytes < 1024)
|
||||
return bytes + " B";
|
||||
int exp = (int) (Math.log(bytes) / Math.log(1024));
|
||||
char pre = "KMGTPE".charAt(exp - 1);
|
||||
return String.format("%.1f %cB", bytes / Math.pow(1024, exp), pre);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an input stream to a string.
|
||||
* @param is the input stream
|
||||
* @author Pavel Repin, earcam (http://stackoverflow.com/a/5445161)
|
||||
*/
|
||||
public static String convertStreamToString(InputStream is) {
|
||||
try (Scanner s = new Scanner(is)) {
|
||||
return s.useDelimiter("\\A").hasNext() ? s.next() : "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the md5 hash of a file in hex form.
|
||||
* @param file the file to hash
|
||||
* @return the md5 hash
|
||||
*/
|
||||
public static String getMD5(File file) {
|
||||
try {
|
||||
InputStream in = new BufferedInputStream(new FileInputStream(file));
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] buf = new byte[4096];
|
||||
|
||||
while (true) {
|
||||
int len = in.read(buf);
|
||||
if (len < 0)
|
||||
break;
|
||||
md.update(buf, 0, len);
|
||||
}
|
||||
in.close();
|
||||
|
||||
byte[] md5byte = md.digest();
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (byte b : md5byte)
|
||||
result.append(String.format("%02x", b));
|
||||
return result.toString();
|
||||
} catch (NoSuchAlgorithmException | IOException e) {
|
||||
//ErrorHandler.error("Failed to calculate MD5 hash.", e, true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted time string for a given number of seconds.
|
||||
* @param seconds the number of seconds
|
||||
* @return the time as a readable string
|
||||
*/
|
||||
public static String getTimeString(int seconds) {
|
||||
if (seconds < 60)
|
||||
return (seconds == 1) ? "1 second" : String.format("%d seconds", seconds);
|
||||
else if (seconds < 3600)
|
||||
return String.format("%02d:%02d", seconds / 60, seconds % 60);
|
||||
else
|
||||
return String.format("%02d:%02d:%02d", seconds / 3600, (seconds / 60) % 60, seconds % 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cubic ease out function.
|
||||
* @param t the current time
|
||||
* @param a the starting position
|
||||
* @param b the finishing position
|
||||
* @param d the duration
|
||||
* @return the eased float
|
||||
*/
|
||||
public static float easeOut(float t, float a, float b, float d) {
|
||||
return b * ((t = t / d - 1f) * t * t + 1f) + a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake bounce ease function.
|
||||
* @param t the current time
|
||||
* @param a the starting position
|
||||
* @param b the finishing position
|
||||
* @param d the duration
|
||||
* @return the eased float
|
||||
*/
|
||||
public static float easeBounce(float t, float a, float b, float d) {
|
||||
if (t < d / 2)
|
||||
return easeOut(t, a, b, d);
|
||||
return easeOut(d - t, a, b, d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the integer string argument as a boolean:
|
||||
* {@code 1} is {@code true}, and all other values are {@code false}.
|
||||
* @param s the {@code String} containing the boolean representation to be parsed
|
||||
* @return the boolean represented by the string argument
|
||||
*/
|
||||
public static boolean parseBoolean(String s) {
|
||||
return (Integer.parseInt(s) == 1);
|
||||
}
|
||||
}
|
446
src/itdelatrisu/opsu/beatmap/Beatmap.java
Normal file
446
src/itdelatrisu/opsu/beatmap/Beatmap.java
Normal file
@ -0,0 +1,446 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.beatmap;
|
||||
|
||||
import itdelatrisu.opsu.Options;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* Beatmap structure storing data parsed from OSU files.
|
||||
*/
|
||||
public class Beatmap implements Comparable<Beatmap> {
|
||||
/** Game modes. */
|
||||
public static final byte MODE_OSU = 0, MODE_TAIKO = 1, MODE_CTB = 2, MODE_MANIA = 3;
|
||||
|
||||
/** The OSU File object associated with this beatmap. */
|
||||
private File file;
|
||||
|
||||
/**
|
||||
* [General]
|
||||
*/
|
||||
|
||||
/** Audio file object. */
|
||||
public File audioFilename;
|
||||
|
||||
/** Delay time before music starts (in ms). */
|
||||
public int audioLeadIn = 0;
|
||||
|
||||
/** Audio hash (deprecated). */
|
||||
// public String audioHash = "";
|
||||
|
||||
/** Start position of music preview (in ms). */
|
||||
public int previewTime = -1;
|
||||
|
||||
/** Countdown type (0:disabled, 1:normal, 2:half, 3:double). */
|
||||
public byte countdown = 0;
|
||||
|
||||
/** Sound samples ("None", "Normal", "Soft"). */
|
||||
public String sampleSet = "";
|
||||
|
||||
/** How often closely placed hit objects will be stacked together. */
|
||||
public float stackLeniency = 0.7f;
|
||||
|
||||
/** Game mode (MODE_* constants). */
|
||||
public byte mode = MODE_OSU;
|
||||
|
||||
/** Whether the letterbox (top/bottom black bars) appears during breaks. */
|
||||
public boolean letterboxInBreaks = false;
|
||||
|
||||
/** Whether the storyboard should be widescreen. */
|
||||
public boolean widescreenStoryboard = false;
|
||||
|
||||
/** Whether to show an epilepsy warning. */
|
||||
public boolean epilepsyWarning = false;
|
||||
|
||||
/**
|
||||
* [Editor]
|
||||
*/
|
||||
|
||||
/** List of editor bookmarks (in ms). */
|
||||
// public int[] bookmarks;
|
||||
|
||||
/** Multiplier for "Distance Snap". */
|
||||
// public float distanceSpacing = 0f;
|
||||
|
||||
/** Beat division. */
|
||||
// public byte beatDivisor = 0;
|
||||
|
||||
/** Size of grid for "Grid Snap". */
|
||||
// public int gridSize = 0;
|
||||
|
||||
/** Zoom in the editor timeline. */
|
||||
// public int timelineZoom = 0;
|
||||
|
||||
/**
|
||||
* [Metadata]
|
||||
*/
|
||||
|
||||
/** Song title. */
|
||||
public String title = "", titleUnicode = "";
|
||||
|
||||
/** Song artist. */
|
||||
public String artist = "", artistUnicode = "";
|
||||
|
||||
/** Beatmap creator. */
|
||||
public String creator = "";
|
||||
|
||||
/** Beatmap difficulty. */
|
||||
public String version = "";
|
||||
|
||||
/** Song source. */
|
||||
public String source = "";
|
||||
|
||||
/** Song tags (for searching). */
|
||||
public String tags = "";
|
||||
|
||||
/** Beatmap ID. */
|
||||
public int beatmapID = 0;
|
||||
|
||||
/** Beatmap set ID. */
|
||||
public int beatmapSetID = 0;
|
||||
|
||||
/**
|
||||
* [Difficulty]
|
||||
*/
|
||||
|
||||
/** HP: Health drain rate (0:easy ~ 10:hard) */
|
||||
public float HPDrainRate = 5f;
|
||||
|
||||
/** CS: Size of circles and sliders (0:large ~ 10:small). */
|
||||
public float circleSize = 4f;
|
||||
|
||||
/** OD: Affects timing window, spinners, and approach speed (0:easy ~ 10:hard). */
|
||||
public float overallDifficulty = 5f;
|
||||
|
||||
/** AR: How long circles stay on the screen (0:long ~ 10:short). */
|
||||
public float approachRate = -1f;
|
||||
|
||||
/** Slider movement speed multiplier. */
|
||||
public float sliderMultiplier = 1f;
|
||||
|
||||
/** Rate at which slider ticks are placed (x per beat). */
|
||||
public float sliderTickRate = 1f;
|
||||
|
||||
/**
|
||||
* [Events]
|
||||
*/
|
||||
|
||||
/** Background image file. */
|
||||
public File bg;
|
||||
|
||||
/** Background video file. */
|
||||
// public File video;
|
||||
|
||||
/** All break periods (start time, end time, ...). */
|
||||
public ArrayList<Integer> breaks;
|
||||
|
||||
/**
|
||||
* [TimingPoints]
|
||||
*/
|
||||
|
||||
/** All timing points. */
|
||||
public ArrayList<TimingPoint> timingPoints;
|
||||
|
||||
/** Song BPM range. */
|
||||
public int bpmMin = 0, bpmMax = 0;
|
||||
|
||||
/**
|
||||
* [Colours]
|
||||
*/
|
||||
|
||||
/** Combo colors (max 8). If null, the skin value is used. */
|
||||
public Color[] combo;
|
||||
|
||||
/** Slider border color. If null, the skin value is used. */
|
||||
public Color sliderBorder;
|
||||
|
||||
/** MD5 hash of this file. */
|
||||
public String md5Hash;
|
||||
|
||||
/**
|
||||
* [HitObjects]
|
||||
*/
|
||||
|
||||
/** All hit objects. */
|
||||
public HitObject[] objects;
|
||||
|
||||
/** Number of individual objects. */
|
||||
public int
|
||||
hitObjectCircle = 0,
|
||||
hitObjectSlider = 0,
|
||||
hitObjectSpinner = 0;
|
||||
|
||||
/** Last object end time (in ms). */
|
||||
public int endTime = -1;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param file the file associated with this beatmap
|
||||
*/
|
||||
public Beatmap(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the associated file object.
|
||||
* @return the File object
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of combo colors (max 8).
|
||||
* If the beatmap does not provide colors, the skin colors will be returned instead.
|
||||
* @return the combo colors
|
||||
*/
|
||||
public Color[] getComboColors() {
|
||||
return (combo != null) ? combo : Options.getSkin().getComboColors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slider border color.
|
||||
* If the beatmap does not provide a color, the skin color will be returned instead.
|
||||
* @return the slider border color
|
||||
*/
|
||||
public Color getSliderBorderColor() {
|
||||
return (sliderBorder != null) ? sliderBorder : Options.getSkin().getSliderBorderColor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the beatmap background.
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
* @param alpha the alpha value
|
||||
* @param stretch if true, stretch to screen dimensions; otherwise, maintain aspect ratio
|
||||
* @return true if successful, false if any errors were produced
|
||||
*/
|
||||
/*public boolean drawBG(int width, int height, float alpha, boolean stretch) {
|
||||
if (bg == null)
|
||||
return false;
|
||||
try {
|
||||
Image bgImage = bgImageCache.get(this);
|
||||
if (bgImage == null) {
|
||||
bgImage = new Image(bg.getAbsolutePath());
|
||||
bgImageCache.put(this, bgImage);
|
||||
}
|
||||
|
||||
int swidth = width;
|
||||
int sheight = height;
|
||||
if (!stretch) {
|
||||
// fit image to screen
|
||||
if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
|
||||
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
||||
else
|
||||
swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
|
||||
} else {
|
||||
// fill screen while maintaining aspect ratio
|
||||
if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
|
||||
swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
|
||||
else
|
||||
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
||||
}
|
||||
bgImage = bgImage.getScaledCopy(swidth, sheight);
|
||||
|
||||
bgImage.setAlpha(alpha);
|
||||
bgImage.drawCentered(width / 2, height / 2);
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to get background image '%s'.", bg), e);
|
||||
bg = null; // don't try to load the file again until a restart
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Compares two Beatmap objects first by overall difficulty, then by total objects.
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(Beatmap that) {
|
||||
int cmp = Float.compare(this.overallDifficulty, that.overallDifficulty);
|
||||
if (cmp == 0)
|
||||
cmp = Integer.compare(
|
||||
this.hitObjectCircle + this.hitObjectSlider + this.hitObjectSpinner,
|
||||
that.hitObjectCircle + that.hitObjectSlider + that.hitObjectSpinner
|
||||
);
|
||||
return cmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted string: "Artist - Title [Version]"
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s - %s [%s]", getArtist(), getTitle(), version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link #breaks} field formatted as a string,
|
||||
* or null if the field is null.
|
||||
*/
|
||||
public String breaksToString() {
|
||||
if (breaks == null)
|
||||
return null;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i : breaks) {
|
||||
sb.append(i);
|
||||
sb.append(',');
|
||||
}
|
||||
if (sb.length() > 0)
|
||||
sb.setLength(sb.length() - 1);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link #breaks} field from a string.
|
||||
* @param s the string
|
||||
*/
|
||||
public void breaksFromString(String s) {
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
this.breaks = new ArrayList<Integer>();
|
||||
String[] tokens = s.split(",");
|
||||
for (int i = 0; i < tokens.length; i++)
|
||||
breaks.add(Integer.parseInt(tokens[i]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link #timingPoints} field formatted as a string,
|
||||
* or null if the field is null.
|
||||
*/
|
||||
public String timingPointsToString() {
|
||||
if (timingPoints == null)
|
||||
return null;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (TimingPoint p : timingPoints) {
|
||||
sb.append(p.toString());
|
||||
sb.append('|');
|
||||
}
|
||||
if (sb.length() > 0)
|
||||
sb.setLength(sb.length() - 1);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link #timingPoints} field from a string.
|
||||
* @param s the string
|
||||
*/
|
||||
public void timingPointsFromString(String s) {
|
||||
this.timingPoints = new ArrayList<TimingPoint>();
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
String[] tokens = s.split("\\|");
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
try {
|
||||
timingPoints.add(new TimingPoint(tokens[i]));
|
||||
} catch (Exception e) {
|
||||
// Log.warn(String.format("Failed to read timing point '%s'.", tokens[i]), e);
|
||||
}
|
||||
}
|
||||
timingPoints.trimToSize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link #combo} field formatted as a string,
|
||||
* or null if the field is null or the default combo.
|
||||
*/
|
||||
public String comboToString() {
|
||||
if (combo == null)
|
||||
return null;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < combo.length; i++) {
|
||||
Color c = combo[i];
|
||||
sb.append(c.getRed());
|
||||
sb.append(',');
|
||||
sb.append(c.getGreen());
|
||||
sb.append(',');
|
||||
sb.append(c.getBlue());
|
||||
sb.append('|');
|
||||
}
|
||||
if (sb.length() > 0)
|
||||
sb.setLength(sb.length() - 1);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link #combo} field from a string.
|
||||
* @param s the string
|
||||
*/
|
||||
public void comboFromString(String s) {
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
LinkedList<Color> colors = new LinkedList<Color>();
|
||||
String[] tokens = s.split("\\|");
|
||||
for (int i = 0; i < tokens.length; i++) {
|
||||
String[] rgb = tokens[i].split(",");
|
||||
colors.add(Color.rgb(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2])));
|
||||
}
|
||||
if (!colors.isEmpty())
|
||||
this.combo = colors.toArray(new Color[colors.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link #sliderBorder} field formatted as a string,
|
||||
* or null if the field is null.
|
||||
*/
|
||||
public String sliderBorderToString() {
|
||||
if (sliderBorder == null)
|
||||
return null;
|
||||
|
||||
return String.format("%d,%d,%d", sliderBorder.getRed(), sliderBorder.getGreen(), sliderBorder.getBlue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link #sliderBorder} field from a string.
|
||||
* @param s the string
|
||||
*/
|
||||
public void sliderBorderFromString(String s) {
|
||||
if (s == null)
|
||||
return;
|
||||
|
||||
String[] rgb = s.split(",");
|
||||
this.sliderBorder = Color.rgb(Integer.parseInt(rgb[0]), Integer.parseInt(rgb[1]), Integer.parseInt(rgb[2]));
|
||||
}
|
||||
}
|
624
src/itdelatrisu/opsu/beatmap/BeatmapParser.java
Normal file
624
src/itdelatrisu/opsu/beatmap/BeatmapParser.java
Normal file
@ -0,0 +1,624 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.beatmap;
|
||||
|
||||
import itdelatrisu.opsu.Log;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.io.MD5InputStreamWrapper;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* Parser for beatmaps.
|
||||
*/
|
||||
public class BeatmapParser {
|
||||
/** The string lookup database. */
|
||||
private static HashMap<String, String> stringdb = new HashMap<>();
|
||||
|
||||
/** The expected pattern for beatmap directories, used to find beatmap set IDs. */
|
||||
private static final String DIR_MSID_PATTERN = "^\\d+ .*";
|
||||
|
||||
/** The current file being parsed. */
|
||||
private static File currentFile;
|
||||
|
||||
/** The current directory number while parsing. */
|
||||
private static int currentDirectoryIndex = -1;
|
||||
|
||||
/** The total number of directories to parse. */
|
||||
private static int totalDirectories = -1;
|
||||
|
||||
/** Parser statuses. */
|
||||
public enum Status { NONE, PARSING, CACHE, INSERTING };
|
||||
|
||||
/** The current status. */
|
||||
private static Status status = Status.NONE;
|
||||
|
||||
/** If no Provider supports a MessageDigestSpi implementation for the MD5 algorithm. */
|
||||
private static boolean hasNoMD5Algorithm = false;
|
||||
|
||||
// This class should not be instantiated.
|
||||
private BeatmapParser() {}
|
||||
|
||||
/**
|
||||
* Parses a beatmap.
|
||||
* @param file the file to parse
|
||||
* @param dir the directory containing the beatmap
|
||||
* @param beatmaps the song group
|
||||
* @param parseObjects if true, hit objects will be fully parsed now
|
||||
* @return the new beatmap
|
||||
*/
|
||||
public static Beatmap parseFile(File file, File dir, boolean parseObjects) {
|
||||
Beatmap beatmap = new Beatmap(file);
|
||||
beatmap.timingPoints = new ArrayList<TimingPoint>();
|
||||
|
||||
try (
|
||||
InputStream bis = new BufferedInputStream(new FileInputStream(file));
|
||||
MD5InputStreamWrapper md5stream = (!hasNoMD5Algorithm) ? new MD5InputStreamWrapper(bis) : null;
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader((md5stream != null) ? md5stream : bis, "UTF-8"));
|
||||
) {
|
||||
String line = in.readLine();
|
||||
String tokens[] = null;
|
||||
while (line != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line)) {
|
||||
line = in.readLine();
|
||||
continue;
|
||||
}
|
||||
switch (line) {
|
||||
case "[General]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
if ((tokens = tokenize(line)) == null)
|
||||
continue;
|
||||
try {
|
||||
switch (tokens[0]) {
|
||||
case "AudioFilename":
|
||||
File audioFileName = new File(dir, tokens[1]);
|
||||
if (!audioFileName.isFile()) {
|
||||
// try to find the file with a case-insensitive match
|
||||
boolean match = false;
|
||||
for (String s : dir.list()) {
|
||||
if (s.equalsIgnoreCase(tokens[1])) {
|
||||
audioFileName = new File(dir, s);
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!match) {
|
||||
Log.error(String.format("Audio file '%s' not found in directory '%s'.", tokens[1], dir.getName()));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
beatmap.audioFilename = audioFileName;
|
||||
break;
|
||||
case "AudioLeadIn":
|
||||
beatmap.audioLeadIn = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
// case "AudioHash": // deprecated
|
||||
// beatmap.audioHash = tokens[1];
|
||||
// break;
|
||||
case "PreviewTime":
|
||||
beatmap.previewTime = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "Countdown":
|
||||
beatmap.countdown = Byte.parseByte(tokens[1]);
|
||||
break;
|
||||
case "SampleSet":
|
||||
beatmap.sampleSet = getDBString(tokens[1]);
|
||||
break;
|
||||
case "StackLeniency":
|
||||
beatmap.stackLeniency = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "Mode":
|
||||
beatmap.mode = Byte.parseByte(tokens[1]);
|
||||
|
||||
/* Non-Opsu! standard files not implemented (obviously). */
|
||||
if (beatmap.mode != Beatmap.MODE_OSU)
|
||||
return null;
|
||||
|
||||
break;
|
||||
case "LetterboxInBreaks":
|
||||
beatmap.letterboxInBreaks = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "WidescreenStoryboard":
|
||||
beatmap.widescreenStoryboard = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "EpilepsyWarning":
|
||||
beatmap.epilepsyWarning = Utils.parseBoolean(tokens[1]);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read line '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "[Editor]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
/* Not implemented. */
|
||||
// if ((tokens = tokenize(line)) == null)
|
||||
// continue;
|
||||
// try {
|
||||
// switch (tokens[0]) {
|
||||
// case "Bookmarks":
|
||||
// String[] bookmarks = tokens[1].split(",");
|
||||
// beatmap.bookmarks = new int[bookmarks.length];
|
||||
// for (int i = 0; i < bookmarks.length; i++)
|
||||
// osu.bookmarks[i] = Integer.parseInt(bookmarks[i]);
|
||||
// break;
|
||||
// case "DistanceSpacing":
|
||||
// beatmap.distanceSpacing = Float.parseFloat(tokens[1]);
|
||||
// break;
|
||||
// case "BeatDivisor":
|
||||
// beatmap.beatDivisor = Byte.parseByte(tokens[1]);
|
||||
// break;
|
||||
// case "GridSize":
|
||||
// beatmap.gridSize = Integer.parseInt(tokens[1]);
|
||||
// break;
|
||||
// case "TimelineZoom":
|
||||
// beatmap.timelineZoom = Integer.parseInt(tokens[1]);
|
||||
// break;
|
||||
// default:
|
||||
// break;
|
||||
// }
|
||||
// } catch (Exception e) {
|
||||
// Log.warn(String.format("Failed to read editor line '%s' for file '%s'.",
|
||||
// line, file.getAbsolutePath()), e);
|
||||
// }
|
||||
}
|
||||
break;
|
||||
case "[Metadata]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
if ((tokens = tokenize(line)) == null)
|
||||
continue;
|
||||
try {
|
||||
switch (tokens[0]) {
|
||||
case "Title":
|
||||
beatmap.title = getDBString(tokens[1]);
|
||||
break;
|
||||
case "TitleUnicode":
|
||||
beatmap.titleUnicode = getDBString(tokens[1]);
|
||||
break;
|
||||
case "Artist":
|
||||
beatmap.artist = getDBString(tokens[1]);
|
||||
break;
|
||||
case "ArtistUnicode":
|
||||
beatmap.artistUnicode = getDBString(tokens[1]);
|
||||
break;
|
||||
case "Creator":
|
||||
beatmap.creator = getDBString(tokens[1]);
|
||||
break;
|
||||
case "Version":
|
||||
beatmap.version = getDBString(tokens[1]);
|
||||
break;
|
||||
case "Source":
|
||||
beatmap.source = getDBString(tokens[1]);
|
||||
break;
|
||||
case "Tags":
|
||||
beatmap.tags = getDBString(tokens[1].toLowerCase());
|
||||
break;
|
||||
case "BeatmapID":
|
||||
beatmap.beatmapID = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "BeatmapSetID":
|
||||
beatmap.beatmapSetID = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read metadata '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
if (beatmap.beatmapSetID <= 0) { // try to determine MSID from directory name
|
||||
if (dir != null && dir.isDirectory()) {
|
||||
String dirName = dir.getName();
|
||||
if (!dirName.isEmpty() && dirName.matches(DIR_MSID_PATTERN))
|
||||
beatmap.beatmapSetID = Integer.parseInt(dirName.substring(0, dirName.indexOf(' ')));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "[Difficulty]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
if ((tokens = tokenize(line)) == null)
|
||||
continue;
|
||||
try {
|
||||
switch (tokens[0]) {
|
||||
case "HPDrainRate":
|
||||
beatmap.HPDrainRate = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "CircleSize":
|
||||
beatmap.circleSize = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "OverallDifficulty":
|
||||
beatmap.overallDifficulty = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "ApproachRate":
|
||||
beatmap.approachRate = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "SliderMultiplier":
|
||||
beatmap.sliderMultiplier = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
case "SliderTickRate":
|
||||
beatmap.sliderTickRate = Float.parseFloat(tokens[1]);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read difficulty '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
if (beatmap.approachRate == -1f) // not in old format
|
||||
beatmap.approachRate = beatmap.overallDifficulty;
|
||||
break;
|
||||
case "[Events]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
tokens = line.split(",");
|
||||
switch (tokens[0]) {
|
||||
case "0": // background
|
||||
tokens[2] = tokens[2].replaceAll("^\"|\"$", "");
|
||||
String ext = BeatmapParser.getExtension(tokens[2]);
|
||||
if (ext.equals("jpg") || ext.equals("png"))
|
||||
beatmap.bg = new File(dir, getDBString(tokens[2]));
|
||||
break;
|
||||
case "2": // break periods
|
||||
try {
|
||||
if (beatmap.breaks == null) // optional, create if needed
|
||||
beatmap.breaks = new ArrayList<Integer>();
|
||||
beatmap.breaks.add(Integer.parseInt(tokens[1]));
|
||||
beatmap.breaks.add(Integer.parseInt(tokens[2]));
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read break period '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
/* Not implemented. */
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (beatmap.breaks != null)
|
||||
beatmap.breaks.trimToSize();
|
||||
break;
|
||||
case "[TimingPoints]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
|
||||
try {
|
||||
// parse timing point
|
||||
TimingPoint timingPoint = new TimingPoint(line);
|
||||
|
||||
// calculate BPM
|
||||
if (!timingPoint.isInherited()) {
|
||||
int bpm = Math.round(60000 / timingPoint.getBeatLength());
|
||||
if (beatmap.bpmMin == 0)
|
||||
beatmap.bpmMin = beatmap.bpmMax = bpm;
|
||||
else if (bpm < beatmap.bpmMin)
|
||||
beatmap.bpmMin = bpm;
|
||||
else if (bpm > beatmap.bpmMax)
|
||||
beatmap.bpmMax = bpm;
|
||||
}
|
||||
|
||||
beatmap.timingPoints.add(timingPoint);
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read timing point '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
beatmap.timingPoints.trimToSize();
|
||||
break;
|
||||
case "[Colours]":
|
||||
LinkedList<Color> colors = new LinkedList<Color>();
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
if ((tokens = tokenize(line)) == null)
|
||||
continue;
|
||||
try {
|
||||
String[] rgb = tokens[1].split(",");
|
||||
Color color = Color.rgb(
|
||||
Integer.parseInt(rgb[0]),
|
||||
Integer.parseInt(rgb[1]),
|
||||
Integer.parseInt(rgb[2])
|
||||
);
|
||||
switch (tokens[0]) {
|
||||
case "Combo1":
|
||||
case "Combo2":
|
||||
case "Combo3":
|
||||
case "Combo4":
|
||||
case "Combo5":
|
||||
case "Combo6":
|
||||
case "Combo7":
|
||||
case "Combo8":
|
||||
colors.add(color);
|
||||
break;
|
||||
case "SliderBorder":
|
||||
beatmap.sliderBorder = color;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read color '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
if (!colors.isEmpty())
|
||||
beatmap.combo = colors.toArray(new Color[colors.size()]);
|
||||
break;
|
||||
case "[HitObjects]":
|
||||
int type = 0;
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
/* Only type counts parsed at this time. */
|
||||
tokens = line.split(",");
|
||||
try {
|
||||
type = Integer.parseInt(tokens[3]);
|
||||
if ((type & HitObject.TYPE_CIRCLE) > 0)
|
||||
beatmap.hitObjectCircle++;
|
||||
else if ((type & HitObject.TYPE_SLIDER) > 0)
|
||||
beatmap.hitObjectSlider++;
|
||||
else //if ((type & HitObject.TYPE_SPINNER) > 0)
|
||||
beatmap.hitObjectSpinner++;
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read hit object '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// map length = last object end time (TODO: end on slider?)
|
||||
if ((type & HitObject.TYPE_SPINNER) > 0) {
|
||||
// some 'endTime' fields contain a ':' character (?)
|
||||
int index = tokens[5].indexOf(':');
|
||||
if (index != -1)
|
||||
tokens[5] = tokens[5].substring(0, index);
|
||||
beatmap.endTime = Integer.parseInt(tokens[5]);
|
||||
} else if (type != 0)
|
||||
beatmap.endTime = Integer.parseInt(tokens[2]);
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read hit object end time '%s' for file '%s'.",
|
||||
line, file.getAbsolutePath()), e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
line = in.readLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (md5stream != null)
|
||||
beatmap.md5Hash = md5stream.getMD5();
|
||||
} catch (IOException e) {
|
||||
Log.error(String.format("Failed to read file '%s'.", file.getAbsolutePath()), e, false);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.error("Failed to get MD5 hash stream.", e, true);
|
||||
|
||||
// retry without MD5
|
||||
hasNoMD5Algorithm = true;
|
||||
return parseFile(file, dir, parseObjects);
|
||||
}
|
||||
|
||||
// no associated audio file?
|
||||
if (beatmap.audioFilename == null)
|
||||
return null;
|
||||
|
||||
// parse hit objects now?
|
||||
if (parseObjects)
|
||||
parseHitObjects(beatmap);
|
||||
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses all hit objects in a beatmap.
|
||||
* @param beatmap the beatmap to parse
|
||||
*/
|
||||
public static void parseHitObjects(Beatmap beatmap) {
|
||||
if (beatmap.objects != null) // already parsed
|
||||
return;
|
||||
|
||||
beatmap.objects = new HitObject[(beatmap.hitObjectCircle + beatmap.hitObjectSlider + beatmap.hitObjectSpinner)];
|
||||
|
||||
try (BufferedReader in = new BufferedReader(new FileReader(beatmap.getFile()))) {
|
||||
String line = in.readLine();
|
||||
while (line != null) {
|
||||
line = line.trim();
|
||||
if (!line.equals("[HitObjects]"))
|
||||
line = in.readLine();
|
||||
else
|
||||
break;
|
||||
}
|
||||
if (line == null) {
|
||||
Log.warn(String.format("No hit objects found in Beatmap '%s'.", beatmap.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
// combo info
|
||||
Color[] combo = beatmap.getComboColors();
|
||||
int comboIndex = 0; // color index
|
||||
int comboNumber = 1; // combo number
|
||||
|
||||
int objectIndex = 0;
|
||||
boolean first = true;
|
||||
while ((line = in.readLine()) != null && objectIndex < beatmap.objects.length) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
|
||||
// lines must have at minimum 5 parameters
|
||||
int tokenCount = line.length() - line.replace(",", "").length();
|
||||
if (tokenCount < 4)
|
||||
continue;
|
||||
|
||||
try {
|
||||
// create a new HitObject for each line
|
||||
HitObject hitObject = new HitObject(line);
|
||||
|
||||
// set combo info
|
||||
// - new combo: get next combo index, reset combo number
|
||||
// - else: maintain combo index, increase combo number
|
||||
if (hitObject.isNewCombo() || first) {
|
||||
int skip = (hitObject.isSpinner() ? 0 : 1) + hitObject.getComboSkip();
|
||||
for (int i = 0; i < skip; i++) {
|
||||
comboIndex = (comboIndex + 1) % combo.length;
|
||||
comboNumber = 1;
|
||||
}
|
||||
first = false;
|
||||
}
|
||||
|
||||
hitObject.setComboIndex(comboIndex);
|
||||
hitObject.setComboNumber(comboNumber++);
|
||||
|
||||
beatmap.objects[objectIndex++] = hitObject;
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read hit object '%s' for Beatmap '%s'.",
|
||||
line, beatmap.toString()), e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.error(String.format("Failed to read file '%s'.", beatmap.getFile().getAbsolutePath()), e, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the line is too short or commented.
|
||||
*/
|
||||
private static boolean isValidLine(String line) {
|
||||
return (line.length() > 1 && !line.startsWith("//"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits line into two strings: tag, value.
|
||||
* If no ':' character is present, null will be returned.
|
||||
*/
|
||||
private static String[] tokenize(String line) {
|
||||
int index = line.indexOf(':');
|
||||
if (index == -1) {
|
||||
// Log.debug(String.format("Failed to tokenize line: '%s'.", line));
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] tokens = new String[2];
|
||||
tokens[0] = line.substring(0, index).trim();
|
||||
tokens[1] = line.substring(index + 1).trim();
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file extension of a file.
|
||||
*/
|
||||
public static String getExtension(String file) {
|
||||
int i = file.lastIndexOf('.');
|
||||
return (i != -1) ? file.substring(i + 1).toLowerCase() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the current file being parsed, or null if none.
|
||||
*/
|
||||
public static String getCurrentFileName() {
|
||||
if (status == Status.PARSING)
|
||||
return (currentFile != null) ? currentFile.getName() : null;
|
||||
else
|
||||
return (status == Status.NONE) ? null : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the progress of file parsing, or -1 if not parsing.
|
||||
* @return the completion percent [0, 100] or -1
|
||||
*/
|
||||
public static int getParserProgress() {
|
||||
if (currentDirectoryIndex == -1 || totalDirectories == -1)
|
||||
return -1;
|
||||
|
||||
return currentDirectoryIndex * 100 / totalDirectories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current parser status.
|
||||
*/
|
||||
public static Status getStatus() { return status; }
|
||||
|
||||
/**
|
||||
* Returns the String object in the database for the given String.
|
||||
* If none, insert the String into the database and return the original String.
|
||||
* @param s the string to retrieve
|
||||
* @return the string object
|
||||
*/
|
||||
public static String getDBString(String s) {
|
||||
String DBString = stringdb.get(s);
|
||||
if (DBString == null) {
|
||||
stringdb.put(s, s);
|
||||
return s;
|
||||
} else
|
||||
return DBString;
|
||||
}
|
||||
}
|
545
src/itdelatrisu/opsu/beatmap/HitObject.java
Normal file
545
src/itdelatrisu/opsu/beatmap/HitObject.java
Normal file
@ -0,0 +1,545 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.beatmap;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* Data type representing a parsed hit object.
|
||||
*/
|
||||
public class HitObject {
|
||||
/** Hit object types (bits). */
|
||||
public static final int
|
||||
TYPE_CIRCLE = 1,
|
||||
TYPE_SLIDER = 2,
|
||||
TYPE_NEWCOMBO = 4, // not an object
|
||||
TYPE_SPINNER = 8;
|
||||
|
||||
/** Hit object type names. */
|
||||
private static final String
|
||||
CIRCLE = "circle",
|
||||
SLIDER = "slider",
|
||||
SPINNER = "spinner",
|
||||
UNKNOWN = "unknown object";
|
||||
|
||||
/** Hit sound types (bits). */
|
||||
public static final byte
|
||||
SOUND_NORMAL = 0,
|
||||
SOUND_WHISTLE = 2,
|
||||
SOUND_FINISH = 4,
|
||||
SOUND_CLAP = 8;
|
||||
|
||||
/**
|
||||
* Slider curve types.
|
||||
* (Deprecated: only Beziers are currently used.)
|
||||
*/
|
||||
public static final char
|
||||
SLIDER_CATMULL = 'C',
|
||||
SLIDER_BEZIER = 'B',
|
||||
SLIDER_LINEAR = 'L',
|
||||
SLIDER_PASSTHROUGH = 'P';
|
||||
|
||||
/** Max hit object coordinates. */
|
||||
private static final int
|
||||
MAX_X = 512,
|
||||
MAX_Y = 384;
|
||||
|
||||
/** The x and y multipliers for hit object coordinates. */
|
||||
private static float xMultiplier, yMultiplier;
|
||||
|
||||
/** The x and y offsets for hit object coordinates. */
|
||||
private static int
|
||||
xOffset, // offset right of border
|
||||
yOffset; // offset below health bar
|
||||
|
||||
/** The container height. */
|
||||
private static int containerHeight;
|
||||
|
||||
/** The offset per stack. */
|
||||
private static float stackOffset;
|
||||
|
||||
/**
|
||||
* Returns the stack position modifier, in pixels.
|
||||
* @return stack position modifier
|
||||
*/
|
||||
public static float getStackOffset() { return stackOffset; }
|
||||
|
||||
/**
|
||||
* Sets the stack position modifier.
|
||||
* @param offset stack position modifier, in pixels
|
||||
*/
|
||||
public static void setStackOffset(float offset) { stackOffset = offset; }
|
||||
|
||||
/** Starting coordinates. */
|
||||
private float x, y;
|
||||
|
||||
/** Start time (in ms). */
|
||||
private int time;
|
||||
|
||||
/** Hit object type (TYPE_* bitmask). */
|
||||
private int type;
|
||||
|
||||
/** Hit sound type (SOUND_* bitmask). */
|
||||
private byte hitSound;
|
||||
|
||||
/** Hit sound addition (sampleSet, AdditionSampleSet, ?, ...). */
|
||||
private byte[] addition;
|
||||
|
||||
/** Slider curve type (SLIDER_* constant). */
|
||||
private char sliderType;
|
||||
|
||||
/** Slider coordinate lists. */
|
||||
private float[] sliderX, sliderY;
|
||||
|
||||
/** Slider repeat count. */
|
||||
private int repeat;
|
||||
|
||||
/** Slider pixel length. */
|
||||
private float pixelLength;
|
||||
|
||||
/** Spinner end time (in ms). */
|
||||
private int endTime;
|
||||
|
||||
/** Slider edge hit sound type (SOUND_* bitmask). */
|
||||
private byte[] edgeHitSound;
|
||||
|
||||
/** Slider edge hit sound addition (sampleSet, AdditionSampleSet). */
|
||||
private byte[][] edgeAddition;
|
||||
|
||||
/** Current index in combo color array. */
|
||||
private int comboIndex;
|
||||
|
||||
/** Number to display in hit object. */
|
||||
private int comboNumber;
|
||||
|
||||
/** Hit object index in the current stack. */
|
||||
private int stack;
|
||||
|
||||
private String sliderPath;
|
||||
|
||||
/**
|
||||
* Initializes the HitObject data type with container dimensions.
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
*/
|
||||
public static void init(int width, int height) {
|
||||
containerHeight = height;
|
||||
int swidth = width;
|
||||
int sheight = height;
|
||||
if (swidth * 3 > sheight * 4)
|
||||
swidth = sheight * 4 / 3;
|
||||
else
|
||||
sheight = swidth * 3 / 4;
|
||||
xMultiplier = swidth / 640f;
|
||||
yMultiplier = sheight / 480f;
|
||||
xOffset = (int) (width - MAX_X * xMultiplier) / 2;
|
||||
yOffset = (int) (height - MAX_Y * yMultiplier) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X multiplier for coordinates.
|
||||
*/
|
||||
public static float getXMultiplier() { return xMultiplier; }
|
||||
|
||||
/**
|
||||
* Returns the Y multiplier for coordinates.
|
||||
*/
|
||||
public static float getYMultiplier() { return yMultiplier; }
|
||||
|
||||
/**
|
||||
* Returns the X offset for coordinates.
|
||||
*/
|
||||
public static int getXOffset() { return xOffset; }
|
||||
|
||||
/**
|
||||
* Returns the Y offset for coordinates.
|
||||
*/
|
||||
public static int getYOffset() { return yOffset; }
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param line the line to be parsed
|
||||
*/
|
||||
public HitObject(String line) {
|
||||
/**
|
||||
* [OBJECT FORMATS]
|
||||
* Circles:
|
||||
* x,y,time,type,hitSound,addition
|
||||
* 256,148,9466,1,2,0:0:0:0:
|
||||
*
|
||||
* Sliders:
|
||||
* x,y,time,type,hitSound,sliderType|curveX:curveY|...,repeat,pixelLength,edgeHitsound,edgeAddition,addition
|
||||
* 300,68,4591,2,0,B|372:100|332:172|420:192,2,180,2|2|2,0:0|0:0|0:0,0:0:0:0:
|
||||
*
|
||||
* Spinners:
|
||||
* x,y,time,type,hitSound,endTime,addition
|
||||
* 256,192,654,12,0,4029,0:0:0:0:
|
||||
*
|
||||
* NOTE: 'addition' -> sampl:add:cust:vol:hitsound (optional, defaults to "0:0:0:0:")
|
||||
*/
|
||||
String tokens[] = line.split(",");
|
||||
|
||||
// common fields
|
||||
this.x = Float.parseFloat(tokens[0]);
|
||||
this.y = Float.parseFloat(tokens[1]);
|
||||
this.time = Integer.parseInt(tokens[2]);
|
||||
this.type = Integer.parseInt(tokens[3]);
|
||||
this.hitSound = Byte.parseByte(tokens[4]);
|
||||
|
||||
// type-specific fields
|
||||
int additionIndex;
|
||||
if ((type & HitObject.TYPE_CIRCLE) > 0)
|
||||
additionIndex = 5;
|
||||
else if ((type & HitObject.TYPE_SLIDER) > 0) {
|
||||
additionIndex = 10;
|
||||
|
||||
// slider curve type and coordinates
|
||||
sliderPath = tokens[5];
|
||||
String[] sliderTokens = tokens[5].split("\\|");
|
||||
this.sliderType = sliderTokens[0].charAt(0);
|
||||
this.sliderX = new float[sliderTokens.length - 1];
|
||||
this.sliderY = new float[sliderTokens.length - 1];
|
||||
for (int j = 1; j < sliderTokens.length; j++) {
|
||||
String[] sliderXY = sliderTokens[j].split(":");
|
||||
this.sliderX[j - 1] = Integer.parseInt(sliderXY[0]);
|
||||
this.sliderY[j - 1] = Integer.parseInt(sliderXY[1]);
|
||||
}
|
||||
this.repeat = Integer.parseInt(tokens[6]);
|
||||
this.pixelLength = Float.parseFloat(tokens[7]);
|
||||
if (tokens.length > 8) {
|
||||
String[] edgeHitSoundTokens = tokens[8].split("\\|");
|
||||
this.edgeHitSound = new byte[edgeHitSoundTokens.length];
|
||||
for (int j = 0; j < edgeHitSoundTokens.length; j++)
|
||||
edgeHitSound[j] = Byte.parseByte(edgeHitSoundTokens[j]);
|
||||
}
|
||||
if (tokens.length > 9) {
|
||||
String[] edgeAdditionTokens = tokens[9].split("\\|");
|
||||
this.edgeAddition = new byte[edgeAdditionTokens.length][2];
|
||||
for (int j = 0; j < edgeAdditionTokens.length; j++) {
|
||||
String[] tedgeAddition = edgeAdditionTokens[j].split(":");
|
||||
edgeAddition[j][0] = Byte.parseByte(tedgeAddition[0]);
|
||||
edgeAddition[j][1] = Byte.parseByte(tedgeAddition[1]);
|
||||
}
|
||||
}
|
||||
} else { //if ((type & HitObject.TYPE_SPINNER) > 0) {
|
||||
additionIndex = 6;
|
||||
|
||||
// some 'endTime' fields contain a ':' character (?)
|
||||
int index = tokens[5].indexOf(':');
|
||||
if (index != -1)
|
||||
tokens[5] = tokens[5].substring(0, index);
|
||||
this.endTime = Integer.parseInt(tokens[5]);
|
||||
}
|
||||
|
||||
// addition
|
||||
if (tokens.length > additionIndex) {
|
||||
String[] additionTokens = tokens[additionIndex].split(":");
|
||||
this.addition = new byte[additionTokens.length];
|
||||
for (int j = 0; j < additionTokens.length; j++)
|
||||
this.addition[j] = Byte.parseByte(additionTokens[j]);
|
||||
}
|
||||
}
|
||||
|
||||
public String getSliderPath() {
|
||||
return sliderPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw starting x coordinate.
|
||||
*/
|
||||
public float getX() { return x; }
|
||||
|
||||
/**
|
||||
* Returns the raw starting y coordinate.
|
||||
*/
|
||||
public float getY() { return y; }
|
||||
|
||||
/**
|
||||
* Returns the scaled starting x coordinate.
|
||||
*/
|
||||
public float getScaledX() { return (x - stack * stackOffset) * xMultiplier + xOffset; }
|
||||
|
||||
/**
|
||||
* Returns the scaled starting y coordinate.
|
||||
*/
|
||||
public float getScaledY() {
|
||||
/*if (GameMod.HARD_ROCK.isActive())
|
||||
return containerHeight - ((y + stack * stackOffset) * yMultiplier + yOffset);
|
||||
else*/
|
||||
return (y - stack * stackOffset) * yMultiplier + yOffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start time.
|
||||
* @return the start time (in ms)
|
||||
*/
|
||||
public int getTime() { return time; }
|
||||
|
||||
/**
|
||||
* Returns the hit object type.
|
||||
* @return the object type (TYPE_* bitmask)
|
||||
*/
|
||||
public int getType() { return type; }
|
||||
|
||||
/**
|
||||
* Returns the name of the hit object type.
|
||||
*/
|
||||
public String getTypeName() {
|
||||
if (isCircle())
|
||||
return CIRCLE;
|
||||
else if (isSlider())
|
||||
return SLIDER;
|
||||
else if (isSpinner())
|
||||
return SPINNER;
|
||||
else
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the hit sound type.
|
||||
* @return the sound type (SOUND_* bitmask)
|
||||
*/
|
||||
public byte getHitSoundType() { return hitSound; }
|
||||
|
||||
/**
|
||||
* Returns the edge hit sound type.
|
||||
* @param index the slider edge index (ignored for non-sliders)
|
||||
* @return the sound type (SOUND_* bitmask)
|
||||
*/
|
||||
public byte getEdgeHitSoundType(int index) {
|
||||
if (edgeHitSound != null)
|
||||
return edgeHitSound[index];
|
||||
else
|
||||
return hitSound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slider type.
|
||||
* @return the slider type (SLIDER_* constant)
|
||||
*/
|
||||
public char getSliderType() { return sliderType; }
|
||||
|
||||
/**
|
||||
* Returns a list of raw slider x coordinates.
|
||||
*/
|
||||
public float[] getSliderX() { return sliderX; }
|
||||
|
||||
/**
|
||||
* Returns a list of raw slider y coordinates.
|
||||
*/
|
||||
public float[] getSliderY() { return sliderY; }
|
||||
|
||||
/**
|
||||
* Returns a list of scaled slider x coordinates.
|
||||
* Note that this method will create a new array.
|
||||
*/
|
||||
public float[] getScaledSliderX() {
|
||||
if (sliderX == null)
|
||||
return null;
|
||||
|
||||
float[] x = new float[sliderX.length];
|
||||
for (int i = 0; i < x.length; i++)
|
||||
x[i] = (sliderX[i] - stack * stackOffset) * xMultiplier + xOffset;
|
||||
return x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of scaled slider y coordinates.
|
||||
* Note that this method will create a new array.
|
||||
*/
|
||||
public float[] getScaledSliderY() {
|
||||
if (sliderY == null)
|
||||
return null;
|
||||
|
||||
float[] y = new float[sliderY.length];
|
||||
/*if (GameMod.HARD_ROCK.isActive()) {
|
||||
for (int i = 0; i < y.length; i++)
|
||||
y[i] = containerHeight - ((sliderY[i] + stack * stackOffset) * yMultiplier + yOffset);
|
||||
} else*/ {
|
||||
for (int i = 0; i < y.length; i++)
|
||||
y[i] = (sliderY[i] - stack * stackOffset) * yMultiplier + yOffset;
|
||||
}
|
||||
return y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slider repeat count.
|
||||
* @return the repeat count
|
||||
*/
|
||||
public int getRepeatCount() { return repeat; }
|
||||
|
||||
/**
|
||||
* Returns the slider pixel length.
|
||||
* @return the pixel length
|
||||
*/
|
||||
public float getPixelLength() { return pixelLength; }
|
||||
|
||||
/**
|
||||
* Returns the spinner end time.
|
||||
* @return the end time (in ms)
|
||||
*/
|
||||
public int getEndTime() { return endTime; }
|
||||
|
||||
/**
|
||||
* Sets the current index in the combo color array.
|
||||
* @param comboIndex the combo index
|
||||
*/
|
||||
public void setComboIndex(int comboIndex) { this.comboIndex = comboIndex; }
|
||||
|
||||
/**
|
||||
* Returns the current index in the combo color array.
|
||||
* @return the combo index
|
||||
*/
|
||||
public int getComboIndex() { return comboIndex; }
|
||||
|
||||
/**
|
||||
* Sets the number to display in the hit object.
|
||||
* @param comboNumber the combo number
|
||||
*/
|
||||
public void setComboNumber(int comboNumber) { this.comboNumber = comboNumber; }
|
||||
|
||||
/**
|
||||
* Returns the number to display in the hit object.
|
||||
* @return the combo number
|
||||
*/
|
||||
public int getComboNumber() { return comboNumber; }
|
||||
|
||||
/**
|
||||
* Returns whether or not the hit object is a circle.
|
||||
* @return true if circle
|
||||
*/
|
||||
public boolean isCircle() { return (type & TYPE_CIRCLE) > 0; }
|
||||
|
||||
/**
|
||||
* Returns whether or not the hit object is a slider.
|
||||
* @return true if slider
|
||||
*/
|
||||
public boolean isSlider() { return (type & TYPE_SLIDER) > 0; }
|
||||
|
||||
/**
|
||||
* Returns whether or not the hit object is a spinner.
|
||||
* @return true if spinner
|
||||
*/
|
||||
public boolean isSpinner() { return (type & TYPE_SPINNER) > 0; }
|
||||
|
||||
/**
|
||||
* Returns whether or not the hit object starts a new combo.
|
||||
* @return true if new combo
|
||||
*/
|
||||
public boolean isNewCombo() { return (type & TYPE_NEWCOMBO) > 0; }
|
||||
|
||||
/**
|
||||
* Returns the number of extra skips on the combo colors.
|
||||
*/
|
||||
public int getComboSkip() { return (type >> TYPE_NEWCOMBO); }
|
||||
|
||||
/**
|
||||
* Returns the sample set at the given index.
|
||||
* @param index the index (for sliders, ignored otherwise)
|
||||
* @return the sample set, or 0 if none available
|
||||
*/
|
||||
public byte getSampleSet(int index) {
|
||||
if (edgeAddition != null)
|
||||
return edgeAddition[index][0];
|
||||
if (addition != null)
|
||||
return addition[0];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the 'addition' sample set at the given index.
|
||||
* @param index the index (for sliders, ignored otherwise)
|
||||
* @return the sample set, or 0 if none available
|
||||
*/
|
||||
public byte getAdditionSampleSet(int index) {
|
||||
if (edgeAddition != null)
|
||||
return edgeAddition[index][1];
|
||||
if (addition != null)
|
||||
return addition[1];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hit object index in the current stack.
|
||||
* @param stack index in the stack
|
||||
*/
|
||||
public void setStack(int stack) { this.stack = stack; }
|
||||
|
||||
/**
|
||||
* Returns the hit object index in the current stack.
|
||||
* @return index in the stack
|
||||
*/
|
||||
public int getStack() { return stack; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
NumberFormat nf = new DecimalFormat("###.#####");
|
||||
|
||||
// common fields
|
||||
sb.append(nf.format(x)); sb.append(',');
|
||||
sb.append(nf.format(y)); sb.append(',');
|
||||
sb.append(time); sb.append(',');
|
||||
sb.append(type); sb.append(',');
|
||||
sb.append(hitSound); sb.append(',');
|
||||
|
||||
// type-specific fields
|
||||
if (isCircle())
|
||||
;
|
||||
else if (isSlider()) {
|
||||
sb.append(getSliderType());
|
||||
sb.append('|');
|
||||
for (int i = 0; i < sliderX.length; i++) {
|
||||
sb.append(nf.format(sliderX[i])); sb.append(':');
|
||||
sb.append(nf.format(sliderY[i])); sb.append('|');
|
||||
}
|
||||
sb.setCharAt(sb.length() - 1, ',');
|
||||
sb.append(repeat); sb.append(',');
|
||||
sb.append(pixelLength); sb.append(',');
|
||||
if (edgeHitSound != null) {
|
||||
for (int i = 0; i < edgeHitSound.length; i++) {
|
||||
sb.append(edgeHitSound[i]); sb.append('|');
|
||||
}
|
||||
sb.setCharAt(sb.length() - 1, ',');
|
||||
}
|
||||
if (edgeAddition != null) {
|
||||
for (int i = 0; i < edgeAddition.length; i++) {
|
||||
sb.append(edgeAddition[i][0]); sb.append(':');
|
||||
sb.append(edgeAddition[i][1]); sb.append('|');
|
||||
}
|
||||
sb.setCharAt(sb.length() - 1, ',');
|
||||
}
|
||||
} else if (isSpinner()) {
|
||||
sb.append(endTime);
|
||||
sb.append(',');
|
||||
}
|
||||
|
||||
// addition
|
||||
if (addition != null) {
|
||||
for (int i = 0; i < addition.length; i++) {
|
||||
sb.append(addition[i]);
|
||||
sb.append(':');
|
||||
}
|
||||
} else
|
||||
sb.setLength(sb.length() - 1);
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
157
src/itdelatrisu/opsu/beatmap/TimingPoint.java
Normal file
157
src/itdelatrisu/opsu/beatmap/TimingPoint.java
Normal file
@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.beatmap;
|
||||
|
||||
import itdelatrisu.opsu.Utils;
|
||||
|
||||
/**
|
||||
* Data type representing a timing point.
|
||||
*/
|
||||
public class TimingPoint {
|
||||
/** Timing point start time/offset (in ms). */
|
||||
private int time = 0;
|
||||
|
||||
/** Time per beat (in ms). [NON-INHERITED] */
|
||||
private float beatLength = 0f;
|
||||
|
||||
/** Slider multiplier. [INHERITED] */
|
||||
private int velocity = 0;
|
||||
|
||||
/** Beats per measure. */
|
||||
private int meter = 4;
|
||||
|
||||
/** Sound sample type. */
|
||||
private byte sampleType = 1;
|
||||
|
||||
/** Custom sound sample type. */
|
||||
private byte sampleTypeCustom = 0;
|
||||
|
||||
/** Volume of samples. [0, 100] */
|
||||
private int sampleVolume = 100;
|
||||
|
||||
/** Whether or not this timing point is inherited. */
|
||||
private boolean inherited = false;
|
||||
|
||||
/** Whether or not Kiai Mode is active. */
|
||||
private boolean kiai = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param line the line to be parsed
|
||||
*/
|
||||
public TimingPoint(String line) {
|
||||
// TODO: better support for old formats
|
||||
String[] tokens = line.split(",");
|
||||
try {
|
||||
this.time = (int) Float.parseFloat(tokens[0]); // rare float
|
||||
this.meter = Integer.parseInt(tokens[2]);
|
||||
this.sampleType = Byte.parseByte(tokens[3]);
|
||||
this.sampleTypeCustom = Byte.parseByte(tokens[4]);
|
||||
this.sampleVolume = Integer.parseInt(tokens[5]);
|
||||
// this.inherited = Utils.parseBoolean(tokens[6]);
|
||||
if (tokens.length > 7)
|
||||
this.kiai = Utils.parseBoolean(tokens[7]);
|
||||
} catch (ArrayIndexOutOfBoundsException e) {
|
||||
// Log.debug(String.format("Error parsing timing point: '%s'", line));
|
||||
}
|
||||
|
||||
// tokens[1] is either beatLength (positive) or velocity (negative)
|
||||
float beatLength = Float.parseFloat(tokens[1]);
|
||||
if (beatLength > 0)
|
||||
this.beatLength = beatLength;
|
||||
else {
|
||||
this.velocity = (int) beatLength;
|
||||
this.inherited = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timing point start time/offset.
|
||||
* @return the start time (in ms)
|
||||
*/
|
||||
public int getTime() { return time; }
|
||||
|
||||
/**
|
||||
* Returns the beat length. [NON-INHERITED]
|
||||
* @return the time per beat (in ms)
|
||||
*/
|
||||
public float getBeatLength() { return beatLength; }
|
||||
|
||||
/**
|
||||
* Returns the slider multiplier. [INHERITED]
|
||||
*/
|
||||
public float getSliderMultiplier() { return velocity / -100f; }
|
||||
|
||||
/**
|
||||
* Returns the meter.
|
||||
* @return the number of beats per measure
|
||||
*/
|
||||
public int getMeter() { return meter; }
|
||||
|
||||
/**
|
||||
* Returns the sample type.
|
||||
* <ul>
|
||||
* <li>0: none
|
||||
* <li>1: normal
|
||||
* <li>2: soft
|
||||
* <li>3: drum
|
||||
* </ul>
|
||||
*/
|
||||
public byte getSampleType() { return sampleType; }
|
||||
|
||||
/**
|
||||
* Returns the custom sample type.
|
||||
* <ul>
|
||||
* <li>0: default
|
||||
* <li>1: custom 1
|
||||
* <li>2: custom 2
|
||||
* </ul>
|
||||
*/
|
||||
public byte getSampleTypeCustom() { return sampleTypeCustom; }
|
||||
|
||||
/**
|
||||
* Returns the sample volume.
|
||||
* @return the sample volume [0, 1]
|
||||
*/
|
||||
public float getSampleVolume() { return sampleVolume / 100f; }
|
||||
|
||||
/**
|
||||
* Returns whether or not this timing point is inherited.
|
||||
* @return the inherited
|
||||
*/
|
||||
public boolean isInherited() { return inherited; }
|
||||
|
||||
/**
|
||||
* Returns whether or not Kiai Time is active.
|
||||
* @return true if active
|
||||
*/
|
||||
public boolean isKiaiTimeActive() { return kiai; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (inherited)
|
||||
return String.format("%d,%d,%d,%d,%d,%d,%d,%d",
|
||||
time, velocity, meter, (int) sampleType,
|
||||
(int) sampleTypeCustom, sampleVolume, 1, (kiai) ? 1: 0);
|
||||
else
|
||||
return String.format("%d,%g,%d,%d,%d,%d,%d,%d",
|
||||
time, beatLength, meter, (int) sampleType,
|
||||
(int) sampleTypeCustom, sampleVolume, 0, (kiai) ? 1: 0);
|
||||
}
|
||||
}
|
118
src/itdelatrisu/opsu/io/MD5InputStreamWrapper.java
Normal file
118
src/itdelatrisu/opsu/io/MD5InputStreamWrapper.java
Normal file
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Wrapper for an InputStream that computes the MD5 hash while reading the stream.
|
||||
*/
|
||||
public class MD5InputStreamWrapper extends InputStream {
|
||||
/** The input stream. */
|
||||
private InputStream in;
|
||||
|
||||
/** Whether the end of stream has been reached. */
|
||||
private boolean eof = false;
|
||||
|
||||
/** A MessageDigest object that implements the MD5 digest algorithm. */
|
||||
private MessageDigest md;
|
||||
|
||||
/** The computed MD5 hash. */
|
||||
private String md5;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param in the input stream
|
||||
* @throws NoSuchAlgorithmException if no Provider supports a MessageDigestSpi implementation for the MD5 algorithm
|
||||
*/
|
||||
public MD5InputStreamWrapper(InputStream in) throws NoSuchAlgorithmException {
|
||||
this.in = in;
|
||||
this.md = MessageDigest.getInstance("MD5");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int bytesRead = in.read();
|
||||
if (bytesRead >= 0)
|
||||
md.update((byte) bytesRead);
|
||||
else
|
||||
eof = true;
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException { return in.available(); }
|
||||
|
||||
@Override
|
||||
public void close() throws IOException { in.close(); }
|
||||
|
||||
@Override
|
||||
public synchronized void mark(int readlimit) { in.mark(readlimit); }
|
||||
|
||||
@Override
|
||||
public boolean markSupported() { return in.markSupported(); }
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int bytesRead = in.read(b, off, len);
|
||||
if (bytesRead >= 0)
|
||||
md.update(b, off, bytesRead);
|
||||
else
|
||||
eof = true;
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b) throws IOException { return read(b, 0, b.length); }
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
throw new RuntimeException("The reset() method is not implemented.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
throw new RuntimeException("The skip() method is not implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the MD5 hash of the input stream.
|
||||
* @throws IOException if the end of stream has not yet been reached and a call to {@link #read(byte[])} fails
|
||||
*/
|
||||
public String getMD5() throws IOException {
|
||||
if (md5 != null)
|
||||
return md5;
|
||||
|
||||
if (!eof) { // read the rest of the stream
|
||||
byte[] buf = new byte[0x1000];
|
||||
while (!eof)
|
||||
read(buf);
|
||||
}
|
||||
|
||||
byte[] md5byte = md.digest();
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (byte b : md5byte)
|
||||
result.append(String.format("%02x", b));
|
||||
md5 = result.toString();
|
||||
return md5;
|
||||
}
|
||||
}
|
176
src/itdelatrisu/opsu/io/OsuReader.java
Normal file
176
src/itdelatrisu/opsu/io/OsuReader.java
Normal file
@ -0,0 +1,176 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.io;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Reader for osu! file types.
|
||||
*
|
||||
* @author Markus Jarderot (http://stackoverflow.com/questions/28788616)
|
||||
*/
|
||||
public class OsuReader {
|
||||
/** Input stream reader. */
|
||||
private DataInputStream reader;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param file the file to read from
|
||||
* @throws IOException
|
||||
*/
|
||||
public OsuReader(File file) throws IOException {
|
||||
this(new FileInputStream(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param source the input stream to read from
|
||||
*/
|
||||
public OsuReader(InputStream source) {
|
||||
this.reader = new DataInputStream(new BufferedInputStream(source));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the input stream in use.
|
||||
*/
|
||||
public InputStream getInputStream() { return reader; }
|
||||
|
||||
/**
|
||||
* Closes the input stream.
|
||||
*/
|
||||
public void close() throws IOException { reader.close(); }
|
||||
|
||||
/**
|
||||
* Reads a 1-byte value.
|
||||
*/
|
||||
public byte readByte() throws IOException {
|
||||
return this.reader.readByte();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a 2-byte little endian value.
|
||||
*/
|
||||
public short readShort() throws IOException {
|
||||
byte[] bytes = new byte[2];
|
||||
this.reader.readFully(bytes);
|
||||
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||
return bb.getShort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a 4-byte little endian value.
|
||||
*/
|
||||
public int readInt() throws IOException {
|
||||
byte[] bytes = new byte[4];
|
||||
this.reader.readFully(bytes);
|
||||
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||
return bb.getInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an 8-byte little endian value.
|
||||
*/
|
||||
public long readLong() throws IOException {
|
||||
byte[] bytes = new byte[8];
|
||||
this.reader.readFully(bytes);
|
||||
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||
return bb.getLong();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a 4-byte little endian float.
|
||||
*/
|
||||
public float readSingle() throws IOException {
|
||||
byte[] bytes = new byte[4];
|
||||
this.reader.readFully(bytes);
|
||||
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||
return bb.getFloat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an 8-byte little endian double.
|
||||
*/
|
||||
public double readDouble() throws IOException {
|
||||
byte[] bytes = new byte[8];
|
||||
this.reader.readFully(bytes);
|
||||
ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
|
||||
return bb.getDouble();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a 1-byte value as a boolean.
|
||||
*/
|
||||
public boolean readBoolean() throws IOException {
|
||||
return this.reader.readBoolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an unsigned variable length integer (ULEB128).
|
||||
*/
|
||||
public int readULEB128() throws IOException {
|
||||
int value = 0;
|
||||
for (int shift = 0; shift < 32; shift += 7) {
|
||||
byte b = this.reader.readByte();
|
||||
value |= (b & 0x7F) << shift;
|
||||
if (b >= 0)
|
||||
return value; // MSB is zero. End of value.
|
||||
}
|
||||
throw new IOException("ULEB128 too large");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a variable-length string of 1-byte characters.
|
||||
*/
|
||||
public String readString() throws IOException {
|
||||
// 00 = empty string
|
||||
// 0B <length> <char>* = normal string
|
||||
// <length> is encoded as an LEB, and is the byte length of the rest.
|
||||
// <char>* is encoded as UTF8, and is the string content.
|
||||
byte kind = this.reader.readByte();
|
||||
if (kind == 0)
|
||||
return "";
|
||||
if (kind != 0x0B)
|
||||
throw new IOException(String.format("String format error: Expected 0x0B or 0x00, found 0x%02X", kind & 0xFF));
|
||||
int length = readULEB128();
|
||||
if (length == 0)
|
||||
return "";
|
||||
byte[] utf8bytes = new byte[length];
|
||||
this.reader.readFully(utf8bytes);
|
||||
return new String(utf8bytes, "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an 8-byte date in Windows ticks.
|
||||
*/
|
||||
public Date readDate() throws IOException {
|
||||
long ticks = readLong();
|
||||
final long TICKS_AT_EPOCH = 621355968000000000L;
|
||||
final long TICKS_PER_MILLISECOND = 10000;
|
||||
|
||||
return new Date((ticks - TICKS_AT_EPOCH) / TICKS_PER_MILLISECOND);
|
||||
}
|
||||
}
|
163
src/itdelatrisu/opsu/io/OsuWriter.java
Normal file
163
src/itdelatrisu/opsu/io/OsuWriter.java
Normal file
@ -0,0 +1,163 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.io;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Writer for osu! file types.
|
||||
*/
|
||||
public class OsuWriter {
|
||||
/** Output stream writer. */
|
||||
private DataOutputStream writer;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param file the file to write to
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public OsuWriter(File file) throws FileNotFoundException {
|
||||
this(new FileOutputStream(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param dest the output stream to write to
|
||||
*/
|
||||
public OsuWriter(OutputStream dest) {
|
||||
this.writer = new DataOutputStream(new BufferedOutputStream(dest));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the output stream in use.
|
||||
*/
|
||||
public OutputStream getOutputStream() { return writer; }
|
||||
|
||||
/**
|
||||
* Closes the output stream.
|
||||
* @throws IOException
|
||||
*/
|
||||
public void close() throws IOException { writer.close(); }
|
||||
|
||||
/**
|
||||
* Writes a 1-byte value.
|
||||
*/
|
||||
public void write(byte v) throws IOException { writer.writeByte(v); }
|
||||
|
||||
/**
|
||||
* Writes a 2-byte value.
|
||||
*/
|
||||
public void write(short v) throws IOException {
|
||||
byte[] bytes = ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(v).array();
|
||||
writer.write(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a 4-byte value.
|
||||
*/
|
||||
public void write(int v) throws IOException {
|
||||
byte[] bytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(v).array();
|
||||
writer.write(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an 8-byte value.
|
||||
*/
|
||||
public void write(long v) throws IOException {
|
||||
byte[] bytes = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(v).array();
|
||||
writer.write(bytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a 4-byte float.
|
||||
*/
|
||||
public void write(float v) throws IOException { writer.writeFloat(v); }
|
||||
|
||||
/**
|
||||
* Writes an 8-byte double.
|
||||
*/
|
||||
public void write(double v) throws IOException { writer.writeDouble(v); }
|
||||
|
||||
/**
|
||||
* Writes a boolean as a 1-byte value.
|
||||
*/
|
||||
public void write(boolean v) throws IOException { writer.writeBoolean(v); }
|
||||
|
||||
/**
|
||||
* Writes an unsigned variable length integer (ULEB128).
|
||||
*/
|
||||
public void writeULEB128(int i) throws IOException {
|
||||
int value = i;
|
||||
do {
|
||||
byte b = (byte) (value & 0x7F);
|
||||
value >>= 7;
|
||||
if (value != 0)
|
||||
b |= (1 << 7);
|
||||
writer.writeByte(b);
|
||||
} while (value != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a variable-length string of 1-byte characters.
|
||||
*/
|
||||
public void write(String s) throws IOException {
|
||||
// 00 = empty string
|
||||
// 0B <length> <char>* = normal string
|
||||
// <length> is encoded as an LEB, and is the byte length of the rest.
|
||||
// <char>* is encoded as UTF8, and is the string content.
|
||||
if (s == null || s.length() == 0)
|
||||
writer.writeByte(0x00);
|
||||
else {
|
||||
writer.writeByte(0x0B);
|
||||
writeULEB128(s.length());
|
||||
writer.writeBytes(s);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a date in Windows ticks (8 bytes).
|
||||
*/
|
||||
public void write(Date date) throws IOException {
|
||||
final long TICKS_AT_EPOCH = 621355968000000000L;
|
||||
final long TICKS_PER_MILLISECOND = 10000;
|
||||
|
||||
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
||||
calendar.setTime(date);
|
||||
long ticks = TICKS_AT_EPOCH + calendar.getTimeInMillis() * TICKS_PER_MILLISECOND;
|
||||
write(ticks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an array of bytes.
|
||||
*/
|
||||
public void write(byte[] b) throws IOException {
|
||||
writer.write(b);
|
||||
}
|
||||
}
|
57
src/itdelatrisu/opsu/replay/LifeFrame.java
Normal file
57
src/itdelatrisu/opsu/replay/LifeFrame.java
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.replay;
|
||||
|
||||
/**
|
||||
* Captures a single life frame.
|
||||
*
|
||||
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
|
||||
*/
|
||||
public class LifeFrame {
|
||||
/** Time. */
|
||||
private int time;
|
||||
|
||||
/** Percentage. */
|
||||
private float percentage;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param time the time
|
||||
* @param percentage the percentage
|
||||
*/
|
||||
public LifeFrame(int time, float percentage) {
|
||||
this.time = time;
|
||||
this.percentage = percentage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the frame time.
|
||||
*/
|
||||
public int getTime() { return time; }
|
||||
|
||||
/**
|
||||
* Returns the frame percentage.
|
||||
*/
|
||||
public float getPercentage() { return percentage; }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("(%d, %.2f)", time, percentage);
|
||||
}
|
||||
}
|
309
src/itdelatrisu/opsu/replay/Replay.java
Normal file
309
src/itdelatrisu/opsu/replay/Replay.java
Normal file
@ -0,0 +1,309 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.replay;
|
||||
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.io.OsuReader;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
|
||||
|
||||
/**
|
||||
* Captures osu! replay data.
|
||||
* https://osu.ppy.sh/wiki/Osr_%28file_format%29
|
||||
*
|
||||
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
|
||||
*/
|
||||
public class Replay {
|
||||
/** The associated file. */
|
||||
private File file;
|
||||
|
||||
/** The associated score data. */
|
||||
// private ScoreData scoreData;
|
||||
|
||||
/** Whether or not the replay data has been loaded from the file. */
|
||||
public boolean loaded = false;
|
||||
|
||||
/** The game mode. */
|
||||
public byte mode;
|
||||
|
||||
/** Game version when the replay was created. */
|
||||
public int version;
|
||||
|
||||
/** Beatmap MD5 hash. */
|
||||
public String beatmapHash;
|
||||
|
||||
/** The player's name. */
|
||||
public String playerName;
|
||||
|
||||
/** Replay MD5 hash. */
|
||||
public String replayHash;
|
||||
|
||||
/** Hit result counts. */
|
||||
public short hit300, hit100, hit50, geki, katu, miss;
|
||||
|
||||
/** The score. */
|
||||
public int score;
|
||||
|
||||
/** The max combo. */
|
||||
public short combo;
|
||||
|
||||
/** Whether or not a full combo was achieved. */
|
||||
public boolean perfect;
|
||||
|
||||
/** Game mod bitmask. */
|
||||
public int mods;
|
||||
|
||||
/** Life frames. */
|
||||
public LifeFrame[] lifeFrames;
|
||||
|
||||
/** The time when the replay was created. */
|
||||
public Date timestamp;
|
||||
|
||||
/** Length of the replay data. */
|
||||
public int replayLength;
|
||||
|
||||
/** Replay frames. */
|
||||
public ReplayFrame[] frames;
|
||||
|
||||
/** Seed. (?) */
|
||||
public int seed;
|
||||
|
||||
/** Seed string. */
|
||||
private static final String SEED_STRING = "-12345";
|
||||
|
||||
/**
|
||||
* Empty constructor.
|
||||
*/
|
||||
public Replay() {}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param file the file to load from
|
||||
*/
|
||||
public Replay(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the replay data.
|
||||
* @throws IOException failure to load the data
|
||||
*/
|
||||
public void load() throws IOException {
|
||||
if (loaded)
|
||||
return;
|
||||
|
||||
OsuReader reader = new OsuReader(file);
|
||||
loadHeader(reader);
|
||||
loadData(reader);
|
||||
reader.close();
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the replay header only.
|
||||
* @throws IOException failure to load the data
|
||||
*/
|
||||
public void loadHeader() throws IOException {
|
||||
OsuReader reader = new OsuReader(file);
|
||||
loadHeader(reader);
|
||||
reader.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the replay header data.
|
||||
* @param reader the associated reader
|
||||
* @throws IOException
|
||||
*/
|
||||
private void loadHeader(OsuReader reader) throws IOException {
|
||||
this.mode = reader.readByte();
|
||||
this.version = reader.readInt();
|
||||
this.beatmapHash = reader.readString();
|
||||
this.playerName = reader.readString();
|
||||
this.replayHash = reader.readString();
|
||||
this.hit300 = reader.readShort();
|
||||
this.hit100 = reader.readShort();
|
||||
this.hit50 = reader.readShort();
|
||||
this.geki = reader.readShort();
|
||||
this.katu = reader.readShort();
|
||||
this.miss = reader.readShort();
|
||||
this.score = reader.readInt();
|
||||
this.combo = reader.readShort();
|
||||
this.perfect = reader.readBoolean();
|
||||
this.mods = reader.readInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the replay data.
|
||||
* @param reader the associated reader
|
||||
* @throws IOException
|
||||
*/
|
||||
private void loadData(OsuReader reader) throws IOException {
|
||||
// life data
|
||||
String[] lifeData = reader.readString().split(",");
|
||||
List<LifeFrame> lifeFrameList = new ArrayList<>(lifeData.length);
|
||||
for (String frame : lifeData) {
|
||||
String[] tokens = frame.split("\\|");
|
||||
if (tokens.length < 2)
|
||||
continue;
|
||||
try {
|
||||
int time = Integer.parseInt(tokens[0]);
|
||||
float percentage = Float.parseFloat(tokens[1]);
|
||||
lifeFrameList.add(new LifeFrame(time, percentage));
|
||||
} catch (NumberFormatException e) {
|
||||
//Log.warn(String.format("Failed to load life frame: '%s'", frame), e);
|
||||
}
|
||||
}
|
||||
this.lifeFrames = lifeFrameList.toArray(new LifeFrame[lifeFrameList.size()]);
|
||||
|
||||
// timestamp
|
||||
this.timestamp = reader.readDate();
|
||||
|
||||
// LZMA-encoded replay data
|
||||
this.replayLength = reader.readInt();
|
||||
if (replayLength > 0) {
|
||||
LZMACompressorInputStream lzma = new LZMACompressorInputStream(reader.getInputStream());
|
||||
String[] replayFrames = Utils.convertStreamToString(lzma).split(",");
|
||||
lzma.close();
|
||||
List<ReplayFrame> replayFrameList = new ArrayList<>(replayFrames.length);
|
||||
int lastTime = 0;
|
||||
for (String frame : replayFrames) {
|
||||
if (frame.isEmpty())
|
||||
continue;
|
||||
String[] tokens = frame.split("\\|");
|
||||
if (tokens.length < 4)
|
||||
continue;
|
||||
try {
|
||||
if (tokens[0].equals(SEED_STRING)) {
|
||||
seed = Integer.parseInt(tokens[3]);
|
||||
continue;
|
||||
}
|
||||
int timeDiff = Integer.parseInt(tokens[0]);
|
||||
int time = timeDiff + lastTime;
|
||||
float x = Float.parseFloat(tokens[1]);
|
||||
float y = Float.parseFloat(tokens[2]);
|
||||
int keys = Integer.parseInt(tokens[3]);
|
||||
replayFrameList.add(new ReplayFrame(timeDiff, time, x, y, keys));
|
||||
lastTime = time;
|
||||
} catch (NumberFormatException e) {
|
||||
//Log.warn(String.format("Failed to parse frame: '%s'", frame), e);
|
||||
}
|
||||
}
|
||||
this.frames = replayFrameList.toArray(new ReplayFrame[replayFrameList.size()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a ScoreData object encapsulating all replay data.
|
||||
* If score data already exists, the existing object will be returned
|
||||
* (i.e. this will not overwrite existing data).
|
||||
* @param beatmap the beatmap
|
||||
* @return the ScoreData object
|
||||
*/
|
||||
/*public ScoreData getScoreData(Beatmap beatmap) {
|
||||
if (scoreData != null)
|
||||
return scoreData;
|
||||
|
||||
scoreData = new ScoreData();
|
||||
scoreData.timestamp = file.lastModified() / 1000L;
|
||||
scoreData.MID = beatmap.beatmapID;
|
||||
scoreData.MSID = beatmap.beatmapSetID;
|
||||
scoreData.title = beatmap.title;
|
||||
scoreData.artist = beatmap.artist;
|
||||
scoreData.creator = beatmap.creator;
|
||||
scoreData.version = beatmap.version;
|
||||
scoreData.hit300 = hit300;
|
||||
scoreData.hit100 = hit100;
|
||||
scoreData.hit50 = hit50;
|
||||
scoreData.geki = geki;
|
||||
scoreData.katu = katu;
|
||||
scoreData.miss = miss;
|
||||
scoreData.score = score;
|
||||
scoreData.combo = combo;
|
||||
scoreData.perfect = perfect;
|
||||
scoreData.mods = mods;
|
||||
scoreData.replayString = getReplayFilename();
|
||||
scoreData.playerName = playerName;
|
||||
return scoreData;
|
||||
}*/
|
||||
|
||||
/**
|
||||
* Returns the file name of where the replay should be saved and loaded,
|
||||
* or null if the required fields are not set.
|
||||
*/
|
||||
public String getReplayFilename() {
|
||||
if (replayHash == null)
|
||||
return null;
|
||||
|
||||
return String.format("%s-%d%d%d%d%d%d",
|
||||
replayHash, hit300, hit100, hit50, geki, katu, miss);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final int LINE_SPLIT = 5;
|
||||
final int MAX_LINES = LINE_SPLIT * 10;
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("File: "); sb.append(file.getName()); sb.append('\n');
|
||||
sb.append("Mode: "); sb.append(mode); sb.append('\n');
|
||||
sb.append("Version: "); sb.append(version); sb.append('\n');
|
||||
sb.append("Beatmap hash: "); sb.append(beatmapHash); sb.append('\n');
|
||||
sb.append("Player name: "); sb.append(playerName); sb.append('\n');
|
||||
sb.append("Replay hash: "); sb.append(replayHash); sb.append('\n');
|
||||
sb.append("Hits: ");
|
||||
sb.append(hit300); sb.append(' ');
|
||||
sb.append(hit100); sb.append(' ');
|
||||
sb.append(hit50); sb.append(' ');
|
||||
sb.append(geki); sb.append(' ');
|
||||
sb.append(katu); sb.append(' ');
|
||||
sb.append(miss); sb.append('\n');
|
||||
sb.append("Score: "); sb.append(score); sb.append('\n');
|
||||
sb.append("Max combo: "); sb.append(combo); sb.append('\n');
|
||||
sb.append("Perfect: "); sb.append(perfect); sb.append('\n');
|
||||
sb.append("Mods: "); sb.append(mods); sb.append('\n');
|
||||
sb.append("Life data ("); sb.append(lifeFrames.length); sb.append(" total):\n");
|
||||
for (int i = 0; i < lifeFrames.length && i < MAX_LINES; i++) {
|
||||
if (i % LINE_SPLIT == 0)
|
||||
sb.append('\t');
|
||||
sb.append(lifeFrames[i]);
|
||||
sb.append((i % LINE_SPLIT == LINE_SPLIT - 1) ? '\n' : ' ');
|
||||
}
|
||||
sb.append('\n');
|
||||
sb.append("Timestamp: "); sb.append(timestamp); sb.append('\n');
|
||||
sb.append("Replay length: "); sb.append(replayLength); sb.append('\n');
|
||||
if (frames != null) {
|
||||
sb.append("Frames ("); sb.append(frames.length); sb.append(" total):\n");
|
||||
for (int i = 0; i < frames.length && i < MAX_LINES; i++) {
|
||||
if (i % LINE_SPLIT == 0)
|
||||
sb.append('\t');
|
||||
sb.append(frames[i]);
|
||||
sb.append((i % LINE_SPLIT == LINE_SPLIT - 1) ? '\n' : ' ');
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append("Seed: "); sb.append(seed); sb.append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
137
src/itdelatrisu/opsu/replay/ReplayFrame.java
Normal file
137
src/itdelatrisu/opsu/replay/ReplayFrame.java
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.replay;
|
||||
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
|
||||
/**
|
||||
* Captures a single replay frame.
|
||||
*
|
||||
* @author smoogipooo (https://github.com/smoogipooo/osu-Replay-API/)
|
||||
*/
|
||||
public class ReplayFrame {
|
||||
/** Key bits. */
|
||||
public static final int
|
||||
KEY_NONE = 0,
|
||||
KEY_M1 = (1 << 0),
|
||||
KEY_M2 = (1 << 1),
|
||||
KEY_K1 = (1 << 2) | (1 << 0),
|
||||
KEY_K2 = (1 << 3) | (1 << 1);
|
||||
|
||||
/** Time, in milliseconds, since the previous action. */
|
||||
private int timeDiff;
|
||||
|
||||
/** Time, in milliseconds. */
|
||||
private int time;
|
||||
|
||||
/** Cursor coordinates (in OsuPixels). */
|
||||
private float x, y;
|
||||
|
||||
/** Keys pressed (bitmask). */
|
||||
private int keys;
|
||||
|
||||
/**
|
||||
* Returns the start frame.
|
||||
* @param t the value for the {@code time} and {@code timeDiff} fields
|
||||
*/
|
||||
public static ReplayFrame getStartFrame(int t) {
|
||||
return new ReplayFrame(t, t, 256, -500, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param timeDiff time since the previous action (in ms)
|
||||
* @param time time (in ms)
|
||||
* @param x cursor x coordinate [0, 512]
|
||||
* @param y cursor y coordinate [0, 384]
|
||||
* @param keys keys pressed (bitmask)
|
||||
*/
|
||||
public ReplayFrame(int timeDiff, int time, float x, float y, int keys) {
|
||||
this.timeDiff = timeDiff;
|
||||
this.time = time;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the frame time, in milliseconds.
|
||||
*/
|
||||
public int getTime() { return time; }
|
||||
|
||||
/**
|
||||
* Returns the time since the previous action, in milliseconds.
|
||||
*/
|
||||
public int getTimeDiff() { return timeDiff; }
|
||||
|
||||
/**
|
||||
* Sets the time since the previous action, in milliseconds.
|
||||
*/
|
||||
public void setTimeDiff(int diff) { this.timeDiff = diff; }
|
||||
|
||||
/**
|
||||
* Returns the raw cursor x coordinate.
|
||||
*/
|
||||
public float getX() { return x; }
|
||||
|
||||
/**
|
||||
* Returns the raw cursor y coordinate.
|
||||
*/
|
||||
public float getY() { return y; }
|
||||
|
||||
/**
|
||||
* Returns the scaled cursor x coordinate.
|
||||
*/
|
||||
public int getScaledX() { return (int) (x * HitObject.getXMultiplier() + HitObject.getXOffset()); }
|
||||
|
||||
/**
|
||||
* Returns the scaled cursor y coordinate.
|
||||
*/
|
||||
public int getScaledY() { return (int) (y * HitObject.getYMultiplier() + HitObject.getYOffset()); }
|
||||
|
||||
/**
|
||||
* Returns the keys pressed (KEY_* bitmask).
|
||||
*/
|
||||
public int getKeys() { return keys; }
|
||||
|
||||
/**
|
||||
* Returns whether or not a key is pressed.
|
||||
*/
|
||||
public boolean isKeyPressed() { return (keys != KEY_NONE); }
|
||||
|
||||
public String keyAsString() {
|
||||
// StringBuilder sb = new StringBuilder(10);
|
||||
// if ((keys & KEY_K1) != 0) sb.append(" K1");
|
||||
// if ((keys & KEY_K2) != 0) sb.append(" K2");
|
||||
// if ((keys & KEY_M1) != 0) sb.append(" M1");
|
||||
// if ((keys & KEY_M2) != 0) sb.append(" M2");
|
||||
//
|
||||
// return sb.toString().trim();
|
||||
if ((keys & KEY_K1) != 0) return "K1";
|
||||
if ((keys & KEY_K2) != 0) return "K2";
|
||||
if ((keys & KEY_M1) != 0) return "M1";
|
||||
if ((keys & KEY_M2) != 0) return "M2";
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("(%d, [%.2f, %.2f], %d)", time, x, y, keys);
|
||||
}
|
||||
}
|
373
src/itdelatrisu/opsu/skins/Skin.java
Normal file
373
src/itdelatrisu/opsu/skins/Skin.java
Normal file
@ -0,0 +1,373 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.skins;
|
||||
|
||||
import java.io.File;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* Skin configuration (skin.ini).
|
||||
*/
|
||||
public class Skin {
|
||||
/** The default skin name. */
|
||||
public static final String DEFAULT_SKIN_NAME = "Default";
|
||||
|
||||
/** Slider styles. */
|
||||
public static final byte
|
||||
STYLE_PEPPYSLIDER = 1, // fallback
|
||||
STYLE_MMSLIDER = 2, // default (requires OpenGL 3.0)
|
||||
STYLE_TOONSLIDER = 3, // not implemented
|
||||
STYLE_OPENGLSLIDER = 4; // not implemented
|
||||
|
||||
/** The latest skin version. */
|
||||
protected static final int LATEST_VERSION = 2;
|
||||
|
||||
/** The default list of combos with combo sounds. */
|
||||
private static final int[] DEFAULT_CUSTOM_COMBO_BURST_SOUNDS = { 50, 75, 100, 200, 300 };
|
||||
|
||||
/** The default combo colors (used when a beatmap does not provide custom colors). */
|
||||
private static final Color[] DEFAULT_COMBO = {
|
||||
Color.rgb(255, 192, 0),
|
||||
Color.rgb(0, 202, 0),
|
||||
Color.rgb(18, 124, 255),
|
||||
Color.rgb(242, 24, 57)
|
||||
};
|
||||
|
||||
/** The default menu visualization bar color. */
|
||||
private static final Color DEFAULT_MENU_GLOW = Color.rgb(0, 78, 155);
|
||||
|
||||
/** The default slider border color. */
|
||||
private static final Color DEFAULT_SLIDER_BORDER = Color.rgb(255, 255, 255);
|
||||
|
||||
/** The default slider ball color. */
|
||||
private static final Color DEFAULT_SLIDER_BALL = Color.rgb(2, 170, 255);
|
||||
|
||||
/** The default spinner approach circle color. */
|
||||
private static final Color DEFAULT_SPINNER_APPROACH_CIRCLE = Color.rgb(77, 139, 217);
|
||||
|
||||
/** The default color of the active text in the song selection menu. */
|
||||
private static final Color DEFAULT_SONG_SELECT_ACTIVE_TEXT = Color.rgb(255, 255, 255);
|
||||
|
||||
/** The default color of the inactive text in the song selection menu. */
|
||||
private static final Color DEFAULT_SONG_SELECT_INACTIVE_TEXT = Color.rgb(178, 178, 178);
|
||||
|
||||
/** The default color of the stars that fall from the cursor during breaks. */
|
||||
private static final Color DEFAULT_STAR_BREAK_ADDITIVE = Color.rgb(255, 182, 193);
|
||||
|
||||
/** The skin directory. */
|
||||
private File dir;
|
||||
|
||||
/**
|
||||
* [General]
|
||||
*/
|
||||
|
||||
/** The name of the skin. */
|
||||
protected String name = "opsu! Default Skin";
|
||||
|
||||
/** The skin author. */
|
||||
protected String author = "[various authors]";
|
||||
|
||||
/** The skin version. */
|
||||
protected int version = LATEST_VERSION;
|
||||
|
||||
/** When a slider has a reverse, should the ball sprite flip horizontally? */
|
||||
protected boolean sliderBallFlip = false;
|
||||
|
||||
/** Should the cursor sprite rotate constantly? */
|
||||
protected boolean cursorRotate = true;
|
||||
|
||||
/** Should the cursor expand when clicked? */
|
||||
protected boolean cursorExpand = true;
|
||||
|
||||
/** Should the cursor have an origin at the center of the image? (if not, the top-left corner is used) */
|
||||
protected boolean cursorCentre = true;
|
||||
|
||||
/** The number of frames in the slider ball animation. */
|
||||
protected int sliderBallFrames = 10;
|
||||
|
||||
/** Should the hitcircleoverlay sprite be drawn above the hircircle combo number? */
|
||||
protected boolean hitCircleOverlayAboveNumber = true;
|
||||
|
||||
/** Should the sound frequency be modulated depending on the spinner score? */
|
||||
protected boolean spinnerFrequencyModulate = false;
|
||||
|
||||
/** Should the normal hitsound always be played? */
|
||||
protected boolean layeredHitSounds = true;
|
||||
|
||||
/** Should the spinner fade the playfield? */
|
||||
protected boolean spinnerFadePlayfield = true;
|
||||
|
||||
/** Should the last spinner bar blink? */
|
||||
protected boolean spinnerNoBlink = false;
|
||||
|
||||
/** Should the slider combo color tint the slider ball? */
|
||||
protected boolean allowSliderBallTint = false;
|
||||
|
||||
/** The FPS of animations. */
|
||||
protected int animationFramerate = -1;
|
||||
|
||||
/** Should the cursor trail sprite rotate constantly? */
|
||||
protected boolean cursorTrailRotate = false;
|
||||
|
||||
/** List of combos with combo sounds. */
|
||||
protected int[] customComboBurstSounds = DEFAULT_CUSTOM_COMBO_BURST_SOUNDS;
|
||||
|
||||
/** Should the combo burst sprites appear in random order? */
|
||||
protected boolean comboBurstRandom = false;
|
||||
|
||||
/** The slider style to use (see STYLE_* constants). */
|
||||
protected byte sliderStyle = STYLE_MMSLIDER;
|
||||
|
||||
/**
|
||||
* [Colours]
|
||||
*/
|
||||
|
||||
/** Combo colors (max 8). */
|
||||
protected Color[] combo = DEFAULT_COMBO;
|
||||
|
||||
/** The menu visualization bar color. */
|
||||
protected Color menuGlow = DEFAULT_MENU_GLOW;
|
||||
|
||||
/** The color for the slider border. */
|
||||
protected Color sliderBorder = DEFAULT_SLIDER_BORDER;
|
||||
|
||||
/** The slider ball color. */
|
||||
protected Color sliderBall = DEFAULT_SLIDER_BALL;
|
||||
|
||||
/** The spinner approach circle color. */
|
||||
protected Color spinnerApproachCircle = DEFAULT_SPINNER_APPROACH_CIRCLE;
|
||||
|
||||
/** The color of text in the currently active group in song selection. */
|
||||
protected Color songSelectActiveText = DEFAULT_SONG_SELECT_ACTIVE_TEXT;
|
||||
|
||||
/** The color of text in the inactive groups in song selection. */
|
||||
protected Color songSelectInactiveText = DEFAULT_SONG_SELECT_INACTIVE_TEXT;
|
||||
|
||||
/** The color of the stars that fall from the cursor (star2 sprite) in breaks. */
|
||||
protected Color starBreakAdditive = DEFAULT_STAR_BREAK_ADDITIVE;
|
||||
|
||||
/**
|
||||
* [Fonts]
|
||||
*/
|
||||
|
||||
/** The prefix for the hitcircle font sprites. */
|
||||
protected String hitCirclePrefix = "default";
|
||||
|
||||
/** How much should the hitcircle font sprites overlap? */
|
||||
protected int hitCircleOverlap = -2;
|
||||
|
||||
/** The prefix for the score font sprites. */
|
||||
protected String scorePrefix = "score";
|
||||
|
||||
/** How much should the score font sprites overlap? */
|
||||
protected int scoreOverlap = 0;
|
||||
|
||||
/** The prefix for the combo font sprites. */
|
||||
protected String comboPrefix = "score";
|
||||
|
||||
/** How much should the combo font sprites overlap? */
|
||||
protected int comboOverlap = 0;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param dir the skin directory
|
||||
*/
|
||||
public Skin(File dir) {
|
||||
this.dir = dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the skin directory.
|
||||
*/
|
||||
public File getDirectory() { return dir; }
|
||||
|
||||
/**
|
||||
* Returns the name of the skin.
|
||||
*/
|
||||
public String getName() { return name; }
|
||||
|
||||
/**
|
||||
* Returns the skin author.
|
||||
*/
|
||||
public String getAuthor() { return author; }
|
||||
|
||||
/**
|
||||
* Returns the skin version.
|
||||
*/
|
||||
public int getVersion() { return version; }
|
||||
|
||||
/**
|
||||
* Returns whether the slider ball should be flipped horizontally during a reverse.
|
||||
*/
|
||||
public boolean isSliderBallFlipped() { return sliderBallFlip; }
|
||||
|
||||
/**
|
||||
* Returns whether the cursor should rotate.
|
||||
*/
|
||||
public boolean isCursorRotated() { return cursorRotate; }
|
||||
|
||||
/**
|
||||
* Returns whether the cursor should expand when clicked.
|
||||
*/
|
||||
public boolean isCursorExpanded() { return cursorExpand; }
|
||||
|
||||
/**
|
||||
* Returns whether the cursor should have an origin in the center.
|
||||
* @return {@code true} if center, {@code false} if top-left corner
|
||||
*/
|
||||
public boolean isCursorCentered() { return cursorCentre; }
|
||||
|
||||
/**
|
||||
* Returns the number of frames in the slider ball animation.
|
||||
*/
|
||||
public int getSliderBallFrames() { return sliderBallFrames; }
|
||||
|
||||
/**
|
||||
* Returns whether the hit circle overlay should be drawn above the combo number.
|
||||
*/
|
||||
public boolean isHitCircleOverlayAboveNumber() { return hitCircleOverlayAboveNumber; }
|
||||
|
||||
/**
|
||||
* Returns whether the sound frequency should be modulated depending on the spinner score.
|
||||
*/
|
||||
public boolean isSpinnerFrequencyModulated() { return spinnerFrequencyModulate; }
|
||||
|
||||
/**
|
||||
* Returns whether the normal hitsound should always be played (and layered on other sounds).
|
||||
*/
|
||||
public boolean isLayeredHitSounds() { return layeredHitSounds; }
|
||||
|
||||
/**
|
||||
* Returns whether the playfield should fade for spinners.
|
||||
*/
|
||||
public boolean isSpinnerFadePlayfield() { return spinnerFadePlayfield; }
|
||||
|
||||
/**
|
||||
* Returns whether the last spinner bar should blink.
|
||||
*/
|
||||
public boolean isSpinnerNoBlink() { return spinnerNoBlink; }
|
||||
|
||||
/**
|
||||
* Returns whether the slider ball should be tinted with the slider combo color.
|
||||
*/
|
||||
public boolean isAllowSliderBallTint() { return allowSliderBallTint; }
|
||||
|
||||
/**
|
||||
* Returns the frame rate of animations.
|
||||
* @return the FPS, or {@code -1} (TODO)
|
||||
*/
|
||||
public int getAnimationFramerate() { return animationFramerate; }
|
||||
|
||||
/**
|
||||
* Returns whether the cursor trail should rotate.
|
||||
*/
|
||||
public boolean isCursorTrailRotated() { return cursorTrailRotate; }
|
||||
|
||||
/**
|
||||
* Returns a list of combos with combo sounds.
|
||||
*/
|
||||
public int[] getCustomComboBurstSounds() { return customComboBurstSounds; }
|
||||
|
||||
/**
|
||||
* Returns whether combo bursts should appear in random order.
|
||||
*/
|
||||
public boolean isComboBurstRandom() { return comboBurstRandom; }
|
||||
|
||||
/**
|
||||
* Returns the slider style.
|
||||
* <ul>
|
||||
* <li>1: peppysliders (segmented)
|
||||
* <li>2: mmsliders (smooth)
|
||||
* <li>3: toonsliders (smooth, with steps instead of gradient)
|
||||
* <li>4: legacy OpenGL-only sliders
|
||||
* </ul>
|
||||
* @return the style (see STYLE_* constants)
|
||||
*/
|
||||
public byte getSliderStyle() { return sliderStyle; }
|
||||
|
||||
/**
|
||||
* Returns the list of combo colors (max 8).
|
||||
*/
|
||||
public Color[] getComboColors() { return combo; }
|
||||
|
||||
/**
|
||||
* Returns the menu visualization bar color.
|
||||
*/
|
||||
public Color getMenuGlowColor() { return menuGlow; }
|
||||
|
||||
/**
|
||||
* Returns the slider border color.
|
||||
*/
|
||||
public Color getSliderBorderColor() { return sliderBorder; }
|
||||
|
||||
/**
|
||||
* Returns the slider ball color.
|
||||
*/
|
||||
public Color getSliderBallColor() { return sliderBall; }
|
||||
|
||||
/**
|
||||
* Returns the spinner approach circle color.
|
||||
*/
|
||||
public Color getSpinnerApproachCircleColor() { return spinnerApproachCircle; }
|
||||
|
||||
/**
|
||||
* Returns the color of the active text in the song selection menu.
|
||||
*/
|
||||
public Color getSongSelectActiveTextColor() { return songSelectActiveText; }
|
||||
|
||||
/**
|
||||
* Returns the color of the inactive text in the song selection menu.
|
||||
*/
|
||||
public Color getSongSelectInactiveTextColor() { return songSelectInactiveText; }
|
||||
|
||||
/**
|
||||
* Returns the color of the stars that fall from the cursor during breaks.
|
||||
*/
|
||||
public Color getStarBreakAdditiveColor() { return starBreakAdditive; }
|
||||
|
||||
/**
|
||||
* Returns the prefix for the hit circle font sprites.
|
||||
*/
|
||||
public String getHitCircleFontPrefix() { return hitCirclePrefix; }
|
||||
|
||||
/**
|
||||
* Returns the amount of overlap between the hit circle font sprites.
|
||||
*/
|
||||
public int getHitCircleFontOverlap() { return hitCircleOverlap; }
|
||||
|
||||
/**
|
||||
* Returns the prefix for the score font sprites.
|
||||
*/
|
||||
public String getScoreFontPrefix() { return scorePrefix; }
|
||||
|
||||
/**
|
||||
* Returns the amount of overlap between the score font sprites.
|
||||
*/
|
||||
public int getScoreFontOverlap() { return scoreOverlap; }
|
||||
|
||||
/**
|
||||
* Returns the prefix for the combo font sprites.
|
||||
*/
|
||||
public String getComboFontPrefix() { return comboPrefix; }
|
||||
|
||||
/**
|
||||
* Returns the amount of overlap between the combo font sprites.
|
||||
*/
|
||||
public int getComboFontOverlap() { return comboOverlap; }
|
||||
}
|
297
src/itdelatrisu/opsu/skins/SkinLoader.java
Normal file
297
src/itdelatrisu/opsu/skins/SkinLoader.java
Normal file
@ -0,0 +1,297 @@
|
||||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu.skins;
|
||||
|
||||
import itdelatrisu.opsu.Log;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* Loads skin configuration files.
|
||||
*/
|
||||
public class SkinLoader {
|
||||
/** Name of the skin configuration file. */
|
||||
private static final String CONFIG_FILENAME = "skin.ini";
|
||||
|
||||
// This class should not be instantiated.
|
||||
private SkinLoader() {}
|
||||
|
||||
/**
|
||||
* Returns a list of all subdirectories in the Skins directory.
|
||||
* @param root the root directory (search has depth 1)
|
||||
* @return an array of skin directories
|
||||
*/
|
||||
public static File[] getSkinDirectories(File root) {
|
||||
ArrayList<File> dirs = new ArrayList<File>();
|
||||
for (File dir : root.listFiles()) {
|
||||
if (dir.isDirectory())
|
||||
dirs.add(dir);
|
||||
}
|
||||
return dirs.toArray(new File[dirs.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a skin configuration file.
|
||||
* If 'skin.ini' is not found, or if any fields are not specified, the
|
||||
* default values will be used.
|
||||
* @param dir the skin directory
|
||||
* @return the loaded skin
|
||||
*/
|
||||
public static Skin loadSkin(File dir) {
|
||||
File skinFile = new File(dir, CONFIG_FILENAME);
|
||||
Skin skin = new Skin(dir);
|
||||
if (!skinFile.isFile()) // missing skin.ini
|
||||
return skin;
|
||||
|
||||
try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(skinFile), "UTF-8"))) {
|
||||
String line = in.readLine();
|
||||
String tokens[] = null;
|
||||
while (line != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line)) {
|
||||
line = in.readLine();
|
||||
continue;
|
||||
}
|
||||
switch (line) {
|
||||
case "[General]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
if ((tokens = tokenize(line)) == null)
|
||||
continue;
|
||||
try {
|
||||
switch (tokens[0]) {
|
||||
case "Name":
|
||||
skin.name = tokens[1];
|
||||
break;
|
||||
case "Author":
|
||||
skin.author = tokens[1];
|
||||
break;
|
||||
case "Version":
|
||||
if (tokens[1].equalsIgnoreCase("latest"))
|
||||
skin.version = Skin.LATEST_VERSION;
|
||||
else
|
||||
skin.version = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "SliderBallFlip":
|
||||
skin.sliderBallFlip = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "CursorRotate":
|
||||
skin.cursorRotate = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "CursorExpand":
|
||||
skin.cursorExpand = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "CursorCentre":
|
||||
skin.cursorCentre = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "SliderBallFrames":
|
||||
skin.sliderBallFrames = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "HitCircleOverlayAboveNumber":
|
||||
skin.hitCircleOverlayAboveNumber = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "spinnerFrequencyModulate":
|
||||
skin.spinnerFrequencyModulate = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "LayeredHitSounds":
|
||||
skin.layeredHitSounds = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "SpinnerFadePlayfield":
|
||||
skin.spinnerFadePlayfield = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "SpinnerNoBlink":
|
||||
skin.spinnerNoBlink = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "AllowSliderBallTint":
|
||||
skin.allowSliderBallTint = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "AnimationFramerate":
|
||||
skin.animationFramerate = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "CursorTrailRotate":
|
||||
skin.cursorTrailRotate = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "CustomComboBurstSounds":
|
||||
String[] split = tokens[1].split(",");
|
||||
int[] customComboBurstSounds = new int[split.length];
|
||||
for (int i = 0; i < split.length; i++)
|
||||
customComboBurstSounds[i] = Integer.parseInt(split[i]);
|
||||
skin.customComboBurstSounds = customComboBurstSounds;
|
||||
break;
|
||||
case "ComboBurstRandom":
|
||||
skin.comboBurstRandom = Utils.parseBoolean(tokens[1]);
|
||||
break;
|
||||
case "SliderStyle":
|
||||
skin.sliderStyle = Byte.parseByte(tokens[1]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read line '%s' for file '%s'.",
|
||||
line, skinFile.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "[Colours]":
|
||||
LinkedList<Color> colors = new LinkedList<Color>();
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
if ((tokens = tokenize(line)) == null)
|
||||
continue;
|
||||
try {
|
||||
String[] rgb = tokens[1].split(",");
|
||||
Color color = Color.rgb(
|
||||
Integer.parseInt(rgb[0]),
|
||||
Integer.parseInt(rgb[1]),
|
||||
Integer.parseInt(rgb[2])
|
||||
);
|
||||
switch (tokens[0]) {
|
||||
case "Combo1":
|
||||
case "Combo2":
|
||||
case "Combo3":
|
||||
case "Combo4":
|
||||
case "Combo5":
|
||||
case "Combo6":
|
||||
case "Combo7":
|
||||
case "Combo8":
|
||||
colors.add(color);
|
||||
break;
|
||||
case "MenuGlow":
|
||||
skin.menuGlow = color;
|
||||
break;
|
||||
case "SliderBorder":
|
||||
skin.sliderBorder = color;
|
||||
break;
|
||||
case "SliderBall":
|
||||
skin.sliderBall = color;
|
||||
break;
|
||||
case "SpinnerApproachCircle":
|
||||
skin.spinnerApproachCircle = color;
|
||||
break;
|
||||
case "SongSelectActiveText":
|
||||
skin.songSelectActiveText = color;
|
||||
break;
|
||||
case "SongSelectInactiveText":
|
||||
skin.songSelectInactiveText = color;
|
||||
break;
|
||||
case "StarBreakAdditive":
|
||||
skin.starBreakAdditive = color;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read color '%s' for file '%s'.",
|
||||
line, skinFile.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
if (!colors.isEmpty())
|
||||
skin.combo = colors.toArray(new Color[colors.size()]);
|
||||
break;
|
||||
case "[Fonts]":
|
||||
while ((line = in.readLine()) != null) {
|
||||
line = line.trim();
|
||||
if (!isValidLine(line))
|
||||
continue;
|
||||
if (line.charAt(0) == '[')
|
||||
break;
|
||||
if ((tokens = tokenize(line)) == null)
|
||||
continue;
|
||||
try {
|
||||
switch (tokens[0]) {
|
||||
case "HitCirclePrefix":
|
||||
skin.hitCirclePrefix = tokens[1];
|
||||
break;
|
||||
case "HitCircleOverlap":
|
||||
skin.hitCircleOverlap = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "ScorePrefix":
|
||||
skin.scorePrefix = tokens[1];
|
||||
break;
|
||||
case "ScoreOverlap":
|
||||
skin.scoreOverlap = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
case "ComboPrefix":
|
||||
skin.comboPrefix = tokens[1];
|
||||
break;
|
||||
case "ComboOverlap":
|
||||
skin.comboOverlap = Integer.parseInt(tokens[1]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read color '%s' for file '%s'.",
|
||||
line, skinFile.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
line = in.readLine();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.error(String.format("Failed to read file '%s'.", skinFile.getAbsolutePath()), e, false);
|
||||
}
|
||||
|
||||
return skin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns false if the line is too short or commented.
|
||||
*/
|
||||
private static boolean isValidLine(String line) {
|
||||
return (line.length() > 1 && !line.startsWith("//"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits line into two strings: tag, value.
|
||||
* If no ':' character is present, null will be returned.
|
||||
*/
|
||||
private static String[] tokenize(String line) {
|
||||
int index = line.indexOf(':');
|
||||
if (index == -1) {
|
||||
Log.debug(String.format("Failed to tokenize line: '%s'.", line));
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] tokens = new String[2];
|
||||
tokens[0] = line.substring(0, index).trim();
|
||||
tokens[1] = line.substring(index + 1).trim();
|
||||
return tokens;
|
||||
}
|
||||
}
|
BIN
xz-1.5.jar
Normal file
BIN
xz-1.5.jar
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user