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