Merge remote-tracking branch 'org/master' into KinecticScrolling

Conflicts:
	src/itdelatrisu/opsu/ScoreData.java
	src/itdelatrisu/opsu/downloads/DownloadNode.java
	src/itdelatrisu/opsu/states/DownloadsMenu.java
	src/itdelatrisu/opsu/states/SongMenu.java
This commit is contained in:
fd
2015-09-14 19:32:18 -04:00
102 changed files with 6168 additions and 1857 deletions

8
.gitignore vendored
View File

@@ -15,11 +15,17 @@
.metadata
.classpath
.project
.externalToolBuilders/
# IntelliJ
.idea/
*.iml
*.iws
*.ipr
# Gradle
.gradle
build/
Thumbs.db
/target
/target

View File

@@ -1,16 +1,18 @@
# [opsu!](http://itdelatrisu.github.io/opsu/)
**opsu!** is an unofficial open-source client for [osu!](https://osu.ppy.sh/),
a rhythm game based on popular commercial games such as *Ouendan* and
*Elite Beat Agents*. It is written in Java using [Slick2D](http://slick.ninjacave.com/)
and [LWJGL](http://lwjgl.org/), wrappers around the OpenGL and OpenAL libraries.
**opsu!** is an unofficial open-source client for the rhythm game
[osu!](https://osu.ppy.sh/). It is written in Java using
[Slick2D](http://slick.ninjacave.com/) and [LWJGL](http://lwjgl.org/),
wrappers around the OpenGL and OpenAL libraries.
opsu! runs on Windows, OS X, and Linux platforms. A [libGDX port](https://github.com/fluddokt/opsu)
additionally supports Android devices.
opsu! runs on Windows, OS X, and Linux platforms.
A [libGDX port](https://github.com/fluddokt/opsu) additionally supports Android
devices.
## Getting Started
Precompiled binaries for opsu! can be found on the
[releases](https://github.com/itdelatrisu/opsu/releases) page, with the latest
builds at the top. APK releases can be found [here](https://github.com/fluddokt/opsu/releases).
builds at the top. APK releases can be found
[here](https://github.com/fluddokt/opsu/releases).
### Java Setup
The Java Runtime Environment (JRE) must be installed in order to run opsu!.
@@ -19,34 +21,70 @@ The download page is located [here](https://www.java.com/en/download/).
### Beatmaps
opsu! requires beatmaps to run, which are available for download on the
[osu! website](https://osu.ppy.sh/p/beatmaplist) and mirror sites such as
[osu!Mirror](https://osu.yas-online.net/) or [Bloodcat](http://bloodcat.com/osu/).
[osu!Mirror](https://osu.yas-online.net/) and [Bloodcat](http://bloodcat.com/osu/).
Beatmaps can also be downloaded directly through opsu! in the downloads menu.
If osu! is already installed, this application will attempt to load songs
directly from the osu! program folder. Otherwise, place songs in the generated
`Songs` folder or set the `BeatmapDirectory` value in the generated
configuration file to the path of the root song directory.
If osu! is already installed, this application will attempt to load beatmaps
directly from the osu! program folder. Otherwise, place beatmaps in the
generated `Songs` folder or set the "BeatmapDirectory" value in the generated
configuration file to the path of the root beatmap directory.
Note that beatmaps are typically delivered as OSZ files. These can be extracted
with any ZIP tool, and opsu! will automatically extract them into the songs
with any ZIP tool, and opsu! will automatically extract them into the beatmap
folder if placed in the `SongPacks` directory.
### First Run
The `Music Offset` value will likely need to be adjusted when playing for the
The "Music Offset" value will likely need to be adjusted when playing for the
first time, or whenever hit objects are out of sync with the music. This and
other game options can be accessed by clicking the "Other Options" button in
the song menu.
## Building
opsu! is distributed as a Maven project.
### Directory Structure
The following files and folders will be created by opsu! as needed:
* `.opsu.cfg`: The configuration file. Most (but not all) of the settings can
be changed through the options menu.
* `.opsu.db`: The beatmap cache database.
* `.opsu_scores.db`: The scores database.
* `.opsu.log`: The error log. All critical errors displayed in-game are also
logged to this file, and other warnings not shown are logged as well.
* `Songs/`: The beatmap directory (not used if an osu! installation is detected).
The parser searches all of its subdirectories for .osu files to load.
* `SongPacks/`: The beatmap pack directory. The unpacker extracts all .osz
files within this directory to the beatmap directory.
* `Skins/`: The skins directory. Each skin must be placed in a folder within
this directory. Any game resource (in `res/`) can be skinned by placing a
file with the same name in a skin folder. Skins can be selected in the
options menu.
* `Screenshots/`: The screenshot directory. Screenshots can be taken by
pressing the F12 key.
* `Replays/`: The replay directory. Replays of each completed game are saved
as .osr files, and can be viewed at a later time or shared with others.
* `ReplayImport/`: The replay import directory. The importer moves all .osr
files within this directory to the replay directory and saves the scores in
the scores database. Replays can be imported from osu! as well as opsu!.
* `Natives/`: The native libraries directory.
* To run the project, execute the Maven goal `compile exec:exec`.
* To create a single executable JAR file, execute the Maven goal
`install -Djar`. This will link the LWJGL native libraries using a
[modified version](https://github.com/itdelatrisu/JarSplicePlus) of
[JarSplice](http://ninjacave.com/jarsplice), which is included in the
`tools` directory in both its original and modified forms. The resulting
file will be located in `target/opsu-${version}-runnable.jar`.
## Building
opsu! is distributed as both a [Maven](https://maven.apache.org/) and
[Gradle](https://gradle.org/) project.
### Maven
Maven builds are built to the `target` directory.
* To run the project, execute the Maven goal `compile`.
* To create a single executable jar, execute the Maven goal `package -Djar`.
This will compile a jar to `target/opsu-${version}.jar` with the libraries,
resources and natives packed inside the jar. Setting the "XDG" property
(`-DXDG=true`) will make the application use XDG folders under Unix-like
operating systems.
### Gradle
Gradle builds are built to the `build` directory.
* To run the project, execute the Gradle task `run`.
* To create a single executable jar, execute the Gradle task `jar`.
This will compile a jar to `build/libs/opsu-${version}.jar` with the libraries,
resources and natives packed inside the jar. Setting the "XDG" property
(`-PXDG=true`) will make the application use XDG folders under Unix-like
operating systems.
## Credits
This software was created by Jeffrey Han

109
build.gradle Normal file
View File

@@ -0,0 +1,109 @@
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'application'
import org.apache.tools.ant.filters.*
group = 'itdelatrisu'
version = '0.11.0'
mainClassName = 'itdelatrisu.opsu.Opsu'
buildDir = new File(rootProject.projectDir, "build/")
def useXDG = 'false'
if (hasProperty('XDG')) {
useXDG = XDG
}
sourceCompatibility = 1.7
targetCompatibility = 1.7
sourceSets {
main {
java {
srcDir 'src'
}
}
}
repositories {
mavenCentral()
}
dependencies {
compile('org.lwjgl.lwjgl:lwjgl:2.9.3') {
exclude group: 'net.java.jinput', module: 'jinput'
}
compile('org.slick2d:slick2d-core:1.0.1') {
exclude group: 'org.lwjgl.lwjgl', module: 'lwjgl'
}
compile 'org.jcraft:jorbis:0.0.17'
compile 'net.lingala.zip4j:zip4j:1.3.2'
compile 'com.googlecode.soundlibs:jlayer:1.0.1-1'
compile 'com.googlecode.soundlibs:mp3spi:1.9.5-1'
compile 'com.googlecode.soundlibs:tritonus-share:0.3.7-2'
compile 'org.xerial:sqlite-jdbc:3.8.6'
compile 'org.json:json:20140107'
compile 'net.java.dev.jna:jna:4.1.0'
compile 'net.java.dev.jna:jna-platform:4.1.0'
compile 'org.apache.maven:maven-artifact:3.3.3'
compile 'org.apache.commons:commons-compress:1.9'
compile 'org.tukaani:xz:1.5'
compile 'com.github.jponge:lzma-java:1.3'
}
def nativePlatforms = ['windows', 'linux', 'osx']
nativePlatforms.each { platform -> //noinspection GroovyAssignabilityCheck
task "${platform}Natives" {
def outputDir = "${buildDir}/natives/"
inputs.files(configurations.compile)
outputs.dir(outputDir)
doLast {
copy {
def artifacts = configurations.compile.resolvedConfiguration.resolvedArtifacts
.findAll { it.classifier == "natives-$platform" }
artifacts.each {
from zipTree(it.file)
}
into outputDir
}
}
}
}
processResources {
from 'res'
exclude '**/Thumbs.db'
filesMatching('version') {
expand(version: project.version, timestamp: new Date().format("yyyy-MM-dd HH:mm"))
}
}
task unpackNatives {
description "Copies native libraries to the build directory."
dependsOn nativePlatforms.collect { "${it}Natives" }.findAll { tasks[it] }
}
jar {
manifest {
attributes 'Implementation-Title': 'opsu!',
'Implementation-Version': version,
'Main-Class': mainClassName,
'Use-XDG': useXDG
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
baseName = "opsu"
from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } }
exclude '**/Thumbs.db'
outputs.upToDateWhen { false }
}
run {
dependsOn 'unpackNatives'
}

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Tue Aug 25 19:45:21 BST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.3-all.zip

164
gradlew vendored Normal file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

90
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

76
pom.xml
View File

@@ -1,12 +1,15 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>itdelatrisu</groupId>
<artifactId>opsu</artifactId>
<version>0.9.0</version>
<version>0.11.0</version>
<properties>
<version>${project.version}</version>
<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
<mainClassName>itdelatrisu.opsu.Opsu</mainClassName>
<XDG>false</XDG>
</properties>
<build>
<sourceDirectory>src</sourceDirectory>
@@ -67,30 +70,9 @@
<skip>${jar}</skip>
<executable>java</executable>
<arguments>
<argument>-Djava.library.path=${project.build.directory}/natives</argument>
<argument>-cp</argument>
<classpath />
<argument>itdelatrisu.opsu.Opsu</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>jarsplice</id>
<phase>install</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>java</executable>
<workingDirectory>${basedir}/target</workingDirectory>
<arguments>
<argument>-jar</argument>
<argument>-Dinput=opsu-${project.version}.jar</argument>
<argument>-Dmain=itdelatrisu.opsu.Opsu</argument>
<argument>-Doutput=opsu-${project.version}-runnable.jar</argument>
<!-- uncomment the line below to use XDG base directories in Unix -->
<!--<argument>-Dparams=-DXDG=true</argument>-->
<argument>${basedir}/tools/JarSplicePlus.jar</argument>
<argument>${mainClassName}</argument>
</arguments>
</configuration>
</execution>
@@ -98,15 +80,8 @@
</plugin>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<version>2.4.1</version>
<configuration>
<!--
<artifactSet>
<excludes>
<exclude>*:*:natives*</exclude>
</excludes>
</artifactSet>
-->
<filters>
<filter>
<!-- Overwritten classes -->
@@ -124,6 +99,14 @@
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Main-Class>${mainClassName}</Main-Class>
<Use-XDG>${XDG}</Use-XDG>
</manifestEntries>
</transformer>
</transformers>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
<executions>
@@ -142,12 +125,24 @@
<dependency>
<groupId>org.lwjgl.lwjgl</groupId>
<artifactId>lwjgl</artifactId>
<version>2.9.1</version>
<version>2.9.3</version>
<exclusions>
<exclusion>
<groupId>net.java.jinput</groupId>
<artifactId>jinput</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slick2d</groupId>
<artifactId>slick2d-core</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
<exclusions>
<exclusion>
<groupId>org.lwjgl.lwjgl</groupId>
<artifactId>lwjgl</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jcraft</groupId>
@@ -197,17 +192,22 @@
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-artifact</artifactId>
<version>3.0.3</version>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.8</version>
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.tukaani</groupId>
<artifactId>xz</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>com.github.jponge</groupId>
<artifactId>lzma-java</artifactId>
<version>1.2</version>
<version>1.3</version>
</dependency>
</dependencies>
</project>
</project>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 B

BIN
res/chevron-down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
res/chevron-right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
res/download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

BIN
res/options-background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

BIN
res/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
res/star2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
res/update.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,3 +1,3 @@
version=${pom.version}
file=https://github.com/itdelatrisu/opsu/releases/download/${pom.version}/opsu-${pom.version}.jar
version=${version}
file=https://github.com/itdelatrisu/opsu/releases/download/${version}/opsu-${version}.jar
build.date=${timestamp}

1
settings.gradle Normal file
View File

@@ -0,0 +1 @@
rootProject.name = 'opsu'

View File

@@ -21,6 +21,7 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.render.CurveRenderState;
@@ -125,7 +126,7 @@ public class Container extends AppGameContainer {
// reset image references
GameImage.clearReferences();
GameData.Grade.clearReferences();
Beatmap.getBackgroundImageCache().clear();
Beatmap.clearBackgroundImageCache();
// prevent loading tracks from re-initializing OpenAL
MusicController.reset();
@@ -136,6 +137,11 @@ public class Container extends AppGameContainer {
// delete OpenGL objects involved in the Curve rendering
CurveRenderState.shutdown();
// destroy watch service
if (!Options.isWatchServiceEnabled())
BeatmapWatchService.destroy();
BeatmapWatchService.removeListeners();
}
@Override

View File

@@ -20,8 +20,10 @@ package itdelatrisu.opsu;
import java.awt.Cursor;
import java.awt.Desktop;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.util.Properties;
@@ -44,12 +46,13 @@ public class ErrorHandler {
/** Error popup description text. */
private static final String
desc = "opsu! has encountered an error.",
descR = "opsu! has encountered an error. Please report this!";
descReport = "opsu! has encountered an error. Please report this!";
/** Error popup button options. */
private static final String[]
options = {"View Error Log", "Close"},
optionsR = {"Send Report", "View Error Log", "Close"};
optionsLog = {"View Error Log", "Close"},
optionsReport = {"Send Report", "Close"},
optionsLogReport = {"Send Report", "View Error Log", "Close"};
/** Text area for Exception. */
private static final JTextArea textArea = new JTextArea(7, 30);
@@ -59,6 +62,7 @@ public class ErrorHandler {
textArea.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
textArea.setTabSize(2);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
}
/** Scroll pane holding JTextArea. */
@@ -67,7 +71,7 @@ public class ErrorHandler {
/** Error popup objects. */
private static final Object[]
message = { desc, scroll },
messageR = { descR, scroll };
messageReport = { descReport, scroll };
// This class should not be instantiated.
private ErrorHandler() {}
@@ -107,72 +111,112 @@ public class ErrorHandler {
// display popup
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
Desktop desktop = null;
boolean isBrowseSupported = false, isOpenSupported = false;
if (Desktop.isDesktopSupported()) {
// try to open the log file and/or issues webpage
if (report) {
// ask to report the error
int n = JOptionPane.showOptionDialog(null, messageR, title,
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
null, optionsR, optionsR[2]);
if (n == 0) {
// auto-fill debug information
String issueTitle = (error != null) ? error : e.getMessage();
StringBuilder sb = new StringBuilder();
Properties props = new Properties();
props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE));
String version = props.getProperty("version");
if (version != null && !version.equals("${pom.version}")) {
sb.append("**Version:** ");
sb.append(version);
sb.append('\n');
}
String timestamp = props.getProperty("build.date");
if (timestamp != null &&
!timestamp.equals("${maven.build.timestamp}") && !timestamp.equals("${timestamp}")) {
sb.append("**Build date:** ");
sb.append(timestamp);
sb.append('\n');
}
sb.append("**OS:** ");
sb.append(System.getProperty("os.name"));
sb.append(" (");
sb.append(System.getProperty("os.arch"));
sb.append(")\n");
sb.append("**JRE:** ");
sb.append(System.getProperty("java.version"));
sb.append('\n');
if (error != null) {
sb.append("**Error:** `");
sb.append(error);
sb.append("`\n");
}
if (trace != null) {
sb.append("**Stack trace:**");
sb.append("\n```\n");
sb.append(trace);
sb.append("```");
}
URI uri = URI.create(String.format(Options.ISSUES_URL,
URLEncoder.encode(issueTitle, "UTF-8"),
URLEncoder.encode(sb.toString(), "UTF-8")));
Desktop.getDesktop().browse(uri);
} else if (n == 1)
Desktop.getDesktop().open(Options.LOG_FILE);
} else {
// don't report the error
desktop = Desktop.getDesktop();
isBrowseSupported = desktop.isSupported(Desktop.Action.BROWSE);
isOpenSupported = desktop.isSupported(Desktop.Action.OPEN);
}
if (desktop != null && (isOpenSupported || (report && isBrowseSupported))) { // try to open the log file and/or issues webpage
if (report && isBrowseSupported) { // ask to report the error
if (isOpenSupported) { // also ask to open the log
int n = JOptionPane.showOptionDialog(null, messageReport, title,
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
null, optionsLogReport, optionsLogReport[2]);
if (n == 0)
desktop.browse(getIssueURI(error, e, trace));
else if (n == 1)
desktop.open(Options.LOG_FILE);
} else { // only ask to report the error
int n = JOptionPane.showOptionDialog(null, message, title,
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
null, optionsReport, optionsReport[1]);
if (n == 0)
desktop.browse(getIssueURI(error, e, trace));
}
} else { // don't report the error
int n = JOptionPane.showOptionDialog(null, message, title,
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
null, options, options[1]);
null, optionsLog, optionsLog[1]);
if (n == 0)
Desktop.getDesktop().open(Options.LOG_FILE);
desktop.open(Options.LOG_FILE);
}
} else {
// display error only
JOptionPane.showMessageDialog(null, report ? messageR : message,
} else { // display error only
JOptionPane.showMessageDialog(null, report ? messageReport : message,
title, JOptionPane.ERROR_MESSAGE);
}
} catch (Exception e1) {
Log.error("Error opening crash popup.", e1);
Log.error("An error occurred in the crash popup.", e1);
}
}
/**
* Returns the issue reporting URI.
* This will auto-fill the report with the relevant information if possible.
* @param error a description of the error
* @param e the exception causing the error
* @param trace the stack trace
* @return the created URI
*/
private static URI getIssueURI(String error, Throwable e, String trace) {
// generate report information
String issueTitle = (error != null) ? error : e.getMessage();
StringBuilder sb = new StringBuilder();
try {
// read version and build date from version file, if possible
Properties props = new Properties();
props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE));
String version = props.getProperty("version");
if (version != null && !version.equals("${pom.version}")) {
sb.append("**Version:** ");
sb.append(version);
String hash = Utils.getGitHash();
if (hash != null) {
sb.append(" (");
sb.append(hash.substring(0, 12));
sb.append(')');
}
sb.append('\n');
}
String timestamp = props.getProperty("build.date");
if (timestamp != null &&
!timestamp.equals("${maven.build.timestamp}") && !timestamp.equals("${timestamp}")) {
sb.append("**Build date:** ");
sb.append(timestamp);
sb.append('\n');
}
} catch (IOException e1) {
Log.warn("Could not read version file.", e1);
}
sb.append("**OS:** ");
sb.append(System.getProperty("os.name"));
sb.append(" (");
sb.append(System.getProperty("os.arch"));
sb.append(")\n");
sb.append("**JRE:** ");
sb.append(System.getProperty("java.version"));
sb.append('\n');
if (error != null) {
sb.append("**Error:** `");
sb.append(error);
sb.append("`\n");
}
if (trace != null) {
sb.append("**Stack trace:**");
sb.append("\n```\n");
sb.append(trace);
sb.append("```");
}
// return auto-filled URI
try {
return URI.create(String.format(Options.ISSUES_URL,
URLEncoder.encode(issueTitle, "UTF-8"),
URLEncoder.encode(sb.toString(), "UTF-8")));
} catch (UnsupportedEncodingException e1) {
Log.warn("URLEncoder failed to encode the auto-filled issue report URL.");
return URI.create(String.format(Options.ISSUES_URL, "", ""));
}
}
}

View File

@@ -26,8 +26,12 @@ import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
import java.util.Date;
@@ -87,7 +91,7 @@ public class GameData {
D (GameImage.RANKING_D, GameImage.RANKING_D_SMALL);
/** GameImages associated with this grade (large and small sizes). */
private GameImage large, small;
private final GameImage large, small;
/** Large-size image scaled for use in song menu. */
private Image menuImage;
@@ -129,7 +133,7 @@ public class GameData {
return menuImage;
Image img = getSmallImage();
if (!small.hasSkinImage()) // save default image only
if (!small.hasBeatmapSkinImage()) // save default image only
this.menuImage = img;
return img;
}
@@ -199,14 +203,14 @@ public class GameData {
*/
private class HitErrorInfo {
/** The correct hit time. */
private int time;
private final int time;
/** The coordinates of the hit. */
@SuppressWarnings("unused")
private int x, y;
private final int x, y;
/** The difference between the correct and actual hit times. */
private int timeDiff;
private final int timeDiff;
/**
* Constructor.
@@ -232,29 +236,32 @@ public class GameData {
/** Hit result helper class. */
private class HitObjectResult {
/** Object start time. */
public int time;
public final int time;
/** Hit result. */
public int result;
public final int result;
/** Object coordinates. */
public float x, y;
public final float x, y;
/** Combo color. */
public Color color;
public final Color color;
/** The type of the hit object. */
public HitObjectType hitResultType;
public final HitObjectType hitResultType;
/** Slider curve. */
public final Curve curve;
/** Whether or not to expand when animating. */
public final boolean expand;
/** Whether or not to hide the hit result. */
public final boolean hideResult;
/** Alpha level (for fading out). */
public float alpha = 1f;
/** Slider curve. */
public Curve curve;
/** Whether or not to expand when animating. */
public boolean expand;
/**
* Constructor.
* @param time the result's starting track position
@@ -262,11 +269,13 @@ public class GameData {
* @param x the center x coordinate
* @param y the center y coordinate
* @param color the color of the hit object
* @param hitResultType the hit object type
* @param curve the slider curve (or null if not applicable)
* @param expand whether or not the hit result animation should expand (if applicable)
* @param hideResult whether or not to hide the hit result (but still show the other animations)
*/
public HitObjectResult(int time, int result, float x, float y, Color color,
HitObjectType hitResultType, Curve curve, boolean expand) {
HitObjectType hitResultType, Curve curve, boolean expand, boolean hideResult) {
this.time = time;
this.result = result;
this.x = x;
@@ -275,6 +284,7 @@ public class GameData {
this.hitResultType = hitResultType;
this.curve = curve;
this.expand = expand;
this.hideResult = hideResult;
}
}
@@ -316,7 +326,7 @@ public class GameData {
private Replay replay;
/** Whether this object is used for gameplay (true) or score viewing (false). */
private boolean gameplay;
private boolean isGameplay;
/** Container dimensions. */
private int width, height;
@@ -329,7 +339,7 @@ public class GameData {
public GameData(int width, int height) {
this.width = width;
this.height = height;
this.gameplay = true;
this.isGameplay = true;
clear();
}
@@ -345,7 +355,7 @@ public class GameData {
public GameData(ScoreData s, int width, int height) {
this.width = width;
this.height = height;
this.gameplay = false;
this.isGameplay = false;
this.scoreData = s;
this.score = s.score;
@@ -400,8 +410,8 @@ public class GameData {
// gameplay-specific images
if (isGameplay()) {
// combo burst images
if (GameImage.COMBO_BURST.hasSkinImages() ||
(!GameImage.COMBO_BURST.hasSkinImage() && GameImage.COMBO_BURST.getImages() != null))
if (GameImage.COMBO_BURST.hasBeatmapSkinImages() ||
(!GameImage.COMBO_BURST.hasBeatmapSkinImage() && GameImage.COMBO_BURST.getImages() != null))
comboBurstImages = GameImage.COMBO_BURST.getImages();
else
comboBurstImages = new Image[]{ GameImage.COMBO_BURST.getImage() };
@@ -474,6 +484,7 @@ public class GameData {
/**
* Sets the array of hit result offsets.
* @param hitResultOffset the time offset array (of size {@link #HIT_MAX})
*/
public void setHitResultOffset(int[] hitResultOffset) { this.hitResultOffset = hitResultOffset; }
@@ -614,7 +625,7 @@ public class GameData {
);
} else {
// lead-in time (yellow)
g.setColor(Utils.COLOR_YELLOW_ALPHA);
g.setColor(Colors.YELLOW_ALPHA);
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
-90 + (int) (360f * trackPosition / firstObjectTime), -90
);
@@ -651,27 +662,27 @@ public class GameData {
float hitErrorY = height / uiScale - margin - 10;
float barY = (hitErrorY - 3) * uiScale, barHeight = 6 * uiScale;
float tickY = (hitErrorY - 10) * uiScale, tickHeight = 20 * uiScale;
float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a = hitErrorAlpha;
g.setColor(Utils.COLOR_BLACK_ALPHA);
float oldAlphaBlack = Colors.BLACK_ALPHA.a;
Colors.BLACK_ALPHA.a = hitErrorAlpha;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, tickY,
(hitResultOffset[HIT_50] * 2) * uiScale, tickHeight);
Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack;
Utils.COLOR_LIGHT_ORANGE.a = hitErrorAlpha;
g.setColor(Utils.COLOR_LIGHT_ORANGE);
Colors.BLACK_ALPHA.a = oldAlphaBlack;
Colors.LIGHT_ORANGE.a = hitErrorAlpha;
g.setColor(Colors.LIGHT_ORANGE);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, barY,
(hitResultOffset[HIT_50] * 2) * uiScale, barHeight);
Utils.COLOR_LIGHT_ORANGE.a = 1f;
Utils.COLOR_LIGHT_GREEN.a = hitErrorAlpha;
g.setColor(Utils.COLOR_LIGHT_GREEN);
Colors.LIGHT_ORANGE.a = 1f;
Colors.LIGHT_GREEN.a = hitErrorAlpha;
g.setColor(Colors.LIGHT_GREEN);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_100]) * uiScale, barY,
(hitResultOffset[HIT_100] * 2) * uiScale, barHeight);
Utils.COLOR_LIGHT_GREEN.a = 1f;
Utils.COLOR_LIGHT_BLUE.a = hitErrorAlpha;
g.setColor(Utils.COLOR_LIGHT_BLUE);
Colors.LIGHT_GREEN.a = 1f;
Colors.LIGHT_BLUE.a = hitErrorAlpha;
g.setColor(Colors.LIGHT_BLUE);
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_300]) * uiScale, barY,
(hitResultOffset[HIT_300] * 2) * uiScale, barHeight);
Utils.COLOR_LIGHT_BLUE.a = 1f;
Colors.LIGHT_BLUE.a = 1f;
white.a = hitErrorAlpha;
g.setColor(white);
g.fillRect((hitErrorX - 1.5f) * uiScale, tickY, 3 * uiScale, tickHeight);
@@ -830,16 +841,16 @@ public class GameData {
// header
Image rankingTitle = GameImage.RANKING_TITLE.getImage();
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, 100 * uiScale);
rankingTitle.draw((width * 0.97f) - rankingTitle.getWidth(), 0);
float marginX = width * 0.01f, marginY = height * 0.002f;
Utils.FONT_LARGE.drawString(marginX, marginY,
Fonts.LARGE.drawString(marginX, marginY,
String.format("%s - %s [%s]", beatmap.getArtist(), beatmap.getTitle(), beatmap.version), Color.white);
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6,
Fonts.MEDIUM.drawString(marginX, marginY + Fonts.LARGE.getLineHeight() - 6,
String.format("Beatmap by %s", beatmap.creator), Color.white);
String player = (scoreData.playerName == null) ? "" : String.format(" by %s", scoreData.playerName);
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() + Utils.FONT_MEDIUM.getLineHeight() - 10,
Fonts.MEDIUM.drawString(marginX, marginY + Fonts.LARGE.getLineHeight() + Fonts.MEDIUM.getLineHeight() - 10,
String.format("Played%s on %s.", player, scoreData.getTimeString()), Color.white);
// mod icons
@@ -872,7 +883,7 @@ public class GameData {
}
// hit lighting
else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS &&
else if (Options.isHitLightingEnabled() && !hitResult.hideResult && hitResult.result != HIT_MISS &&
hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) {
// TODO: add particle system
Image lighting = GameImage.LIGHTING.getImage();
@@ -885,47 +896,45 @@ public class GameData {
hitResult.hitResultType == HitObjectType.CIRCLE ||
hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
hitResult.hitResultType == HitObjectType.SLIDER_LAST)) {
float scale = (!hitResult.expand) ? 1f : Utils.easeOut(
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME),
1f, HITCIRCLE_ANIM_SCALE - 1f, HITCIRCLE_FADE_TIME
);
float alpha = Utils.easeOut(
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME),
1f, -1f, HITCIRCLE_FADE_TIME
);
float progress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
float scale = (!hitResult.expand) ? 1f : 1f + (HITCIRCLE_ANIM_SCALE - 1f) * progress;
float alpha = 1f - progress;
// slider curve
if (hitResult.curve != null) {
float oldWhiteAlpha = Utils.COLOR_WHITE_FADE.a;
float oldWhiteAlpha = Colors.WHITE_FADE.a;
float oldColorAlpha = hitResult.color.a;
Utils.COLOR_WHITE_FADE.a = alpha;
Colors.WHITE_FADE.a = alpha;
hitResult.color.a = alpha;
hitResult.curve.draw(hitResult.color);
Utils.COLOR_WHITE_FADE.a = oldWhiteAlpha;
Colors.WHITE_FADE.a = oldWhiteAlpha;
hitResult.color.a = oldColorAlpha;
}
// hit circles
Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
scaledHitCircle.setAlpha(alpha);
scaledHitCircleOverlay.setAlpha(alpha);
scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
if (!(hitResult.hitResultType == HitObjectType.CIRCLE && GameMod.HIDDEN.isActive())) {
// "hidden" mod: expanding animation for only circles not drawn
Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
scaledHitCircle.setAlpha(alpha);
scaledHitCircleOverlay.setAlpha(alpha);
scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
}
}
// hit result
if (hitResult.hitResultType == HitObjectType.CIRCLE ||
if (!hitResult.hideResult && (
hitResult.hitResultType == HitObjectType.CIRCLE ||
hitResult.hitResultType == HitObjectType.SPINNER ||
hitResult.curve != null) {
float scale = Utils.easeBounce(
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME),
1f, HITCIRCLE_TEXT_ANIM_SCALE - 1f, HITCIRCLE_TEXT_BOUNCE_TIME
);
float alpha = Utils.easeOut(
Utils.clamp((trackPosition - hitResult.time) - HITCIRCLE_FADE_TIME, 0, HITCIRCLE_TEXT_FADE_TIME),
1f, -1f, HITCIRCLE_TEXT_FADE_TIME
);
hitResult.curve != null)) {
float scaleProgress = AnimationEquation.IN_OUT_BOUNCE.calc(
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME) / HITCIRCLE_TEXT_BOUNCE_TIME);
float scale = 1f + (HITCIRCLE_TEXT_ANIM_SCALE - 1f) * scaleProgress;
float fadeProgress = AnimationEquation.OUT_CUBIC.calc(
(float) Utils.clamp((trackPosition - hitResult.time) - HITCIRCLE_FADE_TIME, 0, HITCIRCLE_TEXT_FADE_TIME) / HITCIRCLE_TEXT_FADE_TIME);
float alpha = 1f - fadeProgress;
Image scaledHitResult = hitResults[hitResult.result].getScaledCopy(scale);
scaledHitResult.setAlpha(alpha);
scaledHitResult.drawCentered(hitResult.x, hitResult.y);
@@ -1037,10 +1046,13 @@ public class GameData {
* or {@code Grade.NULL} if no objects have been processed.
*/
private Grade getGrade() {
boolean silver = (scoreData == null) ?
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive()) :
(scoreData.mods & (GameMod.HIDDEN.getBit() | GameMod.FLASHLIGHT.getBit())) != 0;
return getGrade(
hitResultCount[HIT_300], hitResultCount[HIT_100],
hitResultCount[HIT_50], hitResultCount[HIT_MISS],
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive())
silver
);
}
@@ -1179,7 +1191,7 @@ public class GameData {
switch (result) {
case HIT_SLIDER30:
hitValue = 30;
changeHealth(1f);
changeHealth(2f);
SoundController.playHitSound(
hitObject.getEdgeHitSoundType(repeat),
hitObject.getSampleSet(repeat),
@@ -1187,6 +1199,7 @@ public class GameData {
break;
case HIT_SLIDER10:
hitValue = 10;
changeHealth(1f);
SoundController.playHitSound(HitSound.SLIDERTICK);
break;
case HIT_MISS:
@@ -1204,7 +1217,7 @@ public class GameData {
if (!Options.isPerfectHitBurstEnabled())
; // hide perfect hit results
else
hitResultList.add(new HitObjectResult(time, result, x, y, null, HitObjectType.SLIDERTICK, null, false));
hitResultList.add(new HitObjectResult(time, result, x, y, null, HitObjectType.SLIDERTICK, null, false, false));
}
fullObjectCount++;
}
@@ -1360,20 +1373,18 @@ public class GameData {
int hitResult = handleHitResult(time, result, x, y, color, end, hitObject,
hitResultType, repeat, (curve != null && !sliderHeldToEnd));
if ((hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled())
; // hide perfect hit results
else if (hitResult == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
; // "relax" and "autopilot" mods: hide misses
else {
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand));
if (hitResult == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
return; // "relax" and "autopilot" mods: hide misses
// sliders: add the other curve endpoint for the hit animation
if (curve != null) {
boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST);
float[] p = curve.pointAt((isFirst) ? 1f : 0f);
HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST;
hitResultList.add(new HitObjectResult(time, hitResult, p[0], p[1], color, type, null, expand));
}
boolean hideResult = (hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled();
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand, hideResult));
// sliders: add the other curve endpoint for the hit animation
if (curve != null) {
boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST);
Vec2f p = curve.pointAt((isFirst) ? 1f : 0f);
HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST;
hitResultList.add(new HitObjectResult(time, hitResult, p.x, p.y, color, type, null, expand, hideResult));
}
}
@@ -1460,7 +1471,13 @@ public class GameData {
* Returns whether or not this object is used for gameplay.
* @return true if gameplay, false if score viewing
*/
public boolean isGameplay() { return gameplay; }
public boolean isGameplay() { return isGameplay; }
/**
* Sets whether or not this object is used for gameplay.
* @param gameplay true if gameplay, false if score viewing
*/
public void setGameplay(boolean gameplay) { this.isGameplay = gameplay; }
/**
* Adds the hit into the list of hit error information.

View File

@@ -18,6 +18,8 @@
package itdelatrisu.opsu;
import itdelatrisu.opsu.ui.Fonts;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
@@ -35,8 +37,8 @@ public enum GameImage {
CURSOR ("cursor", "png"),
CURSOR_MIDDLE ("cursormiddle", "png"),
CURSOR_TRAIL ("cursortrail", "png"),
CURSOR_OLD ("cursor2", "png", false, false),
CURSOR_TRAIL_OLD ("cursortrail2", "png", false, false),
CURSOR_OLD ("cursor2", "png", false, false), // custom
CURSOR_TRAIL_OLD ("cursortrail2", "png", false, false), // custom
// Game
SECTION_PASS ("section-pass", "png"),
@@ -251,14 +253,14 @@ public enum GameImage {
MENU_MUSICNOTE ("music-note", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
int r = (int) ((Utils.FONT_LARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 8) / getUIscale());
int r = (int) ((Fonts.LARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() - 8) / getUIscale());
return img.getScaledCopy(r, r);
}
},
MENU_LOADER ("loader", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
int r = (int) ((Utils.FONT_LARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 8) / getUIscale());
int r = (int) ((Fonts.LARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() - 8) / getUIscale());
return img.getScaledCopy(r / 48f);
}
},
@@ -290,12 +292,25 @@ public enum GameImage {
MENU_BUTTON_MID ("button-middle", "png", false, false),
MENU_BUTTON_LEFT ("button-left", "png", false, false),
MENU_BUTTON_RIGHT ("button-right", "png", false, false),
STAR ("star", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
return img.getScaledCopy((MENU_BUTTON_BG.getImage().getHeight() * 0.16f) / img.getHeight());
}
},
STAR2 ("star2", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
return img.getScaledCopy((MENU_BUTTON_BG.getImage().getHeight() * 0.33f) / img.getHeight());
}
},
// Music Player Buttons
MUSIC_PLAY ("music-play", "png", false, false),
MUSIC_PAUSE ("music-pause", "png", false, false),
MUSIC_NEXT ("music-next", "png", false, false),
MUSIC_PREVIOUS ("music-previous", "png", false, false),
DOWNLOADS ("downloads", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
@@ -312,7 +327,7 @@ public enum GameImage {
DELETE ("delete", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
int lineHeight = Utils.FONT_DEFAULT.getLineHeight();
int lineHeight = Fonts.DEFAULT.getLineHeight();
return img.getScaledCopy(lineHeight, lineHeight);
}
},
@@ -328,10 +343,16 @@ public enum GameImage {
return img.getScaledCopy((h / 17f) / img.getHeight());
}
},
BANG ("bang", "png", false, false) {
DOWNLOAD ("download", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
return REPOSITORY.process_sub(img, w, h);
return img.getScaledCopy((h / 14f) / img.getHeight());
}
},
UPDATE ("update", "png", false, false) {
@Override
protected Image process_sub(Image img, int w, int h) {
return img.getScaledCopy((h / 14f) / img.getHeight());
}
},
OPTIONS_BG ("options-background", "png|jpg", false, true) {
@@ -341,6 +362,8 @@ public enum GameImage {
return img.getScaledCopy(w, h);
}
},
CHEVRON_DOWN ("chevron-down", "png", false, false),
CHEVRON_RIGHT ("chevron-right", "png", false, false),
// TODO: ensure this image hasn't been modified (checksum?)
ALPHA_MAP ("alpha", "png", false, false);
@@ -351,22 +374,22 @@ public enum GameImage {
IMG_JPG = 2;
/** The file name. */
private String filename;
private final String filename;
/** The formatted file name string (for loading multiple images). */
private String filenameFormat;
/** Image file type. */
private byte type;
private final byte type;
/**
* Whether or not the image is skinnable by a beatmap.
* These images are typically related to gameplay.
*/
private boolean skinnable;
private final boolean beatmapSkinnable;
/** Whether or not to preload the image when the program starts. */
private boolean preload;
private final boolean preload;
/** The default image. */
private Image defaultImage;
@@ -374,6 +397,9 @@ public enum GameImage {
/** The default image array. */
private Image[] defaultImages;
/** Whether the image is currently skinned by a game skin. */
private boolean isSkinned = false;
/** The beatmap skin image (optional, temporary). */
private Image skinImage;
@@ -421,6 +447,7 @@ public enum GameImage {
for (GameImage img : GameImage.values()) {
img.defaultImage = img.skinImage = null;
img.defaultImages = img.skinImages = null;
img.isSkinned = false;
}
}
@@ -488,7 +515,7 @@ public enum GameImage {
}
/**
* Constructor for game-related images (skinnable and preloaded).
* Constructor for game-related images (beatmap-skinnable and preloaded).
* @param filename the image file name
* @param type the file types (separated by '|')
*/
@@ -497,7 +524,7 @@ public enum GameImage {
}
/**
* Constructor for an array of game-related images (skinnable and preloaded).
* Constructor for an array of game-related images (beatmap-skinnable and preloaded).
* @param filename the image file name
* @param filenameFormat the formatted file name string (for loading multiple images)
* @param type the file types (separated by '|')
@@ -511,21 +538,21 @@ public enum GameImage {
* Constructor for general images.
* @param filename the image file name
* @param type the file types (separated by '|')
* @param skinnable whether or not the image is skinnable
* @param beatmapSkinnable whether or not the image is beatmap-skinnable
* @param preload whether or not to preload the image
*/
GameImage(String filename, String type, boolean skinnable, boolean preload) {
GameImage(String filename, String type, boolean beatmapSkinnable, boolean preload) {
this.filename = filename;
this.type = getType(type);
this.skinnable = skinnable;
this.beatmapSkinnable = beatmapSkinnable;
this.preload = preload;
}
/**
* Returns whether or not the image is skinnable.
* @return true if skinnable
* Returns whether or not the image is beatmap-skinnable.
* @return true if beatmap-skinnable
*/
public boolean isSkinnable() { return skinnable; }
public boolean isBeatmapSkinnable() { return beatmapSkinnable; }
/**
* Returns whether or not to preload the image when the program starts.
@@ -535,7 +562,7 @@ public enum GameImage {
/**
* Returns the image associated with this resource.
* The skin image takes priority over the default image.
* The beatmap skin image takes priority over the default image.
*/
public Image getImage() {
setDefaultImage();
@@ -556,7 +583,7 @@ public enum GameImage {
/**
* Returns the image array associated with this resource.
* The skin images takes priority over the default images.
* The beatmap skin images takes priority over the default images.
*/
public Image[] getImages() {
setDefaultImage();
@@ -565,7 +592,7 @@ public enum GameImage {
/**
* Sets the image associated with this resource to another image.
* The skin image takes priority over the default image.
* The beatmap skin image takes priority over the default image.
* @param img the image to set
*/
public void setImage(Image img) {
@@ -577,7 +604,7 @@ public enum GameImage {
/**
* Sets an image associated with this resource to another image.
* The skin image takes priority over the default image.
* The beatmap skin image takes priority over the default image.
* @param img the image to set
* @param index the index in the image array
*/
@@ -602,16 +629,26 @@ public enum GameImage {
// try to load multiple images
File skinDir = Options.getSkin().getDirectory();
if (filenameFormat != null) {
if ((skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) ||
((defaultImages = loadImageArray(null)) != null)) {
if (skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) {
isSkinned = true;
process();
return;
}
if ((defaultImages = loadImageArray(null)) != null) {
isSkinned = false;
process();
return;
}
}
// try to load a single image
if ((skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) ||
((defaultImage = loadImageSingle(null)) != null)) {
if (skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) {
isSkinned = true;
process();
return;
}
if ((defaultImage = loadImageSingle(null)) != null) {
isSkinned = false;
process();
return;
}
@@ -620,17 +657,17 @@ public enum GameImage {
}
/**
* Sets the associated skin image.
* Sets the associated beatmap skin image.
* If the path does not contain the image, the default image is used.
* @param dir the image directory to search
* @return true if a new skin image is loaded, false otherwise
*/
public boolean setSkinImage(File dir) {
public boolean setBeatmapSkinImage(File dir) {
if (dir == null)
return false;
// destroy the existing images, if any
destroySkinImage();
destroyBeatmapSkinImage();
// beatmap skins disabled
if (Options.isBeatmapSkinIgnored())
@@ -709,21 +746,27 @@ public enum GameImage {
}
/**
* Returns whether a skin image is currently loaded.
* @return true if skin image exists
* Returns whether the default image loaded is part of a game skin.
* @return true if a game skin image is loaded, false if the default image is loaded
*/
public boolean hasSkinImage() { return (skinImage != null && !skinImage.isDestroyed()); }
public boolean hasGameSkinImage() { return isSkinned; }
/**
* Returns whether skin images are currently loaded.
* @return true if any skin image exists
* Returns whether a beatmap skin image is currently loaded.
* @return true if a beatmap skin image exists
*/
public boolean hasSkinImages() { return (skinImages != null); }
public boolean hasBeatmapSkinImage() { return (skinImage != null && !skinImage.isDestroyed()); }
/**
* Destroys the associated skin image(s), if any.
* Returns whether beatmap skin images are currently loaded.
* @return true if any beatmap skin image exists
*/
public void destroySkinImage() {
public boolean hasBeatmapSkinImages() { return (skinImages != null); }
/**
* Destroys the associated beatmap skin image(s), if any.
*/
public void destroyBeatmapSkinImage() {
if (skinImage == null && skinImages == null)
return;
try {
@@ -740,7 +783,7 @@ public enum GameImage {
skinImages = null;
}
} catch (SlickException e) {
ErrorHandler.error(String.format("Failed to destroy skin images for '%s'.", this.name()), e, true);
ErrorHandler.error(String.format("Failed to destroy beatmap skin images for '%s'.", this.name()), e, true);
}
}

View File

@@ -18,7 +18,9 @@
package itdelatrisu.opsu;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.util.Arrays;
import java.util.Collections;
@@ -47,7 +49,7 @@ public enum GameMod {
"DoubleTime", "Zoooooooooom."),
// NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f,
// "Nightcore", "uguuuuuuuu"),
HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, false,
HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f,
"Hidden", "Play with no approach circles and fading notes for a slight score advantage."),
FLASHLIGHT (Category.HARD, 4, GameImage.MOD_FLASHLIGHT, "FL", 1024, Input.KEY_G, 1.12f,
"Flashlight", "Restricted view area."),
@@ -55,9 +57,9 @@ public enum GameMod {
"Relax", "You don't need to click.\nGive your clicking/tapping finger a break from the heat of things.\n**UNRANKED**"),
AUTOPILOT (Category.SPECIAL, 1, GameImage.MOD_AUTOPILOT, "AP", 8192, Input.KEY_X, 0f,
"Relax2", "Automatic cursor movement - just follow the rhythm.\n**UNRANKED**"),
SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_V, 0.9f,
SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_C, 0.9f,
"SpunOut", "Spinners will be automatically completed."),
AUTO (Category.SPECIAL, 3, GameImage.MOD_AUTO, "", 2048, Input.KEY_B, 1f,
AUTO (Category.SPECIAL, 3, GameImage.MOD_AUTO, "", 2048, Input.KEY_V, 1f,
"Autoplay", "Watch a perfect automated play through the song.");
/** Mod categories. */
@@ -67,13 +69,13 @@ public enum GameMod {
SPECIAL (2, "Special", Color.white);
/** Drawing index. */
private int index;
private final int index;
/** Category name. */
private String name;
private final String name;
/** Text color. */
private Color color;
private final Color color;
/** The coordinates of the category. */
private float x, y;
@@ -96,10 +98,10 @@ public enum GameMod {
* @param height the container height
*/
public void init(int width, int height) {
float multY = Utils.FONT_LARGE.getLineHeight() * 2 + height * 0.06f;
float multY = Fonts.LARGE.getLineHeight() * 2 + height * 0.06f;
float offsetY = GameImage.MOD_EASY.getImage().getHeight() * 1.5f;
this.x = width / 30f;
this.y = multY + Utils.FONT_LARGE.getLineHeight() * 3f + offsetY * index;
this.y = multY + Fonts.LARGE.getLineHeight() * 3f + offsetY * index;
}
/**
@@ -124,37 +126,34 @@ public enum GameMod {
}
/** The category for the mod. */
private Category category;
private final Category category;
/** The index in the category (for positioning). */
private int categoryIndex;
private final int categoryIndex;
/** The file name of the mod image. */
private GameImage image;
private final GameImage image;
/** The abbreviation for the mod. */
private String abbrev;
private final String abbrev;
/**
* Bit value associated with the mod.
* See the osu! API: https://github.com/peppy/osu-api/wiki#mods
*/
private int bit;
private final int bit;
/** The shortcut key associated with the mod. */
private int key;
private final int key;
/** The score multiplier. */
private float multiplier;
/** Whether or not the mod is implemented. */
private boolean implemented;
private final float multiplier;
/** The name of the mod. */
private String name;
private final String name;
/** The description of the mod. */
private String description;
private final String description;
/** Whether or not this mod is active. */
private boolean active = false;
@@ -192,13 +191,15 @@ public enum GameMod {
c.init(width, height);
// create buttons
float baseX = Category.EASY.getX() + Utils.FONT_LARGE.getWidth(Category.EASY.getName()) * 1.25f;
float baseX = Category.EASY.getX() + Fonts.LARGE.getWidth(Category.EASY.getName()) * 1.25f;
float offsetX = GameImage.MOD_EASY.getImage().getWidth() * 2.1f;
for (GameMod mod : GameMod.values()) {
Image img = mod.image.getImage();
mod.button = new MenuButton(img,
baseX + (offsetX * mod.categoryIndex) + img.getWidth() / 2f,
mod.category.getY());
mod.button.setHoverAnimationDuration(300);
mod.button.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
mod.button.setHoverExpand(1.2f);
mod.button.setHoverRotate(10f);
@@ -309,24 +310,6 @@ public enum GameMod {
*/
GameMod(Category category, int categoryIndex, GameImage image, String abbrev,
int bit, int key, float multiplier, String name, String description) {
this(category, categoryIndex, image, abbrev, bit, key, multiplier, true, name, description);
}
/**
* Constructor.
* @param category the category for the mod
* @param categoryIndex the index in the category
* @param image the GameImage
* @param abbrev the two-letter abbreviation
* @param bit the bit
* @param key the shortcut key
* @param multiplier the score multiplier
* @param implemented whether the mod is implemented
* @param name the name
* @param description the description
*/
GameMod(Category category, int categoryIndex, GameImage image, String abbrev,
int bit, int key, float multiplier, boolean implemented, String name, String description) {
this.category = category;
this.categoryIndex = categoryIndex;
this.image = image;
@@ -334,7 +317,6 @@ public enum GameMod {
this.bit = bit;
this.key = key;
this.multiplier = multiplier;
this.implemented = implemented;
this.name = name;
this.description = description;
}
@@ -376,20 +358,11 @@ public enum GameMod {
*/
public String getDescription() { return description; }
/**
* Returns whether or not the mod is implemented.
* @return true if implemented
*/
public boolean isImplemented() { return implemented; }
/**
* Toggles the active status of the mod.
* @param checkInverse if true, perform checks for mutual exclusivity
*/
public void toggle(boolean checkInverse) {
if (!implemented)
return;
active = !active;
scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
@@ -446,14 +419,7 @@ public enum GameMod {
/**
* Draws the game mod.
*/
public void draw() {
if (!implemented) {
button.getImage().setAlpha(0.2f);
button.draw();
button.getImage().setAlpha(1f);
} else
button.draw();
}
public void draw() { button.draw(); }
/**
* Checks if the coordinates are within the image bounds.

View File

@@ -0,0 +1,104 @@
/*
* 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.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* Native loader, based on the JarSplice launcher.
*
* @author http://ninjacave.com
*/
public class NativeLoader {
/** The directory to unpack natives to. */
private final File nativeDir;
/**
* Constructor.
* @param dir the directory to unpack natives to
*/
public NativeLoader(File dir) {
nativeDir = dir;
}
/**
* Unpacks natives for the current operating system to the natives directory.
* @throws IOException if an I/O exception occurs
*/
public void loadNatives() throws IOException {
if (!nativeDir.exists())
nativeDir.mkdir();
JarFile jarFile = Utils.getJarFile();
if (jarFile == null)
return;
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry e = entries.nextElement();
if (e == null)
break;
File f = new File(nativeDir, e.getName());
if (isNativeFile(e.getName()) && !e.isDirectory() && e.getName().indexOf('/') == -1 && !f.exists()) {
InputStream in = jarFile.getInputStream(jarFile.getEntry(e.getName()));
OutputStream out = new FileOutputStream(f);
byte[] buffer = new byte[65536];
int bufferSize;
while ((bufferSize = in.read(buffer, 0, buffer.length)) != -1)
out.write(buffer, 0, bufferSize);
in.close();
out.close();
}
}
jarFile.close();
}
/**
* Returns whether the given file name is a native file for the current operating system.
* @param entryName the file name
* @return true if the file is a native that should be loaded, false otherwise
*/
private boolean isNativeFile(String entryName) {
String osName = System.getProperty("os.name");
String name = entryName.toLowerCase();
if (osName.startsWith("Win")) {
if (name.endsWith(".dll"))
return true;
} else if (osName.startsWith("Linux")) {
if (name.endsWith(".so"))
return true;
} else if (osName.startsWith("Mac") || osName.startsWith("Darwin")) {
if (name.endsWith(".dylib") || name.endsWith(".jnilib"))
return true;
}
return false;
}
}

View File

@@ -38,10 +38,14 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.UnknownHostException;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.StateBasedGame;
import org.newdawn.slick.state.transition.FadeInTransition;
@@ -116,16 +120,43 @@ public class Opsu extends StateBasedGame {
// only allow a single instance
try {
SERVER_SOCKET = new ServerSocket(Options.getPort());
SERVER_SOCKET = new ServerSocket(Options.getPort(), 1, InetAddress.getLocalHost());
} catch (UnknownHostException e) {
// shouldn't happen
} catch (IOException e) {
ErrorHandler.error(String.format("Another program is already running on port %d.", Options.getPort()), e, false);
ErrorHandler.error(String.format(
"opsu! could not be launched for one of these reasons:\n" +
"- An instance of opsu! is already running.\n" +
"- Another program is bound to port %d. " +
"You can change the port opsu! uses by editing the \"Port\" field in the configuration file.",
Options.getPort()), null, false);
System.exit(1);
}
// set path for lwjgl natives - NOT NEEDED if using JarSplice
File nativeDir = new File("./target/natives/");
if (nativeDir.isDirectory())
System.setProperty("org.lwjgl.librarypath", nativeDir.getAbsolutePath());
File nativeDir;
if (!Utils.isJarRunning() && (
(nativeDir = new File("./target/natives/")).isDirectory() ||
(nativeDir = new File("./build/natives/")).isDirectory()))
;
else {
nativeDir = Options.NATIVE_DIR;
try {
new NativeLoader(nativeDir).loadNatives();
} catch (IOException e) {
Log.error("Error loading natives.", e);
}
}
System.setProperty("org.lwjgl.librarypath", nativeDir.getAbsolutePath());
System.setProperty("java.library.path", nativeDir.getAbsolutePath());
try {
// Workaround for "java.library.path" property being read-only.
// http://stackoverflow.com/a/24988095
Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths");
fieldSysPath.setAccessible(true);
fieldSysPath.set(null, null);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
Log.warn("Failed to set 'sys_paths' field.", e);
}
// set the resource paths
ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));
@@ -142,16 +173,21 @@ public class Opsu extends StateBasedGame {
Updater.get().setUpdateInfo(args[0], args[1]);
// check for updates
new Thread() {
@Override
public void run() {
try {
Updater.get().checkForUpdates();
} catch (IOException e) {
Log.warn("Check for updates failed.", e);
if (!Options.isUpdaterDisabled()) {
new Thread() {
@Override
public void run() {
try {
Updater.get().checkForUpdates();
} catch (IOException e) {
Log.warn("Check for updates failed.", e);
}
}
}
}.start();
}.start();
}
// disable jinput
Input.disableControllers();
// start the game
try {
@@ -190,30 +226,28 @@ public class Opsu extends StateBasedGame {
SongMenu songMenu = (SongMenu) this.getState(Opsu.STATE_SONGMENU);
if (id == STATE_GAMERANKING) {
GameData data = ((GameRanking) this.getState(Opsu.STATE_GAMERANKING)).getGameData();
if (data != null && data.isGameplay()) {
songMenu.resetGameDataOnLoad();
if (data != null && data.isGameplay())
songMenu.resetTrackOnLoad();
}
} else {
songMenu.resetGameDataOnLoad();
if (id == STATE_GAME) {
MusicController.pause();
MusicController.resume();
} else
songMenu.resetTrackOnLoad();
}
if (UI.getCursor().isSkinned())
if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
songMenu.resetGameDataOnLoad();
this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return false;
}
// show confirmation dialog if any downloads are active
if (DownloadList.get().hasActiveDownloads() &&
UI.showExitConfirmation(DownloadList.EXIT_CONFIRMATION))
UI.showExitConfirmation(DownloadList.EXIT_CONFIRMATION))
return false;
if (Updater.get().getStatus() == Updater.Status.UPDATE_DOWNLOADING &&
UI.showExitConfirmation(Updater.EXIT_CONFIRMATION))
UI.showExitConfirmation(Updater.EXIT_CONFIRMATION))
return false;
return true;
@@ -248,7 +282,7 @@ public class Opsu extends StateBasedGame {
// JARs will not run properly inside directories containing '!'
// http://bugs.java.com/view_bug.do?bug_id=4523159
if (Utils.isJarRunning() && Utils.getRunningDirectory() != null &&
Utils.getRunningDirectory().getAbsolutePath().indexOf('!') != -1)
Utils.getRunningDirectory().getAbsolutePath().indexOf('!') != -1)
ErrorHandler.error("JARs cannot be run from some paths containing '!'. Please move or rename the file and try again.", null, false);
else
ErrorHandler.error(message, e, true);

View File

@@ -22,6 +22,7 @@ import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.skins.Skin;
import itdelatrisu.opsu.skins.SkinLoader;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.UI;
import java.io.BufferedReader;
@@ -37,6 +38,9 @@ import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.lwjgl.input.Keyboard;
import org.newdawn.slick.GameContainer;
@@ -51,12 +55,18 @@ import org.newdawn.slick.util.ResourceLoader;
* Handles all user options.
*/
public class Options {
/** Whether to use XDG directories. */
public static final boolean USE_XDG = checkXDGFlag();
/** The config directory. */
private static final File CONFIG_DIR = getXDGBaseDir("XDG_CONFIG_HOME", ".config");
/** The data directory. */
private static final File DATA_DIR = getXDGBaseDir("XDG_DATA_HOME", ".local/share");
/** The cache directory. */
private static final File CACHE_DIR = getXDGBaseDir("XDG_CACHE_HOME", ".cache");
/** File for logging errors. */
public static final File LOG_FILE = new File(CONFIG_DIR, ".opsu.log");
@@ -83,6 +93,9 @@ public class Options {
/** Score database name. */
public static final File SCORE_DB = new File(DATA_DIR, ".opsu_scores.db");
/** Directory where natives are unpacked. */
public static final File NATIVE_DIR = new File(CACHE_DIR, "Natives/");
/** Font file name. */
public static final String FONT_NAME = "DroidSansFallback.ttf";
@@ -119,15 +132,35 @@ public class Options {
/** Port binding. */
private static int port = 49250;
/**
* Returns whether the XDG flag in the manifest (if any) is set to "true".
* @return true if XDG directories are enabled, false otherwise
*/
private static boolean checkXDGFlag() {
JarFile jarFile = Utils.getJarFile();
if (jarFile == null)
return false;
try {
Manifest manifest = jarFile.getManifest();
if (manifest == null)
return false;
Attributes attributes = manifest.getMainAttributes();
String value = attributes.getValue("Use-XDG");
return (value != null && value.equalsIgnoreCase("true"));
} catch (IOException e) {
return false;
}
}
/**
* Returns the directory based on the XDG base directory specification for
* Unix-like operating systems, only if the system property "XDG" has been defined.
* Unix-like operating systems, only if the "XDG" flag is enabled.
* @param env the environment variable to check (XDG_*_*)
* @param fallback the fallback directory relative to ~home
* @return the XDG base directory, or the working directory if unavailable
*/
private static File getXDGBaseDir(String env, String fallback) {
if (System.getProperty("XDG") == null)
if (!USE_XDG)
return new File("./");
String OS = System.getProperty("os.name").toLowerCase();
@@ -140,8 +173,8 @@ public class Options {
rootPath = String.format("%s/%s", home, fallback);
}
File dir = new File(rootPath, "opsu");
if (!dir.isDirectory())
dir.mkdir();
if (!dir.isDirectory() && !dir.mkdir())
ErrorHandler.error(String.format("Failed to create configuration folder at '%s/opsu'.", rootPath), null, false);
return dir;
} else
return new File("./");
@@ -212,7 +245,7 @@ public class Options {
@Override
public void read(String s) {
int i = Integer.parseInt(s);
if (i > 0 && i < 65535)
if (i > 0 && i <= 65535)
port = i;
}
},
@@ -287,9 +320,9 @@ public class Options {
super.click(container);
if (bool) {
try {
Utils.FONT_LARGE.loadGlyphs();
Utils.FONT_MEDIUM.loadGlyphs();
Utils.FONT_DEFAULT.loadGlyphs();
Fonts.LARGE.loadGlyphs();
Fonts.MEDIUM.loadGlyphs();
Fonts.DEFAULT.loadGlyphs();
} catch (SlickException e) {
Log.warn("Failed to load glyphs.", e);
}
@@ -357,7 +390,7 @@ public class Options {
public String getValueString() { return String.format("%dms", val); }
},
DISABLE_SOUNDS ("Disable All Sound Effects", "DisableSound", "May resolve Linux sound driver issues. Requires a restart.",
(System.getProperty("os.name").toLowerCase().indexOf("linux") > -1)),
(System.getProperty("os.name").toLowerCase().contains("linux"))),
KEY_LEFT ("Left Game Key", "keyOsuLeft", "Select this option to input a key.") {
@Override
public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); }
@@ -380,6 +413,7 @@ public class Options {
},
DISABLE_MOUSE_WHEEL ("Disable mouse wheel in play mode", "MouseDisableWheel", "During play, you can use the mouse wheel to adjust the volume and pause the game.\nThis will disable that functionality.", false),
DISABLE_MOUSE_BUTTONS ("Disable mouse buttons in play mode", "MouseDisableButtons", "This option will disable all mouse buttons.\nSpecifically for people who use their keyboard to click.", false),
DISABLE_CURSOR ("Disable Cursor", "DisableCursor", "Hide the cursor sprite.", false),
BACKGROUND_DIM ("Background Dim", "DimLevel", "Percentage to dim the background image during gameplay.", 50, 0, 100),
FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "ForceDefaultPlayfield", "Override the song background with the default playfield background.", false),
IGNORE_BEATMAP_SKINS ("Ignore All Beatmap Skins", "IgnoreBeatmapSkins", "Never use skin element overrides provided by beatmaps.", false),
@@ -453,16 +487,19 @@ public class Options {
val - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(val)));
}
},
ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true);
ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true),
REPLAY_SEEKING ("Replay Seeking", "ReplaySeeking", "Enable a seeking bar on the left side of the screen during replays.", false),
DISABLE_UPDATER ("Disable Automatic Updates", "DisableUpdater", "Disable automatic checking for updates upon starting opsu!.", false),
ENABLE_WATCH_SERVICE ("Enable Watch Service", "WatchService", "Watch the beatmap directory for changes. Requires a restart.", false);
/** Option name. */
private String name;
private final String name;
/** Option name, as displayed in the configuration file. */
private String displayName;
private final String displayName;
/** Option description. */
private String description;
private final String description;
/** The boolean value for the option (if applicable). */
protected boolean bool;
@@ -484,7 +521,7 @@ public class Options {
* @param displayName the option name, as displayed in the configuration file
*/
GameOption(String displayName) {
this.displayName = displayName;
this(null, displayName, null);
}
/**
@@ -604,7 +641,7 @@ public class Options {
*/
public void drag(GameContainer container, int d) {
if (type == OptionType.NUMERIC)
val = Utils.getBoundedValue(val, d, min, max);
val = Utils.clamp(val + d, min, max);
}
/**
@@ -958,6 +995,24 @@ public class Options {
*/
public static boolean isThemeSongEnabled() { return GameOption.ENABLE_THEME_SONG.getBooleanValue(); }
/**
* Returns whether or not replay seeking is enabled.
* @return true if enabled
*/
public static boolean isReplaySeekingEnabled() { return GameOption.REPLAY_SEEKING.getBooleanValue(); }
/**
* Returns whether or not automatic checking for updates is disabled.
* @return true if disabled
*/
public static boolean isUpdaterDisabled() { return GameOption.DISABLE_UPDATER.getBooleanValue(); }
/**
* Returns whether or not the beatmap watch service is enabled.
* @return true if enabled
*/
public static boolean isWatchServiceEnabled() { return GameOption.ENABLE_WATCH_SERVICE.getBooleanValue(); }
/**
* Sets the track checkpoint time, if within bounds.
* @param time the track position (in ms)
@@ -1005,6 +1060,12 @@ public class Options {
"Mouse buttons are disabled." : "Mouse buttons are enabled.");
}
/**
* Returns whether or not the cursor sprite should be hidden.
* @return true if disabled
*/
public static boolean isCursorDisabled() { return GameOption.DISABLE_CURSOR.getBooleanValue(); }
/**
* Returns the left game key.
* @return the left key code
@@ -1080,7 +1141,10 @@ public class Options {
if (beatmapDir.isDirectory())
return beatmapDir;
}
beatmapDir.mkdir(); // none found, create new directory
// none found, create new directory
if (!beatmapDir.mkdir())
ErrorHandler.error(String.format("Failed to create beatmap directory at '%s'.", beatmapDir.getAbsolutePath()), null, false);
return beatmapDir;
}
@@ -1094,7 +1158,8 @@ public class Options {
return oszDir;
oszDir = new File(DATA_DIR, "SongPacks/");
oszDir.mkdir();
if (!oszDir.isDirectory() && !oszDir.mkdir())
ErrorHandler.error(String.format("Failed to create song packs directory at '%s'.", oszDir.getAbsolutePath()), null, false);
return oszDir;
}
@@ -1108,7 +1173,8 @@ public class Options {
return replayImportDir;
replayImportDir = new File(DATA_DIR, "ReplayImport/");
replayImportDir.mkdir();
if (!replayImportDir.isDirectory() && !replayImportDir.mkdir())
ErrorHandler.error(String.format("Failed to create replay import directory at '%s'.", replayImportDir.getAbsolutePath()), null, false);
return replayImportDir;
}
@@ -1153,7 +1219,10 @@ public class Options {
if (skinRootDir.isDirectory())
return skinRootDir;
}
skinRootDir.mkdir(); // none found, create new directory
// none found, create new directory
if (!skinRootDir.mkdir())
ErrorHandler.error(String.format("Failed to create skins directory at '%s'.", skinRootDir.getAbsolutePath()), null, false);
return skinRootDir;
}

View File

@@ -20,7 +20,10 @@ package itdelatrisu.opsu;
import itdelatrisu.opsu.GameData.Grade;
import itdelatrisu.opsu.states.SongMenu;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.sql.ResultSet;
import java.sql.SQLException;
@@ -84,11 +87,6 @@ public class ScoreData implements Comparable<ScoreData> {
/** Drawing values. */
private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset, buttonAreaHeight;
/** Button background colors. */
private static final Color
BG_NORMAL = new Color(0, 0, 0, 0.25f),
BG_FOCUS = new Color(0, 0, 0, 0.75f);
/**
* Initializes the base coordinates for drawing.
* @param containerWidth the container width
@@ -99,7 +97,7 @@ public class ScoreData implements Comparable<ScoreData> {
baseY = topY;
buttonWidth = containerWidth * 0.4f;
float gradeHeight = GameImage.MENU_BUTTON_BG.getImage().getHeight() * 0.45f;
buttonHeight = Math.max(gradeHeight, Utils.FONT_DEFAULT.getLineHeight() * 3.03f);
buttonHeight = Math.max(gradeHeight, Fonts.DEFAULT.getLineHeight() * 3.03f);
buttonOffset = buttonHeight + gradeHeight / 10f;
buttonAreaHeight = (SongMenu.MAX_SCORE_BUTTONS - 1) * buttonOffset + buttonHeight;
}
@@ -155,14 +153,14 @@ public class ScoreData implements Comparable<ScoreData> {
}
/**
* Empty constructor.
* Creates an empty score data object.
*/
public ScoreData() {}
/**
* Constructor.
* @param rs the ResultSet to read from (at the current cursor position)
* @throws SQLException
* Builds a score data object from a database result set.
* @param rs the {@link ResultSet} to read from (at the current cursor position)
* @throws SQLException if a database access error occurs or the result set is closed
*/
public ScoreData(ResultSet rs) throws SQLException {
this.timestamp = rs.getLong(1);
@@ -244,46 +242,52 @@ public class ScoreData implements Comparable<ScoreData> {
* @param rank the score rank
* @param prevScore the previous (lower) score, or -1 if none
* @param focus whether the button is focused
* @param t the animation progress [0,1]
*/
public void draw(Graphics g, float position, int rank, long prevScore, boolean focus) {
Image img = getGrade().getMenuImage();
float textX = baseX + buttonWidth * 0.24f;
float edgeX = baseX + buttonWidth * 0.98f;
public void draw(Graphics g, float position, int rank, long prevScore, boolean focus, float t) {
float x = baseX - buttonWidth * (1 - AnimationEquation.OUT_BACK.calc(t)) / 2.5f;
float textX = x + buttonWidth * 0.24f;
float edgeX = x + buttonWidth * 0.98f;
float y = baseY + position;
float midY = y + buttonHeight / 2f;
float marginY = Utils.FONT_DEFAULT.getLineHeight() * 0.01f;
float marginY = Fonts.DEFAULT.getLineHeight() * 0.01f;
Color c = Colors.WHITE_FADE;
float alpha = t;
float oldAlpha = c.a;
c.a = alpha;
// rectangle outline
g.setColor((focus) ? BG_FOCUS : BG_NORMAL);
g.fillRect(baseX, y, buttonWidth, buttonHeight);
Color rectColor = (focus) ? Colors.BLACK_BG_FOCUS : Colors.BLACK_BG_NORMAL;
float oldRectAlpha = rectColor.a;
rectColor.a *= AnimationEquation.IN_QUAD.calc(alpha);
g.setColor(rectColor);
g.fillRect(x, y, buttonWidth, buttonHeight);
rectColor.a = oldRectAlpha;
// rank
if (focus) {
Utils.FONT_LARGE.drawString(
baseX + buttonWidth * 0.04f,
y + (buttonHeight - Utils.FONT_LARGE.getLineHeight()) / 2f,
Integer.toString(rank + 1), Color.white
Fonts.LARGE.drawString(
x + buttonWidth * 0.04f,
y + (buttonHeight - Fonts.LARGE.getLineHeight()) / 2f,
Integer.toString(rank + 1), c
);
}
// grade image
img.drawCentered(baseX + buttonWidth * 0.15f, midY);
Image img = getGrade().getMenuImage();
img.setAlpha(alpha);
img.drawCentered(x + buttonWidth * 0.15f, midY);
img.setAlpha(1f);
// score
float textOffset = (buttonHeight - Utils.FONT_MEDIUM.getLineHeight() - Utils.FONT_SMALL.getLineHeight()) / 2f;
Utils.FONT_MEDIUM.drawString(
textX, y + textOffset,
String.format("Score: %s (%dx)", NumberFormat.getNumberInstance().format(score), combo),
Color.white
);
float textOffset = (buttonHeight - Fonts.MEDIUM.getLineHeight() - Fonts.SMALL.getLineHeight()) / 2f;
Fonts.MEDIUM.drawString(textX, y + textOffset,
String.format("Score: %s (%dx)", NumberFormat.getNumberInstance().format(score), combo), c);
// hit counts (custom: osu! shows user instead, above score)
String player = (playerName == null) ? "" : String.format(" (%s)", playerName);
Utils.FONT_SMALL.drawString(
textX, y + textOffset + Utils.FONT_MEDIUM.getLineHeight(),
String.format("300:%d 100:%d 50:%d Miss:%d%s", hit300, hit100, hit50, miss, player),
Color.white
);
Fonts.SMALL.drawString(textX, y + textOffset + Fonts.MEDIUM.getLineHeight(),
String.format("300:%d 100:%d 50:%d Miss:%d%s", hit300, hit100, hit50, miss, player), c);
// mods
StringBuilder sb = new StringBuilder();
@@ -296,39 +300,30 @@ public class ScoreData implements Comparable<ScoreData> {
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
String modString = sb.toString();
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(modString),
y + marginY, modString, Color.white
);
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(modString), y + marginY, modString, c);
}
// accuracy
String accuracy = String.format("%.2f%%", getScorePercent());
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(accuracy),
y + marginY + Utils.FONT_DEFAULT.getLineHeight(),
accuracy, Color.white
);
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(accuracy), y + marginY + Fonts.DEFAULT.getLineHeight(), accuracy, c);
// score difference
String diff = (prevScore < 0 || score < prevScore) ?
"-" : String.format("+%s", NumberFormat.getNumberInstance().format(score - prevScore));
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(diff),
y + marginY + Utils.FONT_DEFAULT.getLineHeight() * 2,
diff, Color.white
);
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(diff), y + marginY + Fonts.DEFAULT.getLineHeight() * 2, diff, c);
// time since
if (getTimeSince() != null) {
Image clock = GameImage.HISTORY.getImage();
clock.drawCentered(baseX + buttonWidth * 1.02f + clock.getWidth() / 2f, midY);
Utils.FONT_DEFAULT.drawString(
baseX + buttonWidth * 1.03f + clock.getWidth(),
midY - Utils.FONT_DEFAULT.getLineHeight() / 2f,
getTimeSince(), Color.white
clock.drawCentered(x + buttonWidth * 1.02f + clock.getWidth() / 2f, midY);
Fonts.DEFAULT.drawString(
x + buttonWidth * 1.03f + clock.getWidth(),
midY - Fonts.DEFAULT.getLineHeight() / 2f,
getTimeSince(), c
);
}
c.a = oldAlpha;
}
/**

View File

@@ -24,14 +24,15 @@ import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.downloads.Download;
import itdelatrisu.opsu.downloads.DownloadNode;
import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.UI;
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.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -43,13 +44,10 @@ 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 java.util.jar.JarFile;
import javax.imageio.ImageIO;
@@ -60,16 +58,10 @@ import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.font.effects.ColorEffect;
import org.newdawn.slick.font.effects.Effect;
import org.newdawn.slick.state.StateBasedGame;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
import com.sun.jna.platform.FileUtils;
@@ -77,34 +69,6 @@ import com.sun.jna.platform.FileUtils;
* Contains miscellaneous utilities.
*/
public class Utils {
/** Game colors. */
public static final Color
COLOR_BLACK_ALPHA = new Color(0, 0, 0, 0.5f),
COLOR_WHITE_ALPHA = new Color(255, 255, 255, 0.5f),
COLOR_BLUE_DIVIDER = new Color(49, 94, 237),
COLOR_BLUE_BACKGROUND = new Color(74, 130, 255),
COLOR_BLUE_BUTTON = new Color(40, 129, 237),
COLOR_ORANGE_BUTTON = new Color(200, 90, 3),
COLOR_YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
COLOR_WHITE_FADE = new Color(255, 255, 255, 1f),
COLOR_RED_HOVER = new Color(255, 112, 112),
COLOR_GREEN = new Color(137, 201, 79),
COLOR_LIGHT_ORANGE = new Color(255,192,128),
COLOR_LIGHT_GREEN = new Color(128,255,128),
COLOR_LIGHT_BLUE = new Color(128,128,255),
COLOR_GREEN_SEARCH = new Color(173, 255, 47),
COLOR_DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f),
COLOR_RED_HIGHLIGHT = new Color(246, 154, 161),
COLOR_BLUE_HIGHLIGHT = new Color(173, 216, 230);
/** Game fonts. */
public static UnicodeFont
FONT_DEFAULT, FONT_BOLD,
FONT_XLARGE, FONT_LARGE, FONT_MEDIUM, FONT_SMALL;
/** Set of all Unicode strings already loaded per font. */
private static HashMap<UnicodeFont, HashSet<String>> loadedGlyphs = new HashMap<UnicodeFont, HashSet<String>>();
/**
* List of illegal filename characters.
* @see #cleanFileName(String, char)
@@ -115,7 +79,7 @@ public class Utils {
24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47
};
static {
Arrays.sort(illegalChars);
Arrays.sort(illegalChars);
}
// game-related variables
@@ -128,10 +92,8 @@ public class Utils {
* Initializes game settings and class data.
* @param container the game container
* @param game the game object
* @throws SlickException
*/
public static void init(GameContainer container, StateBasedGame game)
throws SlickException {
public static void init(GameContainer container, StateBasedGame game) {
input = container.getInput();
int width = container.getWidth();
int height = container.getHeight();
@@ -149,23 +111,8 @@ public class Utils {
GameImage.init(width, height);
// create fonts
float fontBase = 12f * GameImage.getUIscale();
try {
Font javaFont = Font.createFont(Font.TRUETYPE_FONT, ResourceLoader.getResourceAsStream(Options.FONT_NAME));
Font font = javaFont.deriveFont(Font.PLAIN, (int) (fontBase * 4 / 3));
FONT_DEFAULT = new UnicodeFont(font);
FONT_BOLD = new UnicodeFont(font.deriveFont(Font.BOLD));
FONT_XLARGE = new UnicodeFont(font.deriveFont(fontBase * 3));
FONT_LARGE = new UnicodeFont(font.deriveFont(fontBase * 2));
FONT_MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2));
FONT_SMALL = new UnicodeFont(font.deriveFont(fontBase));
ColorEffect colorEffect = new ColorEffect();
loadFont(FONT_DEFAULT, colorEffect);
loadFont(FONT_BOLD, colorEffect);
loadFont(FONT_XLARGE, colorEffect);
loadFont(FONT_LARGE, colorEffect);
loadFont(FONT_MEDIUM, colorEffect);
loadFont(FONT_SMALL, colorEffect);
Fonts.init();
} catch (Exception e) {
ErrorHandler.error("Failed to load fonts.", e, true);
}
@@ -206,36 +153,18 @@ public class 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
* 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 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;
public static int clamp(int val, int low, int high) {
if (val < low)
return low;
if (val > high)
return high;
return val;
}
@@ -269,6 +198,13 @@ public class Utils {
return (float) Math.sqrt((v1 * v1) + (v2 * v2));
}
/**
* Linear interpolation of a and b at t.
*/
public static float lerp(float a, float b, float t) {
return a * (1 - t) + b * t;
}
/**
* Returns true if a game input key is pressed (mouse/keyboard left/right).
* @return true if pressed
@@ -289,11 +225,9 @@ public class Utils {
public static void takeScreenShot() {
// create the screenshot directory
File dir = Options.getScreenshotDir();
if (!dir.isDirectory()) {
if (!dir.mkdir()) {
ErrorHandler.error("Failed to create screenshot directory.", null, false);
return;
}
if (!dir.isDirectory() && !dir.mkdir()) {
ErrorHandler.error(String.format("Failed to create screenshot directory at '%s'.", dir.getAbsolutePath()), null, false);
return;
}
// create file name
@@ -333,54 +267,6 @@ public class Utils {
}.start();
}
/**
* Loads a Unicode font.
* @param font the font to load
* @param effect the font effect
* @throws SlickException
*/
@SuppressWarnings("unchecked")
private static void loadFont(UnicodeFont font, Effect effect) throws SlickException {
font.addAsciiGlyphs();
font.getEffects().add(effect);
font.loadGlyphs();
}
/**
* Adds and loads glyphs for a beatmap's Unicode title and artist strings.
* @param font the font to add the glyphs to
* @param title the title string
* @param artist the artist string
*/
public static void loadGlyphs(UnicodeFont font, String title, String artist) {
// get set of added strings
HashSet<String> set = loadedGlyphs.get(font);
if (set == null) {
set = new HashSet<String>();
loadedGlyphs.put(font, set);
}
// add glyphs if not in set
boolean glyphsAdded = false;
if (title != null && !title.isEmpty() && !set.contains(title)) {
font.addGlyphs(title);
set.add(title);
glyphsAdded = true;
}
if (artist != null && !artist.isEmpty() && !set.contains(artist)) {
font.addGlyphs(artist);
set.add(artist);
glyphsAdded = true;
}
if (glyphsAdded) {
try {
font.loadGlyphs();
} catch (SlickException e) {
Log.warn("Failed to load glyphs.", e);
}
}
}
/**
* Returns a human-readable representation of a given number of bytes.
* @param bytes the number of bytes
@@ -407,15 +293,15 @@ public class Utils {
return null;
boolean doReplace = (replace > 0 && Arrays.binarySearch(illegalChars, replace) < 0);
StringBuilder cleanName = new StringBuilder();
for (int i = 0, n = badFileName.length(); i < n; i++) {
int c = badFileName.charAt(i);
if (Arrays.binarySearch(illegalChars, c) < 0)
cleanName.append((char) c);
else if (doReplace)
cleanName.append(replace);
}
return cleanName.toString();
StringBuilder cleanName = new StringBuilder();
for (int i = 0, n = badFileName.length(); i < n; i++) {
int c = badFileName.charAt(i);
if (Arrays.binarySearch(illegalChars, c) < 0)
cleanName.append((char) c);
else if (doReplace)
cleanName.append(replace);
}
return cleanName.toString();
}
/**
@@ -474,50 +360,12 @@ public class Utils {
dir.delete();
}
/**
* Wraps the given string into a list of split lines based on the width.
* @param text the text to split
* @param font the font used to draw the string
* @param width the maximum width of a line
* @return the list of split strings
* @author davedes (http://slick.ninjacave.com/forum/viewtopic.php?t=3778)
*/
public static List<String> wrap(String text, org.newdawn.slick.Font font, int width) {
List<String> list = new ArrayList<String>();
String str = text;
String line = "";
int i = 0;
int lastSpace = -1;
while (i < str.length()) {
char c = str.charAt(i);
if (Character.isWhitespace(c))
lastSpace = i;
String append = line + c;
if (font.getWidth(append) > width) {
int split = (lastSpace != -1) ? lastSpace : i;
int splitTrimmed = split;
if (lastSpace != -1 && split < str.length() - 1)
splitTrimmed++;
list.add(str.substring(0, split));
str = str.substring(splitTrimmed);
line = "";
i = 0;
lastSpace = -1;
} else {
line = append;
i++;
}
}
if (str.length() != 0)
list.add(str);
return list;
}
/**
* Returns a the contents of a URL as a string.
* @param url the remote URL
* @return the contents as a string, or null if any error occurred
* @author Roland Illig (http://stackoverflow.com/a/4308662)
* @throws IOException if an I/O exception occurs
*/
public static String readDataFromUrl(URL url) throws IOException {
// open connection
@@ -553,6 +401,7 @@ public class Utils {
* Returns a JSON object from a URL.
* @param url the remote URL
* @return the JSON object, or null if an error occurred
* @throws IOException if an I/O exception occurs
*/
public static JSONObject readJsonObjectFromUrl(URL url) throws IOException {
String s = Utils.readDataFromUrl(url);
@@ -571,6 +420,7 @@ public class Utils {
* Returns a JSON array from a URL.
* @param url the remote URL
* @return the JSON array, or null if an error occurred
* @throws IOException if an I/O exception occurs
*/
public static JSONArray readJsonArrayFromUrl(URL url) throws IOException {
String s = Utils.readDataFromUrl(url);
@@ -640,32 +490,6 @@ public class Utils {
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);
}
/**
* Returns whether or not the application is running within a JAR.
* @return true if JAR, false if file
@@ -674,8 +498,25 @@ public class Utils {
return Opsu.class.getResource(String.format("%s.class", Opsu.class.getSimpleName())).toString().startsWith("jar:");
}
/**
* Returns the JarFile for the application.
* @return the JAR file, or null if it could not be determined
*/
public static JarFile getJarFile() {
if (!isJarRunning())
return null;
try {
return new JarFile(new File(Opsu.class.getProtectionDomain().getCodeSource().getLocation().toURI()), false);
} catch (URISyntaxException | IOException e) {
Log.error("Could not determine the JAR file.", e);
return null;
}
}
/**
* Returns the directory where the application is being run.
* @return the directory, or null if it could not be determined
*/
public static File getRunningDirectory() {
try {
@@ -695,4 +536,29 @@ public class Utils {
public static boolean parseBoolean(String s) {
return (Integer.parseInt(s) == 1);
}
/**
* Returns the git hash of the remote-tracking branch 'origin/master' from the
* most recent update to the working directory (e.g. fetch or successful push).
* @return the 40-character SHA-1 hash, or null if it could not be determined
*/
public static String getGitHash() {
if (isJarRunning())
return null;
File f = new File(".git/refs/remotes/origin/master");
if (!f.isFile())
return null;
try (BufferedReader in = new BufferedReader(new FileReader(f))) {
char[] sha = new char[40];
if (in.read(sha, 0, sha.length) < sha.length)
return null;
for (int i = 0; i < sha.length; i++) {
if (Character.digit(sha[i], 16) == -1)
return null;
}
return String.valueOf(sha);
} catch (IOException e) {
return null;
}
}
}

View File

@@ -40,10 +40,10 @@ public enum HitSound implements SoundController.SoundComponent {
// TAIKO ("taiko", 4);
/** The sample set name. */
private String name;
private final String name;
/** The sample set index. */
private int index;
private final int index;
/** Total number of sample sets. */
public static final int SIZE = values().length;
@@ -77,7 +77,7 @@ public enum HitSound implements SoundController.SoundComponent {
private static SampleSet currentDefaultSampleSet = SampleSet.NORMAL;
/** The file name. */
private String filename;
private final String filename;
/** The Clip associated with the hit sound. */
private HashMap<SampleSet, MultiClip> clips;

View File

@@ -49,12 +49,15 @@ public class MultiClip {
private byte[] audioData;
/** The name given to this clip. */
private String name;
private final String name;
/**
* Constructor.
* @param name the clip name
* @param audioIn the associated AudioInputStream
* @throws IOException if an input or output error occurs
* @throws LineUnavailableException if a clip object is not available or
* if the line cannot be opened due to resource restrictions
*/
public MultiClip(String name, AudioInputStream audioIn) throws IOException, LineUnavailableException {
this.name = name;
@@ -105,6 +108,8 @@ public class MultiClip {
* Plays the clip with the specified volume.
* @param volume the volume the play at
* @param listener the line listener
* @throws LineUnavailableException if a clip object is not available or
* if the line cannot be opened due to resource restrictions
*/
public void start(float volume, LineListener listener) throws LineUnavailableException {
Clip clip = getClip();
@@ -130,6 +135,8 @@ public class MultiClip {
* If no clip is available, then a new one is created if under MAX_CLIPS.
* Otherwise, an existing clip will be returned.
* @return the Clip to play
* @throws LineUnavailableException if a clip object is not available or
* if the line cannot be opened due to resource restrictions
*/
private Clip getClip() throws LineUnavailableException {
// TODO:

View File

@@ -22,6 +22,7 @@ import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.ui.UI;
import java.io.File;
import java.io.IOException;
@@ -41,6 +42,7 @@ import org.newdawn.slick.MusicListener;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.openal.Audio;
import org.newdawn.slick.openal.SoundStore;
import org.newdawn.slick.util.ResourceLoader;
import org.tritonus.share.sampled.file.TAudioFileFormat;
/**
@@ -87,6 +89,13 @@ public class MusicController {
public static void play(final Beatmap beatmap, final boolean loop, final boolean preview) {
// new track: load and play
if (lastBeatmap == null || !beatmap.audioFilename.equals(lastBeatmap.audioFilename)) {
final File audioFile = beatmap.audioFilename;
if (!audioFile.isFile() && !ResourceLoader.resourceExists(audioFile.getPath())) {
UI.sendBarNotification(String.format("Could not find track '%s'.", audioFile.getName()));
System.out.println(beatmap);
return;
}
reset();
System.gc();
@@ -96,7 +105,7 @@ public class MusicController {
trackLoader = new Thread() {
@Override
public void run() {
loadTrack(beatmap.audioFilename, (preview) ? beatmap.previewTime : 0, loop);
loadTrack(audioFile, (preview) ? beatmap.previewTime : 0, loop);
}
};
trackLoader.start();
@@ -221,6 +230,7 @@ public class MusicController {
/**
* Fades out the track.
* @param duration the fade time (in ms)
*/
public static void fadeOut(int duration) {
if (isPlaying())

View File

@@ -59,6 +59,9 @@ public class SoundController {
/** Sample volume multiplier, from timing points [0, 1]. */
private static float sampleVolumeMultiplier = 1f;
/** Whether all sounds are muted. */
private static boolean isMuted;
/** The name of the current sound file being loaded. */
private static String currentFileName;
@@ -261,7 +264,7 @@ public class SoundController {
if (clip == null) // clip failed to load properly
return;
if (volume > 0f) {
if (volume > 0f && !isMuted) {
try {
clip.start(volume, listener);
} catch (LineUnavailableException e) {
@@ -317,6 +320,12 @@ public class SoundController {
playClip(s.getClip(), Options.getHitSoundVolume() * sampleVolumeMultiplier * Options.getMasterVolume(), null);
}
/**
* Mutes or unmutes all sounds (hit sounds and sound effects).
* @param mute true to mute, false to unmute
*/
public static void mute(boolean mute) { isMuted = mute; }
/**
* Returns the name of the current file being loaded, or null if none.
*/

View File

@@ -42,7 +42,7 @@ public enum SoundEffect implements SoundController.SoundComponent {
SPINNERSPIN ("spinnerspin");
/** The file name. */
private String filename;
private final String filename;
/** The Clip associated with the sound effect. */
private MultiClip clip;

View File

@@ -23,6 +23,7 @@ import itdelatrisu.opsu.Options;
import java.io.File;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Map;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
@@ -36,16 +37,37 @@ public class Beatmap implements Comparable<Beatmap> {
public static final byte MODE_OSU = 0, MODE_TAIKO = 1, MODE_CTB = 2, MODE_MANIA = 3;
/** Background image cache. */
private static final BeatmapImageCache bgImageCache = new BeatmapImageCache();
@SuppressWarnings("serial")
private static final LRUCache<File, ImageLoader> bgImageCache = new LRUCache<File, ImageLoader>(10) {
@Override
public void eldestRemoved(Map.Entry<File, ImageLoader> eldest) {
if (eldest.getKey() == lastBG)
lastBG = null;
ImageLoader imageLoader = eldest.getValue();
imageLoader.destroy();
}
};
/** The last background image loaded. */
private static File lastBG;
/**
* Returns the background image cache.
* Clears the background image cache.
* <p>
* NOTE: This does NOT destroy the images in the cache, and will cause
* memory leaks if all images have not been destroyed.
*/
public static BeatmapImageCache getBackgroundImageCache() { return bgImageCache; }
public static void clearBackgroundImageCache() { bgImageCache.clear(); }
/** The OSU File object associated with this beatmap. */
private File file;
/** MD5 hash of this file. */
public String md5Hash;
/** The star rating. */
public double starRating = -1;
/**
* [General]
*/
@@ -138,7 +160,7 @@ public class Beatmap implements Comparable<Beatmap> {
public float HPDrainRate = 5f;
/** CS: Size of circles and sliders (0:large ~ 10:small). */
public float circleSize = 4f;
public float circleSize = 5f;
/** OD: Affects timing window, spinners, and approach speed (0:easy ~ 10:hard). */
public float overallDifficulty = 5f;
@@ -147,7 +169,7 @@ public class Beatmap implements Comparable<Beatmap> {
public float approachRate = -1f;
/** Slider movement speed multiplier. */
public float sliderMultiplier = 1f;
public float sliderMultiplier = 1.4f;
/** Rate at which slider ticks are placed (x per beat). */
public float sliderTickRate = 1f;
@@ -185,9 +207,6 @@ public class Beatmap implements Comparable<Beatmap> {
/** Slider border color. If null, the skin value is used. */
public Color sliderBorder;
/** MD5 hash of this file. */
public String md5Hash;
/**
* [HitObjects]
*/
@@ -255,47 +274,74 @@ public class Beatmap implements Comparable<Beatmap> {
}
/**
* Draws the beatmap background.
* Loads the beatmap background image.
*/
public void loadBackground() {
if (bg == null || bgImageCache.containsKey(bg) || !bg.isFile())
return;
if (lastBG != null) {
ImageLoader lastImageLoader = bgImageCache.get(lastBG);
if (lastImageLoader != null && lastImageLoader.isLoading()) {
lastImageLoader.interrupt(); // only allow loading one image at a time
bgImageCache.remove(lastBG);
}
}
ImageLoader imageLoader = new ImageLoader(bg);
bgImageCache.put(bg, imageLoader);
imageLoader.load(true);
lastBG = bg;
}
/**
* Returns whether the beatmap background image is currently loading.
* @return true if loading
*/
public boolean isBackgroundLoading() {
if (bg == null)
return false;
ImageLoader imageLoader = bgImageCache.get(bg);
return (imageLoader != null && imageLoader.isLoading());
}
/**
* Draws the beatmap background image.
* @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) {
public boolean drawBackground(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
ImageLoader imageLoader = bgImageCache.get(bg);
if (imageLoader == null)
return false;
Image bgImage = imageLoader.getImage();
if (bgImage == null)
return true;
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);
return true;
}

View File

@@ -0,0 +1,544 @@
/*
* 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.db.BeatmapDB;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.Vec2f;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.newdawn.slick.util.Log;
/**
* osu!tp's beatmap difficulty algorithm.
*
* @author Tom94 (https://github.com/Tom94/AiModtpDifficultyCalculator)
*/
public class BeatmapDifficultyCalculator {
/** Difficulty types. */
public static final int DIFFICULTY_SPEED = 0, DIFFICULTY_AIM = 1;
/** The star scaling factor. */
private static final double STAR_SCALING_FACTOR = 0.045;
/** The scaling factor that favors extremes. */
private static final double EXTREME_SCALING_FACTOR = 0.5;
/** The playfield width. */
private static final float PLAYFIELD_WIDTH = 512f;
/**
* In milliseconds. For difficulty calculation we will only look at the highest strain value in each
* time interval of size STRAIN_STEP.This is to eliminate higher influence of stream over aim by simply
* having more HitObjects with high strain. The higher this value, the less strains there will be,
* indirectly giving long beatmaps an advantage.
*/
private static final double STRAIN_STEP = 400;
/** The weighting of each strain value decays to 0.9 * its previous value. */
private static final double DECAY_WEIGHT = 0.9;
/** The beatmap. */
private final Beatmap beatmap;
/** The beatmap's hit objects. */
private tpHitObject[] tpHitObjects;
/** The computed star rating. */
private double starRating = -1;
/** The computed difficulties, indexed by the {@code DIFFICULTY_*} constants. */
private double[] difficulties = { -1, -1 };
/** The computed stars, indexed by the {@code DIFFICULTY_*} constants. */
private double[] stars = { -1, -1 };
/**
* Constructor. Call {@link #calculate()} to run all computations.
* <p>
* If any parts of the beatmap have not yet been loaded (e.g. timing points,
* hit objects), they will be loaded here.
* @param beatmap the beatmap
*/
public BeatmapDifficultyCalculator(Beatmap beatmap) {
this.beatmap = beatmap;
if (beatmap.timingPoints == null)
BeatmapDB.load(beatmap, BeatmapDB.LOAD_ARRAY);
BeatmapParser.parseHitObjects(beatmap);
}
/**
* Returns the beatmap's star rating.
*/
public double getStarRating() { return starRating; }
/**
* Returns the difficulty value for a difficulty type.
* @param type the difficulty type ({@code DIFFICULTY_* constant})
*/
public double getDifficulty(int type) { return difficulties[type]; }
/**
* Returns the star value for a difficulty type.
* @param type the difficulty type ({@code DIFFICULTY_* constant})
*/
public double getStars(int type) { return stars[type]; }
/**
* Calculates the difficulty values and star ratings for the beatmap.
*/
public void calculate() {
if (beatmap.objects == null || beatmap.timingPoints == null) {
Log.error(String.format("Trying to calculate difficulty values for beatmap '%s' with %s not yet loaded.",
beatmap.toString(), (beatmap.objects == null) ? "hit objects" : "timing points"));
return;
}
// Fill our custom tpHitObject class, that carries additional information
// TODO: apply hit object stacking algorithm?
HitObject[] hitObjects = beatmap.objects;
this.tpHitObjects = new tpHitObject[hitObjects.length];
float circleRadius = (PLAYFIELD_WIDTH / 16.0f) * (1.0f - 0.7f * (beatmap.circleSize - 5.0f) / 5.0f);
int timingPointIndex = 0;
float beatLengthBase = 1, beatLength = 1;
if (!beatmap.timingPoints.isEmpty()) {
TimingPoint timingPoint = beatmap.timingPoints.get(0);
if (!timingPoint.isInherited()) {
beatLengthBase = beatLength = timingPoint.getBeatLength();
timingPointIndex++;
}
}
for (int i = 0; i < hitObjects.length; i++) {
HitObject hitObject = hitObjects[i];
// pass beatLength to hit objects
int hitObjectTime = hitObject.getTime();
while (timingPointIndex < beatmap.timingPoints.size()) {
TimingPoint timingPoint = beatmap.timingPoints.get(timingPointIndex);
if (timingPoint.getTime() > hitObjectTime)
break;
if (!timingPoint.isInherited())
beatLengthBase = beatLength = timingPoint.getBeatLength();
else
beatLength = beatLengthBase * timingPoint.getSliderMultiplier();
timingPointIndex++;
}
tpHitObjects[i] = new tpHitObject(hitObject, circleRadius, beatmap, beatLength);
}
if (!calculateStrainValues()) {
Log.error("Could not compute strain values. Aborting difficulty calculation.");
return;
}
// OverallDifficulty is not considered in this algorithm and neither is HpDrainRate.
// That means, that in this form the algorithm determines how hard it physically is
// to play the map, assuming, that too much of an error will not lead to a death.
// It might be desirable to include OverallDifficulty into map difficulty, but in my
// personal opinion it belongs more to the weighting of the actual performance
// and is superfluous in the beatmap difficulty rating.
// If it were to be considered, then I would look at the hit window of normal HitCircles only,
// since Sliders and Spinners are (almost) "free" 300s and take map length into account as well.
difficulties[DIFFICULTY_SPEED] = calculateDifficulty(DIFFICULTY_SPEED);
difficulties[DIFFICULTY_AIM] = calculateDifficulty(DIFFICULTY_AIM);
// The difficulty can be scaled by any desired metric.
// In osu!tp it gets squared to account for the rapid increase in difficulty as the
// limit of a human is approached. (Of course it also gets scaled afterwards.)
// It would not be suitable for a star rating, therefore:
// The following is a proposal to forge a star rating from 0 to 5. It consists of taking
// the square root of the difficulty, since by simply scaling the easier
// 5-star maps would end up with one star.
stars[DIFFICULTY_SPEED] = Math.sqrt(difficulties[DIFFICULTY_SPEED]) * STAR_SCALING_FACTOR;
stars[DIFFICULTY_AIM] = Math.sqrt(difficulties[DIFFICULTY_AIM]) * STAR_SCALING_FACTOR;
// Again, from own observations and from the general opinion of the community
// a map with high speed and low aim (or vice versa) difficulty is harder,
// than a map with mediocre difficulty in both. Therefore we can not just add
// both difficulties together, but will introduce a scaling that favors extremes.
// Another approach to this would be taking Speed and Aim separately to a chosen
// power, which again would be equivalent. This would be more convenient if
// the hit window size is to be considered as well.
// Note: The star rating is tuned extremely tight! Airman (/b/104229) and
// Freedom Dive (/b/126645), two of the hardest ranked maps, both score ~4.66 stars.
// Expect the easier kind of maps that officially get 5 stars to obtain around 2 by
// this metric. The tutorial still scores about half a star.
// Tune by yourself as you please. ;)
this.starRating = stars[DIFFICULTY_SPEED] + stars[DIFFICULTY_AIM] +
Math.abs(stars[DIFFICULTY_SPEED] - stars[DIFFICULTY_AIM]) * EXTREME_SCALING_FACTOR;
}
/**
* Computes the strain values for the beatmap.
* @return true if successful, false otherwise
*/
private boolean calculateStrainValues() {
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from
// the strain value of CurrentHitObject and environment.
if (tpHitObjects.length == 0) {
Log.warn("Can not compute difficulty of empty beatmap.");
return false;
}
tpHitObject currentHitObject = tpHitObjects[0];
tpHitObject nextHitObject;
int index = 0;
// First hitObject starts at strain 1. 1 is the default for strain values,
// so we don't need to set it here. See tpHitObject.
while (++index < tpHitObjects.length) {
nextHitObject = tpHitObjects[index];
nextHitObject.calculateStrains(currentHitObject);
currentHitObject = nextHitObject;
}
return true;
}
/**
* Calculates the difficulty value for a difficulty type.
* @param type the difficulty type ({@code DIFFICULTY_* constant})
* @return the difficulty value
*/
private double calculateDifficulty(int type) {
// Find the highest strain value within each strain step
List<Double> highestStrains = new ArrayList<Double>();
double intervalEndTime = STRAIN_STEP;
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
tpHitObject previousHitObject = null;
for (int i = 0; i < tpHitObjects.length; i++) {
tpHitObject hitObject = tpHitObjects[i];
// While we are beyond the current interval push the currently available maximum to our strain list
while (hitObject.baseHitObject.getTime() > intervalEndTime) {
highestStrains.add(maximumStrain);
// The maximum strain of the next interval is not zero by default! We need to take the last
// hitObject we encountered, take its strain and apply the decay until the beginning of the next interval.
if (previousHitObject == null)
maximumStrain = 0;
else {
double decay = Math.pow(tpHitObject.DECAY_BASE[type], (intervalEndTime - previousHitObject.baseHitObject.getTime()) / 1000);
maximumStrain = previousHitObject.getStrain(type) * decay;
}
// Go to the next time interval
intervalEndTime += STRAIN_STEP;
}
// Obtain maximum strain
if (hitObject.getStrain(type) > maximumStrain)
maximumStrain = hitObject.getStrain(type);
previousHitObject = hitObject;
}
// Build the weighted sum over the highest strains for each interval
double difficulty = 0;
double weight = 1;
Collections.sort(highestStrains, Collections.reverseOrder()); // Sort from highest to lowest strain.
for (double strain : highestStrains) {
difficulty += weight * strain;
weight *= DECAY_WEIGHT;
}
return difficulty;
}
}
/**
* Hit object helper class for calculating strains.
*/
class tpHitObject {
/**
* Factor by how much speed / aim strain decays per second. Those values are results
* of tweaking a lot and taking into account general feedback.
* Opinionated observation: Speed is easier to maintain than accurate jumps.
*/
public static final double[] DECAY_BASE = { 0.3, 0.15 };
/** Almost the normed diameter of a circle (104 osu pixel). That is -after- position transforming. */
private static final double ALMOST_DIAMETER = 90;
/**
* Pseudo threshold values to distinguish between "singles" and "streams".
* Of course the border can not be defined clearly, therefore the algorithm
* has a smooth transition between those values. They also are based on tweaking
* and general feedback.
*/
private static final double STREAM_SPACING_TRESHOLD = 110, SINGLE_SPACING_TRESHOLD = 125;
/**
* Scaling values for weightings to keep aim and speed difficulty in balance.
* Found from testing a very large map pool (containing all ranked maps) and
* keeping the average values the same.
*/
private static final double[] SPACING_WEIGHT_SCALING = { 1400, 26.25 };
/**
* In milliseconds. The smaller the value, the more accurate sliders are approximated.
* 0 leads to an infinite loop, so use something bigger.
*/
private static final int LAZY_SLIDER_STEP_LENGTH = 1;
/** The base hit object. */
public final HitObject baseHitObject;
/** The strain values, indexed by the {@code DIFFICULTY_*} constants. */
private double[] strains = { 1, 1 };
/** The normalized start and end positions. */
private Vec2f normalizedStartPosition, normalizedEndPosition;
/** The slider lengths. */
private float lazySliderLengthFirst = 0, lazySliderLengthSubsequent = 0;
/**
* Constructor.
* @param baseHitObject the base hit object
* @param circleRadius the circle radius
* @param beatmap the beatmap that contains the hit object
* @param beatLength the current beat length
*/
public tpHitObject(HitObject baseHitObject, float circleRadius, Beatmap beatmap, float beatLength) {
this.baseHitObject = baseHitObject;
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = (52.0f / circleRadius);
normalizedStartPosition = new Vec2f(baseHitObject.getX(), baseHitObject.getY()).scale(scalingFactor);
// Calculate approximation of lazy movement on the slider
if (baseHitObject.isSlider()) {
tpSlider slider = new tpSlider(baseHitObject, beatmap.sliderMultiplier, beatLength);
// Not sure if this is correct, but here we do not need 100% exact values. This comes pretty darn close in my tests.
float sliderFollowCircleRadius = circleRadius * 3;
int segmentLength = slider.getSegmentLength(); // baseHitObject.Length / baseHitObject.SegmentCount;
int segmentEndTime = baseHitObject.getTime() + segmentLength;
// For simplifying this step we use actual osu! coordinates and simply scale the length,
// that we obtain by the ScalingFactor later
Vec2f cursorPos = new Vec2f(baseHitObject.getX(), baseHitObject.getY());
// Actual computation of the first lazy curve
for (int time = baseHitObject.getTime() + LAZY_SLIDER_STEP_LENGTH; time < segmentEndTime; time += LAZY_SLIDER_STEP_LENGTH) {
Vec2f difference = slider.getPositionAtTime(time).sub(cursorPos);
float distance = difference.len();
// Did we move away too far?
if (distance > sliderFollowCircleRadius) {
// Yep, we need to move the cursor
difference.normalize(); // Obtain the direction of difference. We do no longer need the actual difference
distance -= sliderFollowCircleRadius;
cursorPos.add(difference.cpy().scale(distance)); // We move the cursor just as far as needed to stay in the follow circle
lazySliderLengthFirst += distance;
}
}
lazySliderLengthFirst *= scalingFactor;
// If we have an odd amount of repetitions the current position will be the end of the slider.
// Note that this will -always- be triggered if baseHitObject.SegmentCount <= 1, because
// baseHitObject.SegmentCount can not be smaller than 1. Therefore normalizedEndPosition will
// always be initialized
if (baseHitObject.getRepeatCount() % 2 == 1)
normalizedEndPosition = cursorPos.cpy().scale(scalingFactor);
// If we have more than one segment, then we also need to compute the length of subsequent
// lazy curves. They are different from the first one, since the first one starts right
// at the beginning of the slider.
if (baseHitObject.getRepeatCount() > 1) {
// Use the next segment
segmentEndTime += segmentLength;
for (int time = segmentEndTime - segmentLength + LAZY_SLIDER_STEP_LENGTH; time < segmentEndTime; time += LAZY_SLIDER_STEP_LENGTH) {
Vec2f difference = slider.getPositionAtTime(time).sub(cursorPos);
float distance = difference.len();
// Did we move away too far?
if (distance > sliderFollowCircleRadius) {
// Yep, we need to move the cursor
difference.normalize(); // Obtain the direction of difference. We do no longer need the actual difference
distance -= sliderFollowCircleRadius;
cursorPos.add(difference.cpy().scale(distance)); // We move the cursor just as far as needed to stay in the follow circle
lazySliderLengthSubsequent += distance;
}
}
lazySliderLengthSubsequent *= scalingFactor;
// If we have an even amount of repetitions the current position will be the end of the slider
if (baseHitObject.getRepeatCount() % 2 == 0) // == 1)
normalizedEndPosition = cursorPos.cpy().scale(scalingFactor);
}
} else {
// We have a normal HitCircle or a spinner
normalizedEndPosition = normalizedStartPosition.cpy(); //baseHitObject.EndPosition * ScalingFactor;
}
}
/**
* Returns the strain value for a difficulty type.
* @param type the difficulty type ({@code DIFFICULTY_* constant})
*/
public double getStrain(int type) { return strains[type]; }
/**
* Calculates the strain values given the previous hit object.
* @param previousHitObject the previous hit object
*/
public void calculateStrains(tpHitObject previousHitObject) {
calculateSpecificStrain(previousHitObject, BeatmapDifficultyCalculator.DIFFICULTY_SPEED);
calculateSpecificStrain(previousHitObject, BeatmapDifficultyCalculator.DIFFICULTY_AIM);
}
/**
* Returns the spacing weight for a distance.
* @param distance the distance
* @param type the difficulty type ({@code DIFFICULTY_* constant})
*/
private static double spacingWeight(double distance, int type) {
// Caution: The subjective values are strong with this one
switch (type) {
case BeatmapDifficultyCalculator.DIFFICULTY_SPEED:
double weight;
if (distance > SINGLE_SPACING_TRESHOLD)
weight = 2.5;
else if (distance > STREAM_SPACING_TRESHOLD)
weight = 1.6 + 0.9 * (distance - STREAM_SPACING_TRESHOLD) / (SINGLE_SPACING_TRESHOLD - STREAM_SPACING_TRESHOLD);
else if (distance > ALMOST_DIAMETER)
weight = 1.2 + 0.4 * (distance - ALMOST_DIAMETER) / (STREAM_SPACING_TRESHOLD - ALMOST_DIAMETER);
else if (distance > ALMOST_DIAMETER / 2)
weight = 0.95 + 0.25 * (distance - (ALMOST_DIAMETER / 2)) / (ALMOST_DIAMETER / 2);
else
weight = 0.95;
return weight;
case BeatmapDifficultyCalculator.DIFFICULTY_AIM:
return Math.pow(distance, 0.99);
default:
// Should never happen.
return 0;
}
}
/**
* Calculates the strain value for a difficulty type given the previous hit object.
* @param previousHitObject the previous hit object
* @param type the difficulty type ({@code DIFFICULTY_* constant})
*/
private void calculateSpecificStrain(tpHitObject previousHitObject, int type) {
double addition = 0;
double timeElapsed = baseHitObject.getTime() - previousHitObject.baseHitObject.getTime();
double decay = Math.pow(DECAY_BASE[type], timeElapsed / 1000);
if (baseHitObject.isSpinner()) {
// Do nothing for spinners
} else if (baseHitObject.isSlider()) {
switch (type) {
case BeatmapDifficultyCalculator.DIFFICULTY_SPEED:
// For speed strain we treat the whole slider as a single spacing entity,
// since "Speed" is about how hard it is to click buttons fast.
// The spacing weight exists to differentiate between being able to easily
// alternate or having to single.
addition = spacingWeight(previousHitObject.lazySliderLengthFirst +
previousHitObject.lazySliderLengthSubsequent * (Math.max(previousHitObject.baseHitObject.getRepeatCount(), 1) - 1) +
distanceTo(previousHitObject), type) * SPACING_WEIGHT_SCALING[type];
break;
case BeatmapDifficultyCalculator.DIFFICULTY_AIM:
// For Aim strain we treat each slider segment and the jump after the end of
// the slider as separate jumps, since movement-wise there is no difference
// to multiple jumps.
addition = (spacingWeight(previousHitObject.lazySliderLengthFirst, type) +
spacingWeight(previousHitObject.lazySliderLengthSubsequent, type) * (Math.max(previousHitObject.baseHitObject.getRepeatCount(), 1) - 1) +
spacingWeight(distanceTo(previousHitObject), type)) * SPACING_WEIGHT_SCALING[type];
break;
}
} else if (baseHitObject.isCircle()) {
addition = spacingWeight(distanceTo(previousHitObject), type) * SPACING_WEIGHT_SCALING[type];
}
// Scale addition by the time, that elapsed. Filter out HitObjects that are too
// close to be played anyway to avoid crazy values by division through close to zero.
// You will never find maps that require this amongst ranked maps.
addition /= Math.max(timeElapsed, 50);
strains[type] = previousHitObject.strains[type] * decay + addition;
}
/**
* Returns the distance to another hit object.
* @param other the other hit object
*/
public double distanceTo(tpHitObject other) {
// Scale the distance by circle size.
return (normalizedStartPosition.cpy().sub(other.normalizedEndPosition)).len();
}
}
/**
* Slider helper class to fill in some missing pieces needed in the strain calculations.
*/
class tpSlider {
/** The slider start time. */
private final int startTime;
/** The time duration of the slider, in milliseconds. */
private final int sliderTime;
/** The slider Curve. */
private final Curve curve;
/**
* Constructor.
* @param hitObject the hit object
* @param sliderMultiplier the slider movement speed multiplier
* @param beatLength the beat length
*/
public tpSlider(HitObject hitObject, float sliderMultiplier, float beatLength) {
this.startTime = hitObject.getTime();
this.sliderTime = (int) hitObject.getSliderTime(sliderMultiplier, beatLength);
this.curve = hitObject.getSliderCurve(false);
}
/**
* Returns the time duration of a slider segment, in milliseconds.
*/
public int getSegmentLength() { return sliderTime; }
/**
* Returns the coordinates of the slider at a given track position.
* @param time the track position
*/
public Vec2f getPositionAtTime(int time) {
float t = (time - startTime) / sliderTime;
float floor = (float) Math.floor(t);
t = (floor % 2 == 0) ? t - floor : floor + 1 - t;
return curve.pointAt(t);
}
}

View File

@@ -1,86 +0,0 @@
/*
* 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.io.File;
import java.util.LinkedHashMap;
import java.util.Map;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.util.Log;
/**
* LRU cache for beatmap background images.
*/
public class BeatmapImageCache {
/** Maximum number of cached images. */
private static final int MAX_CACHE_SIZE = 10;
/** Map of all loaded background images. */
private LinkedHashMap<File, Image> cache;
/**
* Constructor.
*/
@SuppressWarnings("serial")
public BeatmapImageCache() {
this.cache = new LinkedHashMap<File, Image>(MAX_CACHE_SIZE + 1, 1.1f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<File, Image> eldest) {
if (size() > MAX_CACHE_SIZE) {
// destroy the eldest image
Image img = eldest.getValue();
if (img != null && !img.isDestroyed()) {
try {
img.destroy();
} catch (SlickException e) {
Log.warn(String.format("Failed to destroy image '%s'.", img.getResourceReference()), e);
}
}
return true;
}
return false;
}
};
}
/**
* Returns the image mapped to the specified beatmap.
* @param beatmap the Beatmap
* @return the Image, or {@code null} if no such mapping exists
*/
public Image get(Beatmap beatmap) { return cache.get(beatmap.bg); }
/**
* Creates a mapping from the specified beatmap to the given image.
* @param beatmap the Beatmap
* @param image the Image
* @return the previously mapped Image, or {@code null} if no such mapping existed
*/
public Image put(Beatmap beatmap, Image image) { return cache.put(beatmap.bg, image); }
/**
* Removes all entries from the cache.
* <p>
* NOTE: This does NOT destroy the images in the cache, and will cause
* memory leaks if all images have not been destroyed.
*/
public void clear() { cache.clear(); }
}

View File

@@ -19,6 +19,7 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.io.MD5InputStreamWrapper;
@@ -83,6 +84,10 @@ public class BeatmapParser {
// create a new BeatmapSetList
BeatmapSetList.create();
// create a new watch service
if (Options.isWatchServiceEnabled())
BeatmapWatchService.create();
// parse all directories
parseDirectories(root.listFiles());
}
@@ -110,6 +115,9 @@ public class BeatmapParser {
List<Beatmap> cachedBeatmaps = new LinkedList<Beatmap>(); // loaded from database
List<Beatmap> parsedBeatmaps = new LinkedList<Beatmap>(); // loaded from parser
// watch service
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
// parse directories
BeatmapSetNode lastNode = null;
for (File dir : dirs) {
@@ -134,17 +142,19 @@ public class BeatmapParser {
// check if beatmap is cached
String path = String.format("%s/%s", dir.getName(), file.getName());
if (map.containsKey(path)) {
// check last modified times
long lastModified = map.get(path);
if (lastModified == file.lastModified()) {
// add to cached beatmap list
Beatmap beatmap = new Beatmap(file);
beatmaps.add(beatmap);
cachedBeatmaps.add(beatmap);
continue;
} else
BeatmapDB.delete(dir.getName(), file.getName());
if (map != null) {
Long lastModified = map.get(path);
if (lastModified != null) {
// check last modified times
if (lastModified == file.lastModified()) {
// add to cached beatmap list
Beatmap beatmap = new Beatmap(file);
beatmaps.add(beatmap);
cachedBeatmaps.add(beatmap);
continue;
} else
BeatmapDB.delete(dir.getName(), file.getName());
}
}
// Parse hit objects only when needed to save time/memory.
@@ -162,6 +172,8 @@ public class BeatmapParser {
if (!beatmaps.isEmpty()) {
beatmaps.trimToSize();
allBeatmaps.add(beatmaps);
if (ws != null)
ws.registerAll(dir.toPath());
}
// stop parsing files (interrupted)
@@ -676,10 +688,15 @@ public class BeatmapParser {
beatmap.objects[objectIndex++] = hitObject;
} catch (Exception e) {
Log.warn(String.format("Failed to read hit object '%s' for Beatmap '%s'.",
Log.warn(String.format("Failed to read hit object '%s' for beatmap '%s'.",
line, beatmap.toString()), e);
}
}
// check that all objects were parsed
if (objectIndex != beatmap.objects.length)
ErrorHandler.error(String.format("Parsed %d objects for beatmap '%s', %d objects expected.",
objectIndex, beatmap.toString(), beatmap.objects.length), null, true);
} catch (IOException e) {
ErrorHandler.error(String.format("Failed to read file '%s'.", beatmap.getFile().getAbsolutePath()), e, false);
}
@@ -711,6 +728,7 @@ public class BeatmapParser {
/**
* Returns the file extension of a file.
* @param file the file name
*/
public static String getExtension(String file) {
int i = file.lastIndexOf('.');

View File

@@ -21,14 +21,15 @@ package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameMod;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
/**
* Data type containing all beatmaps in a beatmap set.
*/
public class BeatmapSet {
public class BeatmapSet implements Iterable<Beatmap> {
/** List of associated beatmaps. */
private ArrayList<Beatmap> beatmaps;
private final ArrayList<Beatmap> beatmaps;
/**
* Constructor.
@@ -46,7 +47,7 @@ public class BeatmapSet {
/**
* Returns the beatmap at the given index.
* @param index the beatmap index
* @throws IndexOutOfBoundsException
* @throws IndexOutOfBoundsException if the index is out of range
*/
public Beatmap get(int index) { return beatmaps.get(index); }
@@ -54,10 +55,13 @@ public class BeatmapSet {
* Removes the beatmap at the given index.
* @param index the beatmap index
* @return the removed beatmap
* @throws IndexOutOfBoundsException
* @throws IndexOutOfBoundsException if the index is out of range
*/
public Beatmap remove(int index) { return beatmaps.remove(index); }
@Override
public Iterator<Beatmap> iterator() { return beatmaps.iterator(); }
/**
* Returns an array of strings containing beatmap information.
* <ul>
@@ -65,10 +69,10 @@ public class BeatmapSet {
* <li>1: Mapped by {Creator}
* <li>2: Length: {} BPM: {} Objects: {}
* <li>3: Circles: {} Sliders: {} Spinners: {}
* <li>4: CS:{} HP:{} AR:{} OD:{}
* <li>4: CS:{} HP:{} AR:{} OD:{} Stars:{}
* </ul>
* @param index the beatmap index
* @throws IndexOutOfBoundsException
* @throws IndexOutOfBoundsException if the index is out of range
*/
public String[] getInfo(int index) {
Beatmap beatmap = beatmaps.get(index);
@@ -88,11 +92,12 @@ public class BeatmapSet {
(beatmap.hitObjectCircle + beatmap.hitObjectSlider + beatmap.hitObjectSpinner));
info[3] = String.format("Circles: %d Sliders: %d Spinners: %d",
beatmap.hitObjectCircle, beatmap.hitObjectSlider, beatmap.hitObjectSpinner);
info[4] = String.format("CS:%.1f HP:%.1f AR:%.1f OD:%.1f",
info[4] = String.format("CS:%.1f HP:%.1f AR:%.1f OD:%.1f%s",
Math.min(beatmap.circleSize * multiplier, 10f),
Math.min(beatmap.HPDrainRate * multiplier, 10f),
Math.min(beatmap.approachRate * multiplier, 10f),
Math.min(beatmap.overallDifficulty * multiplier, 10f));
Math.min(beatmap.overallDifficulty * multiplier, 10f),
(beatmap.starRating >= 0) ? String.format(" Stars:%.2f", beatmap.starRating) : "");
return info;
}
@@ -126,7 +131,7 @@ public class BeatmapSet {
return true;
// search: version, tags (remaining beatmaps)
for (int i = 1; i < beatmaps.size(); i++) {
for (int i = 1, n = beatmaps.size(); i < n; i++) {
beatmap = beatmaps.get(i);
if (beatmap.version.toLowerCase().contains(query) ||
beatmap.tags.contains(query))
@@ -138,7 +143,7 @@ public class BeatmapSet {
/**
* Checks whether the beatmap set matches a given condition.
* @param type the condition type (ar, cs, od, hp, bpm, length)
* @param type the condition type (ar, cs, od, hp, bpm, length, star/stars)
* @param operator the operator {@literal (=/==, >, >=, <, <=)}
* @param value the value
* @return true if the condition is met
@@ -154,6 +159,8 @@ public class BeatmapSet {
case "hp": v = beatmap.HPDrainRate; break;
case "bpm": v = beatmap.bpmMax; break;
case "length": v = beatmap.endTime / 1000; break;
case "star":
case "stars": v = Math.round(beatmap.starRating * 100) / 100f; break;
default: return false;
}

View File

@@ -19,6 +19,7 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.db.BeatmapDB;
@@ -44,7 +45,7 @@ public class BeatmapSetList {
/** Search pattern for conditional expressions. */
private static final Pattern SEARCH_CONDITION_PATTERN = Pattern.compile(
"(ar|cs|od|hp|bpm|length)(=|==|>|>=|<|<=)((\\d*\\.)?\\d+)"
"(ar|cs|od|hp|bpm|length|stars?)(==?|>=?|<=?)((\\d*\\.)?\\d+)"
);
/** List containing all parsed nodes. */
@@ -163,12 +164,17 @@ public class BeatmapSetList {
}
// remove all node references
Beatmap beatmap = node.getBeatmapSet().get(0);
BeatmapSet beatmapSet = node.getBeatmapSet();
Beatmap beatmap = beatmapSet.get(0);
nodes.remove(index);
parsedNodes.remove(eCur);
mapCount -= node.getBeatmapSet().size();
mapCount -= beatmapSet.size();
if (beatmap.beatmapSetID > 0)
MSIDdb.remove(beatmap.beatmapSetID);
for (Beatmap bm : beatmapSet) {
if (bm.md5Hash != null)
this.beatmapHashDB.remove(bm.md5Hash);
}
// reset indices
for (int i = index, size = size(); i < size; i++)
@@ -199,11 +205,16 @@ public class BeatmapSetList {
BeatmapDB.delete(dir.getName());
// delete the associated directory
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
if (ws != null)
ws.pause();
try {
Utils.deleteToTrash(dir);
} catch (IOException e) {
ErrorHandler.error("Could not delete song group.", e, true);
}
if (ws != null)
ws.resume();
return true;
}
@@ -235,6 +246,8 @@ public class BeatmapSetList {
// remove song reference
Beatmap beatmap = node.getBeatmapSet().remove(node.beatmapIndex);
mapCount--;
if (beatmap.md5Hash != null)
beatmapHashDB.remove(beatmap.md5Hash);
// re-link nodes
if (node.prev != null)
@@ -247,11 +260,16 @@ public class BeatmapSetList {
BeatmapDB.delete(file.getParentFile().getName(), file.getName());
// delete the associated file
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
if (ws != null)
ws.pause();
try {
Utils.deleteToTrash(file);
} catch (IOException e) {
ErrorHandler.error("Could not delete song.", e, true);
}
if (ws != null)
ws.resume();
return true;
}
@@ -312,6 +330,7 @@ public class BeatmapSetList {
/**
* Expands the node at an index by inserting a new node for each Beatmap
* in that node and hiding the group node.
* @param index the node index
* @return the first of the newly-inserted nodes
*/
public BeatmapSetNode expand(int index) {

View File

@@ -21,7 +21,8 @@ package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameData.Grade;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
@@ -31,7 +32,7 @@ import org.newdawn.slick.Image;
*/
public class BeatmapSetNode {
/** The associated beatmap set. */
private BeatmapSet beatmapSet;
private final BeatmapSet beatmapSet;
/** Index of the selected beatmap (-1 if not focused). */
public int beatmapIndex = -1;
@@ -56,6 +57,14 @@ public class BeatmapSetNode {
*/
public BeatmapSet getBeatmapSet() { return beatmapSet; }
/**
* Returns the selected beatmap (based on {@link #beatmapIndex}).
* @return the beatmap, or null if the index is invalid
*/
public Beatmap getSelectedBeatmap() {
return (beatmapIndex < 0 || beatmapIndex >= beatmapSet.size()) ? null : beatmapSet.get(beatmapIndex);
}
/**
* Draws the button.
* @param x the x coordinate
@@ -78,16 +87,16 @@ public class BeatmapSetNode {
bgColor = Color.white;
textColor = Options.getSkin().getSongSelectActiveTextColor();
} else
bgColor = Utils.COLOR_BLUE_BUTTON;
bgColor = Colors.BLUE_BUTTON;
beatmap = beatmapSet.get(beatmapIndex);
} else {
bgColor = Utils.COLOR_ORANGE_BUTTON;
bgColor = Colors.ORANGE_BUTTON;
beatmap = beatmapSet.get(0);
}
bg.draw(x, y, bgColor);
float cx = x + (bg.getWidth() * 0.043f);
float cy = y + (bg.getHeight() * 0.2f) - 3;
float cy = y + (bg.getHeight() * 0.18f) - 3;
// draw grade
if (grade != Grade.NULL) {
@@ -98,15 +107,58 @@ public class BeatmapSetNode {
// draw text
if (Options.useUnicodeMetadata()) { // load glyphs
Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, null);
Utils.loadGlyphs(Utils.FONT_DEFAULT, null, beatmap.artistUnicode);
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.titleUnicode);
Fonts.loadGlyphs(Fonts.DEFAULT, beatmap.artistUnicode);
}
Utils.FONT_MEDIUM.drawString(cx, cy, beatmap.getTitle(), textColor);
Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 2,
Fonts.MEDIUM.drawString(cx, cy, beatmap.getTitle(), textColor);
Fonts.DEFAULT.drawString(cx, cy + Fonts.MEDIUM.getLineHeight() - 3,
String.format("%s // %s", beatmap.getArtist(), beatmap.creator), textColor);
if (expanded || beatmapSet.size() == 1)
Utils.FONT_BOLD.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 4,
Fonts.BOLD.drawString(cx, cy + Fonts.MEDIUM.getLineHeight() + Fonts.DEFAULT.getLineHeight() - 6,
beatmap.version, textColor);
// draw stars
// (note: in osu!, stars are also drawn for beatmap sets of size 1)
if (expanded) {
if (beatmap.starRating >= 0) {
Image star = GameImage.STAR.getImage();
float starOffset = star.getWidth() * 1.7f;
float starX = cx + starOffset * 0.04f;
float starY = cy + Fonts.MEDIUM.getLineHeight() + Fonts.DEFAULT.getLineHeight() * 2 - 8f * GameImage.getUIscale();
float starCenterY = starY + star.getHeight() / 2f;
final float baseAlpha = focus ? 1f : 0.8f;
final float smallStarScale = 0.4f;
star.setAlpha(baseAlpha);
int i = 1;
for (; i < beatmap.starRating && i <= 5; i++) {
if (focus)
star.drawFlash(starX + (i - 1) * starOffset, starY, star.getWidth(), star.getHeight(), textColor);
else
star.draw(starX + (i - 1) * starOffset, starY);
}
if (i <= 5) {
float partialStarScale = smallStarScale + (float) (beatmap.starRating - i + 1) * (1f - smallStarScale);
Image partialStar = star.getScaledCopy(partialStarScale);
partialStar.setAlpha(baseAlpha);
float partialStarY = starCenterY - partialStar.getHeight() / 2f;
if (focus)
partialStar.drawFlash(starX + (i - 1) * starOffset, partialStarY, partialStar.getWidth(), partialStar.getHeight(), textColor);
else
partialStar.draw(starX + (i - 1) * starOffset, partialStarY);
}
if (++i <= 5) {
Image smallStar = star.getScaledCopy(smallStarScale);
smallStar.setAlpha(0.5f);
float smallStarY = starCenterY - smallStar.getHeight() / 2f;
for (; i <= 5; i++) {
if (focus)
smallStar.drawFlash(starX + (i - 1) * starOffset, smallStarY, smallStar.getWidth(), smallStar.getHeight(), textColor);
else
smallStar.draw(starX + (i - 1) * starOffset, smallStarY);
}
}
}
}
}
/**

View File

@@ -39,13 +39,13 @@ public enum BeatmapSortOrder {
LENGTH (4, "Length", new LengthOrder());
/** The ID of the sort (used for tab positioning). */
private int id;
private final int id;
/** The name of the sort. */
private String name;
private final String name;
/** The comparator for the sort. */
private Comparator<BeatmapSetNode> comparator;
private final Comparator<BeatmapSetNode> comparator;
/** The tab associated with the sort (displayed in Song Menu screen). */
private MenuButton tab;
@@ -123,13 +123,11 @@ public enum BeatmapSortOrder {
@Override
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
int vMax = 0, wMax = 0;
for (int i = 0, size = v.getBeatmapSet().size(); i < size; i++) {
Beatmap beatmap = v.getBeatmapSet().get(i);
for (Beatmap beatmap : v.getBeatmapSet()) {
if (beatmap.endTime > vMax)
vMax = beatmap.endTime;
}
for (int i = 0, size = w.getBeatmapSet().size(); i < size; i++) {
Beatmap beatmap = w.getBeatmapSet().get(i);
for (Beatmap beatmap : w.getBeatmapSet()) {
if (beatmap.endTime > wMax)
wMax = beatmap.endTime;
}

View File

@@ -0,0 +1,295 @@
/*
* 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.ErrorHandler;
import itdelatrisu.opsu.Options;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.newdawn.slick.util.Log;
/*
* Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* - Neither the name of Oracle nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Watches the beatmap directory tree for changes.
*
* @author The Java Tutorials (http://docs.oracle.com/javase/tutorial/essential/io/examples/WatchDir.java) (base)
*/
public class BeatmapWatchService {
/** Beatmap watcher service instance. */
private static BeatmapWatchService ws;
/**
* Creates a new watch service instance (overwriting any previous instance),
* registers the beatmap directory, and starts processing events.
*/
public static void create() {
// close the existing watch service
destroy();
// create a new watch service
try {
ws = new BeatmapWatchService();
ws.register(Options.getBeatmapDir().toPath());
} catch (IOException e) {
ErrorHandler.error("An I/O exception occurred while creating the watch service.", e, true);
return;
}
// start processing events
ws.start();
}
/**
* Destroys the watch service instance, if any.
* Subsequent calls to {@link #get()} will return {@code null}.
*/
public static void destroy() {
if (ws == null)
return;
try {
ws.watcher.close();
ws.service.shutdownNow();
ws = null;
} catch (IOException e) {
ws = null;
ErrorHandler.error("An I/O exception occurred while closing the previous watch service.", e, true);
}
}
/**
* Returns the single instance of this class.
*/
public static BeatmapWatchService get() { return ws; }
/** Watch service listener interface. */
public interface BeatmapWatchServiceListener {
/**
* Indication that an event was received.
* @param kind the event kind
* @param child the child directory
*/
public void eventReceived(WatchEvent.Kind<?> kind, Path child);
}
/** The list of listeners. */
private static final List<BeatmapWatchServiceListener> listeners = new ArrayList<BeatmapWatchServiceListener>();
/**
* Adds a listener.
* @param listener the listener to add
*/
public static void addListener(BeatmapWatchServiceListener listener) { listeners.add(listener); }
/**
* Removes a listener.
* @param listener the listener to remove
*/
public static void removeListener(BeatmapWatchServiceListener listener) { listeners.remove(listener); }
/**
* Removes all listeners.
*/
public static void removeListeners() { listeners.clear(); }
/** The watch service. */
private final WatchService watcher;
/** The WatchKey -> Path mapping for registered directories. */
private final Map<WatchKey, Path> keys;
/** The Executor. */
private ExecutorService service;
/** Whether the watch service is paused (i.e. does not fire events). */
private boolean paused = false;
/**
* Creates the WatchService.
* @throws IOException if an I/O error occurs
*/
private BeatmapWatchService() throws IOException {
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new ConcurrentHashMap<WatchKey, Path>();
}
/**
* Register the given directory with the WatchService.
* @param dir the directory to register
* @throws IOException if an I/O error occurs
*/
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
keys.put(key, dir);
}
/**
* Register the given directory, and all its sub-directories, with the WatchService.
* @param start the root directory to register
*/
public void registerAll(final Path start) {
try {
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
try {
register(dir);
} catch (IOException e) {
Log.warn(String.format("Failed to register path '%s' with the watch service.", dir.toString()), e);
}
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
Log.warn(String.format("Failed to register paths from root directory '%s' with the watch service.", start.toString()), e);
}
}
@SuppressWarnings("unchecked")
private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>) event;
}
/**
* Start processing events in a new thread.
*/
private void start() {
if (service != null)
return;
this.service = Executors.newCachedThreadPool();
service.submit(new Runnable() {
@Override
public void run() { ws.processEvents(); }
});
}
/**
* Process all events for keys queued to the watcher
*/
private void processEvents() {
while (true) {
// wait for key to be signaled
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException | ClosedWatchServiceException e) {
return;
}
Path dir = keys.get(key);
if (dir == null)
continue;
boolean isPaused = paused;
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == StandardWatchEventKinds.OVERFLOW)
continue;
// context for directory entry event is the file name of entry
WatchEvent<Path> ev = cast(event);
Path name = ev.context();
Path child = dir.resolve(name);
//System.out.printf("%s: %s\n", kind.name(), child);
// fire listeners
if (!isPaused) {
for (BeatmapWatchServiceListener listener : listeners)
listener.eventReceived(kind, child);
}
// if directory is created, then register it and its sub-directories
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
if (Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS))
registerAll(child);
}
}
// reset key and remove from set if directory no longer accessible
if (!key.reset()) {
keys.remove(key);
if (keys.isEmpty())
break; // all directories are inaccessible
}
}
}
/**
* Stops listener events from being fired.
*/
public void pause() { paused = true; }
/**
* Resumes firing listener events.
*/
public void resume() { paused = false; }
}

View File

@@ -19,6 +19,11 @@
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.objects.curves.CatmullCurve;
import itdelatrisu.opsu.objects.curves.CircumscribedCircle;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.LinearBezier;
import itdelatrisu.opsu.objects.curves.Vec2f;
import java.text.DecimalFormat;
import java.text.NumberFormat;
@@ -34,13 +39,6 @@ public class HitObject {
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,
@@ -101,9 +99,18 @@ public class HitObject {
/** Hit sound type (SOUND_* bitmask). */
private byte hitSound;
/** Hit sound addition (sampleSet, AdditionSampleSet, ?, ...). */
/** Hit sound addition (sampleSet, AdditionSampleSet). */
private byte[] addition;
/** Addition custom sample index. */
private byte additionCustomSampleIndex;
/** Addition hit sound volume. */
private int additionHitSoundVolume;
/** Addition hit sound file. */
private String additionHitSound;
/** Slider curve type (SLIDER_* constant). */
private char sliderType;
@@ -250,9 +257,17 @@ public class HitObject {
// 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]);
if (additionTokens.length > 1) {
this.addition = new byte[2];
addition[0] = Byte.parseByte(additionTokens[0]);
addition[1] = Byte.parseByte(additionTokens[1]);
}
if (additionTokens.length > 2)
this.additionCustomSampleIndex = Byte.parseByte(additionTokens[2]);
if (additionTokens.length > 3)
this.additionHitSoundVolume = Integer.parseInt(additionTokens[3]);
if (additionTokens.length > 4)
this.additionHitSound = additionTokens[4];
}
}
@@ -298,13 +313,13 @@ public class HitObject {
*/
public String getTypeName() {
if (isCircle())
return CIRCLE;
return "circle";
else if (isSlider())
return SLIDER;
return "slider";
else if (isSpinner())
return SPINNER;
return "spinner";
else
return UNKNOWN;
return "unknown object type";
}
/**
@@ -386,6 +401,35 @@ public class HitObject {
*/
public float getPixelLength() { return pixelLength; }
/**
* Returns the time duration of the slider (excluding repeats), in milliseconds.
* @param sliderMultiplier the beatmap's slider movement speed multiplier
* @param beatLength the beat length
* @return the slider segment length
*/
public float getSliderTime(float sliderMultiplier, float beatLength) {
return beatLength * (pixelLength / sliderMultiplier) / 100f;
}
/**
* Returns the slider curve.
* @param scaled whether to use scaled coordinates
* @return a new Curve instance
*/
public Curve getSliderCurve(boolean scaled) {
if (sliderType == SLIDER_PASSTHROUGH && sliderX.length == 2) {
Vec2f nora = new Vec2f(sliderX[0] - x, sliderY[0] - y).nor();
Vec2f norb = new Vec2f(sliderX[0] - sliderX[1], sliderY[0] - sliderY[1]).nor();
if (Math.abs(norb.x * nora.y - norb.y * nora.x) < 0.00001f)
return new LinearBezier(this, false, scaled); // vectors parallel, use linear bezier instead
else
return new CircumscribedCircle(this, scaled);
} else if (sliderType == SLIDER_CATMULL)
return new CatmullCurve(this, scaled);
else
return new LinearBezier(this, sliderType == SLIDER_LINEAR, scaled);
}
/**
* Returns the spinner end time.
* @return the end time (in ms)
@@ -471,6 +515,21 @@ public class HitObject {
return 0;
}
/**
* Returns the custom sample index (addition).
*/
public byte getCustomSampleIndex() { return additionCustomSampleIndex; }
/**
* Returns the hit sound volume (addition).
*/
public int getHitSoundVolume() { return additionHitSoundVolume; }
/**
* Returns the hit sound file (addition).
*/
public String getHitSoundFile() { return additionHitSound; }
/**
* Sets the hit object index in the current stack.
* @param stack index in the stack
@@ -529,9 +588,12 @@ public class HitObject {
// addition
if (addition != null) {
for (int i = 0; i < addition.length; i++) {
sb.append(addition[i]);
sb.append(':');
sb.append(addition[i]); sb.append(':');
}
sb.append(additionCustomSampleIndex); sb.append(':');
sb.append(additionHitSoundVolume); sb.append(':');
if (additionHitSound != null)
sb.append(additionHitSound);
} else
sb.setLength(sb.length() - 1);

View File

@@ -0,0 +1,179 @@
/*
* 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.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import org.newdawn.slick.Image;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.opengl.ImageData;
import org.newdawn.slick.opengl.ImageDataFactory;
import org.newdawn.slick.opengl.LoadableImageData;
import org.newdawn.slick.util.Log;
/**
* Simple threaded image loader for a single image file.
*/
public class ImageLoader {
/** The image file. */
private final File file;
/** The loaded image. */
private Image image;
/** The image data. */
private LoadedImageData data;
/** The image loader thread. */
private Thread loaderThread;
/** ImageData wrapper, needed because {@code ImageIOImageData} doesn't implement {@code getImageBufferData()}. */
private class LoadedImageData implements ImageData {
/** The image data implementation. */
private final ImageData imageData;
/** The stored image. */
private final ByteBuffer buffer;
/**
* Constructor.
* @param imageData the class holding the image properties
* @param buffer the stored image
*/
public LoadedImageData(ImageData imageData, ByteBuffer buffer) {
this.imageData = imageData;
this.buffer = buffer;
}
@Override public int getDepth() { return imageData.getDepth(); }
@Override public int getWidth() { return imageData.getWidth(); }
@Override public int getHeight() { return imageData.getHeight();}
@Override public int getTexWidth() { return imageData.getTexWidth(); }
@Override public int getTexHeight() { return imageData.getTexHeight(); }
@Override public ByteBuffer getImageBufferData() { return buffer; }
}
/** Image loading thread. */
private class ImageLoaderThread extends Thread {
/** The image file input stream. */
private BufferedInputStream in;
@Override
public void interrupt() {
super.interrupt();
if (in != null) {
try {
in.close(); // interrupt I/O
} catch (IOException e) {}
}
}
@Override
public void run() {
// load image data into a ByteBuffer to use constructor Image(ImageData)
LoadableImageData imageData = ImageDataFactory.getImageDataFor(file.getAbsolutePath());
try (BufferedInputStream in = this.in = new BufferedInputStream(new FileInputStream(file))) {
ByteBuffer textureBuffer = imageData.loadImage(in, false, null);
if (!isInterrupted())
data = new LoadedImageData(imageData, textureBuffer);
} catch (IOException e) {
if (!isInterrupted())
Log.warn(String.format("Failed to load background image '%s'.", file), e);
}
this.in = null;
}
}
/**
* Constructor. Call {@link ImageLoader#load(boolean)} to load the image.
* @param file the image file
*/
public ImageLoader(File file) {
this.file = file;
}
/**
* Loads the image.
* @param threaded true to load the image data in a new thread
*/
public void load(boolean threaded) {
if (!file.isFile())
return;
if (threaded) {
if (loaderThread != null && loaderThread.isAlive())
loaderThread.interrupt();
loaderThread = new ImageLoaderThread();
loaderThread.start();
} else {
try {
image = new Image(file.getAbsolutePath());
} catch (SlickException e) {
Log.warn(String.format("Failed to load background image '%s'.", file), e);
}
}
}
/**
* Returns the image.
* @return the loaded image, or null if not loaded
*/
public Image getImage() {
if (image == null && data != null) {
image = new Image(data);
data = null;
}
return image;
}
/**
* Returns whether an image is currently loading in another thread.
* @return true if loading, false otherwise
*/
public boolean isLoading() { return (loaderThread != null && loaderThread.isAlive()); }
/**
* Interrupts the image loader, if running.
*/
public void interrupt() {
if (isLoading())
loaderThread.interrupt();
}
/**
* Releases all resources.
*/
public void destroy() {
interrupt();
loaderThread = null;
if (image != null && !image.isDestroyed()) {
try {
image.destroy();
} catch (SlickException e) {
Log.warn(String.format("Failed to destroy image '%s'.", image.getResourceReference()), e);
}
image = null;
}
data = null;
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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.util.LinkedHashMap;
import java.util.Map;
/**
* Least recently used cache.
*
* @param <K> the type of keys maintained by this map
* @param <V> the type of mapped values
*/
@SuppressWarnings("serial")
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
/** The cache capacity. */
private final int capacity;
/**
* Creates a least recently used cache with the given capacity.
* @param capacity the capacity
*/
public LRUCache(int capacity) {
super(capacity + 1, 1.1f, true);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
if (size() > capacity) {
eldestRemoved(eldest);
return true;
}
return false;
}
/**
* Notification that the eldest entry was removed.
* Can be used to clean up any resources when this happens (via override).
* @param eldest the removed entry
*/
public void eldestRemoved(Map.Entry<K, V> eldest) {}
}

View File

@@ -16,7 +16,10 @@
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu;
package itdelatrisu.opsu.beatmap;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Options;
import java.io.File;
import java.io.FilenameFilter;
@@ -62,6 +65,9 @@ public class OszUnpacker {
}
// unpack OSZs
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
if (ws != null)
ws.pause();
for (File file : files) {
fileIndex++;
String dirName = file.getName().substring(0, file.getName().lastIndexOf('.'));
@@ -73,6 +79,8 @@ public class OszUnpacker {
dirs.add(songDir);
}
}
if (ws != null)
ws.resume();
fileIndex = -1;
files = null;

View File

@@ -43,7 +43,7 @@ public class BeatmapDB {
* Current database version.
* This value should be changed whenever the database format changes.
*/
private static final String DATABASE_VERSION = "2015-06-11";
private static final String DATABASE_VERSION = "2015-09-02";
/** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
@@ -58,7 +58,7 @@ public class BeatmapDB {
private static Connection connection;
/** Query statements. */
private static PreparedStatement insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt, updateSizeStmt;
private static PreparedStatement insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt, setStarsStmt, updateSizeStmt;
/** Current size of beatmap cache table. */
private static int cacheSize = -1;
@@ -95,12 +95,13 @@ public class BeatmapDB {
try {
insertStmt = connection.prepareStatement(
"INSERT INTO beatmaps VALUES (" +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
setStarsStmt = connection.prepareStatement("UPDATE beatmaps SET stars = ? WHERE dir = ? AND file = ?");
} catch (SQLException e) {
ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
}
@@ -123,7 +124,7 @@ public class BeatmapDB {
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT, " +
"md5hash TEXT" +
"md5hash TEXT, stars REAL" +
"); " +
"CREATE TABLE IF NOT EXISTS info (" +
"key TEXT NOT NULL UNIQUE, value TEXT" +
@@ -342,6 +343,7 @@ public class BeatmapDB {
stmt.setString(39, beatmap.breaksToString());
stmt.setString(40, beatmap.comboToString());
stmt.setString(41, beatmap.md5Hash);
stmt.setDouble(42, beatmap.starRating);
} catch (SQLException e) {
throw e;
} catch (Exception e) {
@@ -484,6 +486,7 @@ public class BeatmapDB {
beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
beatmap.sliderBorderFromString(rs.getString(37));
beatmap.md5Hash = rs.getString(41);
beatmap.starRating = rs.getDouble(42);
} catch (SQLException e) {
throw e;
} catch (Exception e) {
@@ -571,6 +574,25 @@ public class BeatmapDB {
}
}
/**
* Sets the star rating for a beatmap in the database.
* @param beatmap the beatmap
*/
public static void setStars(Beatmap beatmap) {
if (connection == null)
return;
try {
setStarsStmt.setDouble(1, beatmap.starRating);
setStarsStmt.setString(2, beatmap.getFile().getParentFile().getName());
setStarsStmt.setString(3, beatmap.getFile().getName());
setStarsStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error(String.format("Failed to save star rating '%.4f' for beatmap '%s' in database.",
beatmap.starRating, beatmap.toString()), e, true);
}
}
/**
* Closes the connection to the database.
*/

View File

@@ -119,7 +119,9 @@ public class ScoreDB {
"timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " +
"creator = ? AND version = ? AND hit300 = ? AND hit100 = ? AND hit50 = ? AND " +
"geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ? AND " +
"replay = ? AND playerName = ?"
"(replay = ? OR (replay IS NULL AND ? IS NULL)) AND " +
"(playerName = ? OR (playerName IS NULL AND ? IS NULL))"
// TODO: extra playerName checks not needed if name is guaranteed not null
);
} catch (SQLException e) {
ErrorHandler.error("Failed to prepare score statements.", e, true);
@@ -222,6 +224,7 @@ public class ScoreDB {
try {
setStatementFields(insertStmt, data);
insertStmt.setString(18, data.replayString);
insertStmt.setString(19, data.playerName);
insertStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error("Failed to save score to database.", e, true);
@@ -238,6 +241,10 @@ public class ScoreDB {
try {
setStatementFields(deleteScoreStmt, data);
deleteScoreStmt.setString(18, data.replayString);
deleteScoreStmt.setString(19, data.replayString);
deleteScoreStmt.setString(20, data.playerName);
deleteScoreStmt.setString(21, data.playerName);
deleteScoreStmt.executeUpdate();
} catch (SQLException e) {
ErrorHandler.error("Failed to delete score from database.", e, true);
@@ -289,8 +296,6 @@ public class ScoreDB {
stmt.setInt(15, data.combo);
stmt.setBoolean(16, data.perfect);
stmt.setInt(17, data.mods);
stmt.setString(18, data.replayString);
stmt.setString(19, data.playerName);
}
/**

View File

@@ -46,6 +46,9 @@ public class Download {
/** Read timeout, in ms. */
public static final int READ_TIMEOUT = 10000;
/** Maximum number of HTTP/HTTPS redirects to follow. */
public static final int MAX_REDIRECTS = 3;
/** Time between download speed and ETA updates, in ms. */
private static final int UPDATE_INTERVAL = 1000;
@@ -58,7 +61,7 @@ public class Download {
ERROR ("Error");
/** The status name. */
private String name;
private final String name;
/**
* Constructor.
@@ -172,13 +175,57 @@ public class Download {
new Thread() {
@Override
public void run() {
// open connection, get content length
// open connection
HttpURLConnection conn = null;
try {
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setUseCaches(false);
URL downloadURL = url;
int redirectCount = 0;
boolean isRedirect = false;
do {
isRedirect = false;
conn = (HttpURLConnection) downloadURL.openConnection();
conn.setConnectTimeout(CONNECTION_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
conn.setUseCaches(false);
// allow HTTP <--> HTTPS redirects
// http://download.java.net/jdk7u2/docs/technotes/guides/deployment/deployment-guide/upgrade-guide/article-17.html
conn.setInstanceFollowRedirects(false);
conn.setRequestProperty("User-Agent", "Mozilla/5.0...");
// check for redirect
int status = conn.getResponseCode();
if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM ||
status == HttpURLConnection.HTTP_SEE_OTHER || status == HttpURLConnection.HTTP_USE_PROXY) {
URL base = conn.getURL();
String location = conn.getHeaderField("Location");
URL target = null;
if (location != null)
target = new URL(base, location);
conn.disconnect();
// check for problems
String error = null;
if (location == null)
error = String.format("Download for URL '%s' is attempting to redirect without a 'location' header.", base.toString());
else if (!target.getProtocol().equals("http") && !target.getProtocol().equals("https"))
error = String.format("Download for URL '%s' is attempting to redirect to a non-HTTP/HTTPS protocol '%s'.", base.toString(), target.getProtocol());
else if (redirectCount > MAX_REDIRECTS)
error = String.format("Download for URL '%s' is attempting too many redirects (over %d).", base.toString(), MAX_REDIRECTS);
if (error != null) {
ErrorHandler.error(error, null, false);
throw new IOException();
}
// follow redirect
downloadURL = target;
redirectCount++;
isRedirect = true;
}
} while (isRedirect);
// store content length
contentLength = conn.getContentLength();
} catch (IOException e) {
status = Status.ERROR;
@@ -198,9 +245,18 @@ public class Download {
fos = fileOutputStream;
status = Status.DOWNLOADING;
updateReadSoFar();
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
long bytesRead = fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
if (status == Status.DOWNLOADING) { // not interrupted
// TODO: if connection is lost before a download finishes, it's still marked as "complete"
// check if the entire file was received
if (bytesRead < contentLength) {
status = Status.ERROR;
Log.warn(String.format("Download '%s' failed: %d bytes expected, %d bytes received.", url.toString(), contentLength, bytesRead));
if (listener != null)
listener.error();
return;
}
// mark download as complete
status = Status.COMPLETE;
rbc.close();
fos.close();
@@ -273,7 +329,7 @@ public class Download {
public long readSoFar() {
switch (status) {
case COMPLETE:
return contentLength;
return (rbc != null) ? rbc.getReadSoFar() : contentLength;
case DOWNLOADING:
if (rbc != null)
return rbc.getReadSoFar();

View File

@@ -75,7 +75,7 @@ public class DownloadList {
}
/**
* Returns the size of the doownloads list.
* Returns the size of the downloads list.
*/
public int size() { return nodes.size(); }

View File

@@ -26,6 +26,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.downloads.Download.DownloadListener;
import itdelatrisu.opsu.downloads.Download.Status;
import itdelatrisu.opsu.downloads.servers.DownloadServer;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.UI;
import java.io.File;
@@ -42,19 +44,19 @@ public class DownloadNode {
private Download download;
/** Beatmap set ID. */
private int beatmapSetID;
private final int beatmapSetID;
/** Last updated date string. */
private String date;
private final String date;
/** Song title. */
private String title, titleUnicode;
private final String title, titleUnicode;
/** Song artist. */
private String artist, artistUnicode;
private final String artist, artistUnicode;
/** Beatmap creator. */
private String creator;
private final String creator;
/** Button drawing values. */
private static float buttonBaseX, buttonBaseY, buttonWidth, buttonHeight, buttonOffset;
@@ -68,12 +70,6 @@ public class DownloadNode {
/** Container width. */
private static int containerWidth;
/** Button background colors. */
public static final Color
BG_NORMAL = new Color(0, 0, 0, 0.25f),
BG_HOVER = new Color(0, 0, 0, 0.5f),
BG_FOCUS = new Color(0, 0, 0, 0.75f);
/**
* Initializes the base coordinates for drawing.
* @param width the container width
@@ -86,16 +82,16 @@ public class DownloadNode {
buttonBaseX = width * 0.024f;
buttonBaseY = height * 0.2f;
buttonWidth = width * 0.7f;
buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2.1f;
buttonHeight = Fonts.MEDIUM.getLineHeight() * 2.1f;
buttonOffset = buttonHeight * 1.1f;
// download info
infoBaseX = width * 0.75f;
infoBaseY = height * 0.07f + Utils.FONT_LARGE.getLineHeight() * 2f;
infoBaseY = height * 0.07f + Fonts.LARGE.getLineHeight() * 2f;
infoWidth = width * 0.25f;
infoHeight = Utils.FONT_DEFAULT.getLineHeight() * 2.4f;
infoHeight = Fonts.DEFAULT.getLineHeight() * 2.4f;
float searchY = (height * 0.05f) + Utils.FONT_LARGE.getLineHeight();
float searchY = (height * 0.05f) + Fonts.LARGE.getLineHeight();
float buttonHeight = height * 0.038f;
maxResultsShown = (int) ((height - buttonBaseY - searchY) / buttonOffset);
maxDownloadsShown = (int) ((height - infoBaseY - searchY - buttonHeight) / infoHeight);
@@ -228,10 +224,9 @@ public class DownloadNode {
* @param total the total number of buttons
*/
public static void drawResultScrollbar(Graphics g, float position, float total) {
UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset,
buttonBaseX, buttonBaseY,
UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset, buttonBaseX, buttonBaseY,
buttonWidth * 1.01f, (maxResultsShown-1) * buttonOffset + buttonHeight,
BG_NORMAL, Color.white, true);
Colors.BLACK_BG_NORMAL, Color.white, true);
}
/**
@@ -242,11 +237,18 @@ public class DownloadNode {
*/
public static void drawDownloadScrollbar(Graphics g, float index, float total) {
UI.drawScrollbar(g, index, total, maxDownloadsShown * infoHeight, infoBaseX, infoBaseY,
infoWidth, maxDownloadsShown * infoHeight, BG_NORMAL, Color.white, true);
infoWidth, maxDownloadsShown * infoHeight, Colors.BLACK_BG_NORMAL, Color.white, true);
}
/**
* Constructor.
* @param beatmapSetID the beatmap set ID
* @param date the last modified date string
* @param title the song title
* @param titleUnicode the Unicode song title (or {@code null} if none)
* @param artist the song artist
* @param artistUnicode the Unicode song artist (or {@code null} if none)
* @param creator the beatmap creator
*/
public DownloadNode(int beatmapSetID, String date, String title,
String titleUnicode, String artist, String artistUnicode, String creator) {
@@ -273,7 +275,7 @@ public class DownloadNode {
return;
String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID);
String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title);
this.download = new Download(url, path, rename);
Download download = new Download(url, path, rename);
download.setListener(new DownloadListener() {
@Override
public void completed() {
@@ -285,8 +287,9 @@ public class DownloadNode {
UI.sendBarNotification("Download failed due to a connection error.");
}
});
this.download = download;
if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_LARGE, getTitle(), null);
Fonts.loadGlyphs(Fonts.LARGE, getTitle());
}
/**
@@ -348,12 +351,12 @@ public class DownloadNode {
Download dl = DownloadList.get().getDownload(beatmapSetID);
// rectangle outline
g.setColor((focus) ? BG_FOCUS : (hover) ? BG_HOVER : BG_NORMAL);
g.setColor((focus) ? Colors.BLACK_BG_FOCUS : (hover) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
// map is already loaded
if (BeatmapSetList.get().containsBeatmapSetID(beatmapSetID)) {
g.setColor(Utils.COLOR_BLUE_BUTTON);
g.setColor(Colors.BLUE_BUTTON);
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
}
@@ -361,7 +364,7 @@ public class DownloadNode {
if (dl != null) {
float progress = dl.getProgress();
if (progress > 0f) {
g.setColor(Utils.COLOR_GREEN);
g.setColor(Colors.GREEN);
g.fillRect(buttonBaseX, y, buttonWidth * progress / 100f, buttonHeight);
}
}
@@ -373,21 +376,22 @@ public class DownloadNode {
// text
// TODO: if the title/artist line is too long, shorten it (e.g. add "...") instead of just clipping
if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_BOLD, getTitle(), getArtist());
if (Options.useUnicodeMetadata()) { // load glyphs
Fonts.loadGlyphs(Fonts.BOLD, getTitle());
Fonts.loadGlyphs(Fonts.BOLD, getArtist());
}
// TODO can't set clip again or else old clip will be cleared
//g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Utils.FONT_DEFAULT.getWidth(creator)), Utils.FONT_BOLD.getLineHeight());
Utils.FONT_BOLD.drawString(
//g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Fonts.DEFAULT.getWidth(creator)), Fonts.BOLD.getLineHeight());
Fonts.BOLD.drawString(
textX, y + marginY,
String.format("%s - %s%s", getArtist(), getTitle(),
(dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white);
//g.clearClip();
Utils.FONT_DEFAULT.drawString(
textX, y + marginY + Utils.FONT_BOLD.getLineHeight(),
Fonts.DEFAULT.drawString(
textX, y + marginY + Fonts.BOLD.getLineHeight(),
String.format("Last updated: %s", date), Color.white);
Utils.FONT_DEFAULT.drawString(
edgeX - Utils.FONT_DEFAULT.getWidth(creator), y + marginY,
Fonts.DEFAULT.drawString(
edgeX - Fonts.DEFAULT.getWidth(creator), y + marginY,
creator, Color.white);
}
@@ -399,6 +403,7 @@ public class DownloadNode {
* @param hover true if the mouse is hovering over this button
*/
public void drawDownload(Graphics g, float position, int id, boolean hover) {
Download download = this.download; // in case clearDownload() is called asynchronously
if (download == null) {
ErrorHandler.error("Trying to draw download information for button without Download object.", null, false);
return;
@@ -410,7 +415,7 @@ public class DownloadNode {
float marginY = infoHeight * 0.04f;
// rectangle outline
g.setColor((id % 2 == 0) ? BG_HOVER : BG_NORMAL);
g.setColor((id % 2 == 0) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
g.fillRect(infoBaseX, y, infoWidth, infoHeight);
// text
@@ -428,8 +433,8 @@ public class DownloadNode {
info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress,
Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength()));
}
Utils.FONT_BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
Utils.FONT_DEFAULT.drawString(textX, y + marginY + Utils.FONT_BOLD.getLineHeight(), info, Color.white);
Fonts.BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
Fonts.DEFAULT.drawString(textX, y + marginY + Fonts.BOLD.getLineHeight(), info, Color.white);
// 'x' button
if (hover) {

View File

@@ -28,7 +28,7 @@ import java.nio.channels.ReadableByteChannel;
*/
public class ReadableByteChannelWrapper implements ReadableByteChannel {
/** The wrapped ReadableByteChannel. */
private ReadableByteChannel rbc;
private final ReadableByteChannel rbc;
/** The number of bytes read. */
private long bytesRead;

View File

@@ -77,7 +77,7 @@ public class Updater {
UPDATE_FINAL ("Update queued.");
/** The status description. */
private String description;
private final String description;
/**
* Constructor.
@@ -194,9 +194,10 @@ public class Updater {
/**
* Checks the program version against the version file on the update server.
* @throws IOException if an I/O exception occurs
*/
public void checkForUpdates() throws IOException {
if (status != Status.INITIAL || System.getProperty("XDG") != null)
if (status != Status.INITIAL || Options.USE_XDG)
return;
status = Status.CHECKING;

View File

@@ -75,4 +75,7 @@ public abstract class DownloadServer {
public String getPreviewURL(int beatmapSetID) {
return String.format(PREVIEW_URL, beatmapSetID);
}
@Override
public String toString() { return getName(); }
}

View File

@@ -0,0 +1,202 @@
/*
* 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.downloads.servers;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.downloads.DownloadNode;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
/**
* Download server: http://osu.mengsky.net/
*/
public class MengSkyServer extends DownloadServer {
/** Server name. */
private static final String SERVER_NAME = "MengSky";
/** Formatted download URL: {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "http://osu.mengsky.net/d.php?id=%d";
/** Formatted search URL: {@code query} */
private static final String SEARCH_URL = "http://osu.mengsky.net/index.php?search_keywords=%s";
/** Formatted home URL: {@code page} */
private static final String HOME_URL = "http://osu.mengsky.net/index.php?next=1&page=%d";
/** Maximum beatmaps displayed per page. */
private static final int PAGE_LIMIT = 20;
/** Total result count from the last query. */
private int totalResults = -1;
/** Constructor. */
public MengSkyServer() {}
@Override
public String getName() { return SERVER_NAME; }
@Override
public String getDownloadURL(int beatmapSetID) {
return String.format(DOWNLOAD_URL, beatmapSetID);
}
@Override
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null;
try {
// read HTML
String search;
boolean isSearch;
if (query.isEmpty()) {
isSearch = false;
search = String.format(HOME_URL, page - 1);
} else {
isSearch = true;
search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"));
}
String html = Utils.readDataFromUrl(new URL(search));
if (html == null) {
this.totalResults = -1;
return null;
}
// parse results
// NOTE: Maybe an HTML parser would be better for this...
// FORMAT:
// <div class="beatmap" style="{{...}}">
// <div class="preview" style="background-image:url(http://b.ppy.sh/thumb/{{id}}l.jpg)"></div>
// <div class="name"> <a href="">{{artist}} - {{title}}</a> </div>
// <div class="douban_details">
// <span>Creator:</span> {{creator}}<br>
// <span>MaxBpm:</span> {{bpm}}<br>
// <span>Title:</span> {{titleUnicode}}<br>
// <span>Artist:</span> {{artistUnicode}}<br>
// <span>Status:</span> <font color={{"#00CD00" || "#EE0000"}}>{{"Ranked?" || "Unranked"}}</font><br>
// </div>
// <div class="details"> <a href=""></a> <br>
// <span>Fork:</span> bloodcat<br>
// <span>UpdateTime:</span> {{yyyy}}/{{mm}}/{{dd}} {{hh}}:{{mm}}:{{ss}}<br>
// <span>Mode:</span> <img id="{{'s' || 'c' || ...}}" src="/img/{{'s' || 'c' || ...}}.png"> {{...}}
// </div>
// <div class="download">
// <a href="https://osu.ppy.sh/s/{{id}}" class=" btn" target="_blank">Osu.ppy</a>
// </div>
// <div class="download">
// <a href="http://osu.mengsky.net/d.php?id={{id}}" class=" btn" target="_blank">DownLoad</a>
// </div>
// </div>
List<DownloadNode> nodeList = new ArrayList<DownloadNode>();
final String
START_TAG = "<div class=\"beatmap\"", NAME_TAG = "<div class=\"name\"> <a href=\"\">",
CREATOR_TAG = "<span>Creator:</span> ", TITLE_TAG = "<span>Title:</span> ", ARTIST_TAG = "<span>Artist:</span> ",
TIMESTAMP_TAG = "<span>UpdateTime:</span> ", DOWNLOAD_TAG = "<div class=\"download\">",
BR_TAG = "<br>", HREF_TAG = "<a href=\"", HREF_TAG_END = "</a>";
int index = -1;
int nextIndex = html.indexOf(START_TAG, index + 1);
int divCount = 0;
while ((index = nextIndex) != -1) {
nextIndex = html.indexOf(START_TAG, index + 1);
int n = (nextIndex == -1) ? html.length() : nextIndex;
divCount++;
int i, j;
// find beatmap
i = html.indexOf(NAME_TAG, index + START_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf(HREF_TAG_END, i + 1);
if (j == -1 || j > n) continue;
String beatmap = html.substring(i + NAME_TAG.length(), j);
String[] beatmapTokens = beatmap.split(" - ", 2);
if (beatmapTokens.length < 2)
continue;
String artist = beatmapTokens[0];
String title = beatmapTokens[1];
// find other beatmap details
i = html.indexOf(CREATOR_TAG, j + HREF_TAG_END.length());
if (i == -1 || i > n) continue;
j = html.indexOf(BR_TAG, i + CREATOR_TAG.length());
if (j == -1 || j > n) continue;
String creator = html.substring(i + CREATOR_TAG.length(), j);
i = html.indexOf(TITLE_TAG, j + BR_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf(BR_TAG, i + TITLE_TAG.length());
if (j == -1 || j > n) continue;
String titleUnicode = html.substring(i + TITLE_TAG.length(), j);
i = html.indexOf(ARTIST_TAG, j + BR_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf(BR_TAG, i + ARTIST_TAG.length());
if (j == -1 || j > n) continue;
String artistUnicode = html.substring(i + ARTIST_TAG.length(), j);
i = html.indexOf(TIMESTAMP_TAG, j + BR_TAG.length());
if (i == -1 || i >= n) continue;
j = html.indexOf(BR_TAG, i + TIMESTAMP_TAG.length());
if (j == -1 || j > n) continue;
String date = html.substring(i + TIMESTAMP_TAG.length(), j);
// find beatmap ID
i = html.indexOf(DOWNLOAD_TAG, j + BR_TAG.length());
if (i == -1 || i >= n) continue;
i = html.indexOf(HREF_TAG, i + DOWNLOAD_TAG.length());
if (i == -1 || i > n) continue;
j = html.indexOf('"', i + HREF_TAG.length());
if (j == -1 || j > n) continue;
String downloadURL = html.substring(i + HREF_TAG.length(), j);
String[] downloadTokens = downloadURL.split("(?=\\d*$)", 2);
if (downloadTokens[1].isEmpty()) continue;
int id;
try {
id = Integer.parseInt(downloadTokens[1]);
} catch (NumberFormatException e) {
continue;
}
nodeList.add(new DownloadNode(id, date, title, titleUnicode, artist, artistUnicode, creator));
}
nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
// store total result count
if (isSearch)
this.totalResults = nodes.length;
else {
int resultCount = nodes.length + (page - 1) * PAGE_LIMIT;
if (divCount == PAGE_LIMIT)
resultCount++;
this.totalResults = resultCount;
}
} catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
}
return nodes;
}
@Override
public int minQueryLength() { return 2; }
@Override
public int totalResults() { return totalResults; }
}

View File

@@ -0,0 +1,133 @@
/*
* 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.downloads.servers;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.downloads.DownloadNode;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Download server: http://osu.uu.gl/
*/
public class MnetworkServer extends DownloadServer {
/** Server name. */
private static final String SERVER_NAME = "Mnetwork";
/** Formatted download URL: {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "http://osu.uu.gl/s/%d";
/** Formatted search URL: {@code query} */
private static final String SEARCH_URL = "http://osu.uu.gl/d/%s";
/** Total result count from the last query. */
private int totalResults = -1;
/** Beatmap pattern. */
private Pattern BEATMAP_PATTERN = Pattern.compile("^(\\d+) ([^-]+) - (.+)\\.osz$");
/** Constructor. */
public MnetworkServer() {}
@Override
public String getName() { return SERVER_NAME; }
@Override
public String getDownloadURL(int beatmapSetID) {
return String.format(DOWNLOAD_URL, beatmapSetID);
}
@Override
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null;
try {
// read HTML
String queryString = (query.isEmpty()) ? "-" : query;
String search = String.format(SEARCH_URL, URLEncoder.encode(queryString, "UTF-8"));
String html = Utils.readDataFromUrl(new URL(search));
if (html == null) {
this.totalResults = -1;
return null;
}
// parse results
// NOTE: Not using a full HTML parser because this is a relatively simple operation.
// FORMAT:
// <div class="tr_title">
// <b><a href='/s/{{id}}'>{{id}} {{artist}} - {{title}}.osz</a></b><br />
// BPM: {{bpm}} <b>|</b> Total Time: {{m}}:{{s}}<br/>
// Genre: {{genre}} <b>|</b> Updated: {{MMM}} {{d}}, {{yyyy}}<br />
List<DownloadNode> nodeList = new ArrayList<DownloadNode>();
final String START_TAG = "<div class=\"tr_title\">", HREF_TAG = "<a href=", HREF_TAG_END = "</a>", UPDATED = "Updated: ";
int index = -1;
int nextIndex = html.indexOf(START_TAG, index + 1);
while ((index = nextIndex) != -1) {
nextIndex = html.indexOf(START_TAG, index + 1);
int n = (nextIndex == -1) ? html.length() : nextIndex;
int i, j;
// find beatmap
i = html.indexOf(HREF_TAG, index + START_TAG.length());
if (i == -1 || i > n) continue;
i = html.indexOf('>', i + HREF_TAG.length());
if (i == -1 || i >= n) continue;
j = html.indexOf(HREF_TAG_END, i + 1);
if (j == -1 || j > n) continue;
String beatmap = html.substring(i + 1, j).trim();
// find date
i = html.indexOf(UPDATED, j);
if (i == -1 || i >= n) continue;
j = html.indexOf('<', i + UPDATED.length());
if (j == -1 || j > n) continue;
String date = html.substring(i + UPDATED.length(), j).trim();
// parse id, title, and artist
Matcher m = BEATMAP_PATTERN.matcher(beatmap);
if (!m.matches())
continue;
nodeList.add(new DownloadNode(Integer.parseInt(m.group(1)), date, m.group(3), null, m.group(2), null, ""));
}
nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
// store total result count
this.totalResults = nodes.length;
} catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
}
return nodes;
}
@Override
public int minQueryLength() { return 0; }
@Override
public int totalResults() { return totalResults; }
}

View File

@@ -39,6 +39,8 @@ import org.json.JSONObject;
/**
* Download server: http://loli.al/
* <p>
* <i>This server went offline in August 2015.</i>
*/
public class OsuMirrorServer extends DownloadServer {
/** Server name. */

View File

@@ -0,0 +1,204 @@
/*
* 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.downloads.servers;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.downloads.DownloadNode;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import org.json.JSONObject;
/**
* Download server: http://osu.yas-online.net/
*/
public class YaSOnlineServer extends DownloadServer {
/** Server name. */
private static final String SERVER_NAME = "YaS Online";
/** Formatted download URL (returns JSON): {@code beatmapSetID} */
private static final String DOWNLOAD_URL = "https://osu.yas-online.net/json.mapdata.php?mapId=%d";
/**
* Formatted download fetch URL: {@code downloadLink}
* (e.g. {@code /fetch/49125122158ef360a66a07bce2d0483596913843-m-10418.osz})
*/
private static final String DOWNLOAD_FETCH_URL = "https://osu.yas-online.net%s";
/** Maximum beatmaps displayed per page. */
private static final int PAGE_LIMIT = 25;
/** Formatted home URL: {@code page} */
private static final String HOME_URL = "https://osu.yas-online.net/json.maplist.php?o=%d";
/** Formatted search URL: {@code query} */
private static final String SEARCH_URL = "https://osu.yas-online.net/json.search.php?searchQuery=%s";
/** Total result count from the last query. */
private int totalResults = -1;
/** Max server download ID seen (for approximating total pages). */
private int maxServerID = 0;
/** Constructor. */
public YaSOnlineServer() {}
@Override
public String getName() { return SERVER_NAME; }
@Override
public String getDownloadURL(int beatmapSetID) {
try {
// TODO: do this asynchronously (will require lots of changes...)
return getDownloadURLFromMapData(beatmapSetID);
} catch (IOException e) {
return null;
}
}
/**
* Returns the beatmap download URL by downloading its map data.
* <p>
* This is needed because there is no other way to find a beatmap's direct
* download URL.
* @param beatmapSetID the beatmap set ID
* @return the URL string, or null if the address could not be determined
* @throws IOException if any connection error occurred
*/
private String getDownloadURLFromMapData(int beatmapSetID) throws IOException {
try {
// read JSON
String search = String.format(DOWNLOAD_URL, beatmapSetID);
JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
JSONObject results;
if (json == null ||
!json.getString("result").equals("success") ||
(results = json.getJSONObject("success")).length() == 0) {
return null;
}
// parse result
Iterator<?> keys = results.keys();
if (!keys.hasNext())
return null;
String key = (String) keys.next();
JSONObject item = results.getJSONObject(key);
String downloadLink = item.getString("downloadLink");
return String.format(DOWNLOAD_FETCH_URL, downloadLink);
} catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem retrieving download URL for beatmap '%d'.", beatmapSetID), e, true);
return null;
}
}
@Override
public DownloadNode[] resultList(String query, int page, boolean rankedOnly) throws IOException {
DownloadNode[] nodes = null;
try {
// read JSON
String search;
boolean isSearch;
if (query.isEmpty()) {
isSearch = false;
search = String.format(HOME_URL, (page - 1) * PAGE_LIMIT);
} else {
isSearch = true;
search = String.format(SEARCH_URL, URLEncoder.encode(query, "UTF-8"));
}
JSONObject json = Utils.readJsonObjectFromUrl(new URL(search));
if (json == null) {
this.totalResults = -1;
return null;
}
JSONObject results;
if (!json.getString("result").equals("success") ||
(results = json.getJSONObject("success")).length() == 0) {
this.totalResults = 0;
return new DownloadNode[0];
}
// parse result list
List<DownloadNode> nodeList = new ArrayList<DownloadNode>();
for (Object obj : results.keySet()) {
String key = (String) obj;
JSONObject item = results.getJSONObject(key);
// parse title and artist
String title, artist;
String str = item.getString("map");
int index = str.indexOf(" - ");
if (index > -1) {
title = str.substring(0, index);
artist = str.substring(index + 3);
} else { // should never happen...
title = str;
artist = "?";
}
// only contains date added if part of a beatmap pack
int added = item.getInt("added");
String date = (added == 0) ? "?" : formatDate(added);
// approximate page count
int serverID = item.getInt("id");
if (serverID > maxServerID)
maxServerID = serverID;
nodeList.add(new DownloadNode(item.getInt("mapid"), date, title, null, artist, null, ""));
}
nodes = nodeList.toArray(new DownloadNode[nodeList.size()]);
// store total result count
if (isSearch)
this.totalResults = nodes.length;
else
this.totalResults = maxServerID;
} catch (MalformedURLException | UnsupportedEncodingException e) {
ErrorHandler.error(String.format("Problem loading result list for query '%s'.", query), e, true);
}
return nodes;
}
@Override
public int minQueryLength() { return 3; }
@Override
public int totalResults() { return totalResults; }
/**
* Returns a formatted date string from a raw date.
* @param timestamp the UTC timestamp, in seconds
* @return the formatted date
*/
private String formatDate(int timestamp) {
Date d = new Date(timestamp * 1000L);
DateFormat fmt = new SimpleDateFormat("d MMM yyyy HH:mm:ss");
return fmt.format(d);
}
}

View File

@@ -61,6 +61,7 @@ public class OsuReader {
/**
* Closes the input stream.
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException { reader.close(); }

View File

@@ -62,7 +62,7 @@ public class OsuWriter {
/**
* Closes the output stream.
* @throws IOException
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException { writer.close(); }

View File

@@ -25,7 +25,9 @@ import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.states.Game;
import itdelatrisu.opsu.ui.Colors;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
@@ -35,9 +37,6 @@ import org.newdawn.slick.Graphics;
* Data type representing a circle object.
*/
public class Circle implements GameObject {
/** The amount of time, in milliseconds, to fade in the circle. */
private static final int FADE_IN_TIME = 375;
/** The diameter of hit circles. */
private static float diameter;
@@ -62,11 +61,10 @@ public class Circle implements GameObject {
/**
* Initializes the Circle data type with map modifiers, images, and dimensions.
* @param container the game container
* @param circleSize the map's circleSize value
* @param circleDiameter the circle diameter
*/
public static void init(GameContainer container, float circleSize) {
diameter = (104 - (circleSize * 8));
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
public static void init(GameContainer container, float circleDiameter) {
diameter = circleDiameter * HitObject.getXMultiplier(); // convert from Osupixels (640x480)
int diameterInt = (int) diameter;
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
@@ -93,26 +91,37 @@ public class Circle implements GameObject {
@Override
public void draw(Graphics g, int trackPosition) {
int timeDiff = hitObject.getTime() - trackPosition;
float scale = timeDiff / (float) game.getApproachTime();
float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
final int approachTime = game.getApproachTime();
final int fadeInTime = game.getFadeInTime();
float scale = timeDiff / (float) approachTime;
float approachScale = 1 + scale * 3;
float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime;
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
Utils.COLOR_WHITE_FADE.a = color.a = alpha;
if (GameMod.HIDDEN.isActive()) {
final int hiddenDecayTime = game.getHiddenDecayTime();
final int hiddenTimeDiff = game.getHiddenTimeDiff();
if (fadeinScale <= 0f && timeDiff < hiddenTimeDiff + hiddenDecayTime) {
float hiddenAlpha = (timeDiff < hiddenTimeDiff) ? 0f : (timeDiff - hiddenTimeDiff) / (float) hiddenDecayTime;
alpha = Math.min(alpha, hiddenAlpha);
}
}
if (timeDiff >= 0)
float oldAlpha = Colors.WHITE_FADE.a;
Colors.WHITE_FADE.a = color.a = alpha;
if (timeDiff >= 0 && !GameMod.HIDDEN.isActive())
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
GameImage.HITCIRCLE.getImage().drawCentered(x, y, color);
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
if (!overlayAboveNumber)
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Colors.WHITE_FADE);
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
GameImage.HITCIRCLE.getImage().getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
if (overlayAboveNumber)
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Colors.WHITE_FADE);
Utils.COLOR_WHITE_FADE.a = oldAlpha;
Colors.WHITE_FADE.a = oldAlpha;
}
/**
@@ -186,7 +195,7 @@ public class Circle implements GameObject {
}
@Override
public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
public Vec2f getPointAt(int trackPosition) { return new Vec2f(x, y); }
@Override
public int getEndTime() { return hitObject.getTime(); }

View File

@@ -19,6 +19,7 @@
package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.Vec2f;
import org.newdawn.slick.Graphics;
@@ -53,7 +54,7 @@ public class DummyObject implements GameObject {
public boolean mousePressed(int x, int y, int trackPosition) { return false; }
@Override
public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
public Vec2f getPointAt(int trackPosition) { return new Vec2f(x, y); }
@Override
public int getEndTime() { return hitObject.getTime(); }

View File

@@ -18,6 +18,8 @@
package itdelatrisu.opsu.objects;
import itdelatrisu.opsu.objects.curves.Vec2f;
import org.newdawn.slick.Graphics;
/**
@@ -55,9 +57,9 @@ public interface GameObject {
/**
* Returns the coordinates of the hit object at a given track position.
* @param trackPosition the track position
* @return the [x,y] coordinates
* @return the position vector
*/
public float[] getPointAt(int trackPosition);
public Vec2f getPointAt(int trackPosition);
/**
* Returns the end time of the hit object.

View File

@@ -26,11 +26,10 @@ import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.CatmullCurve;
import itdelatrisu.opsu.objects.curves.CircumscribedCircle;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.LinearBezier;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.states.Game;
import itdelatrisu.opsu.ui.Colors;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
@@ -56,9 +55,6 @@ public class Slider implements GameObject {
/** The diameter of hit circles. */
private static float diameter;
/** The amount of time, in milliseconds, to fade in the slider. */
private static final int FADE_IN_TIME = 375;
/** The associated HitObject. */
private HitObject hitObject;
@@ -113,22 +109,21 @@ public class Slider implements GameObject {
/**
* Initializes the Slider data type with images and dimensions.
* @param container the game container
* @param circleSize the map's circleSize value
* @param circleDiameter the circle diameter
* @param beatmap the associated beatmap
*/
public static void init(GameContainer container, float circleSize, Beatmap beatmap) {
public static void init(GameContainer container, float circleDiameter, Beatmap beatmap) {
containerWidth = container.getWidth();
containerHeight = container.getHeight();
diameter = (104 - (circleSize * 8));
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
diameter = circleDiameter * HitObject.getXMultiplier(); // convert from Osupixels (640x480)
int diameterInt = (int) diameter;
followRadius = diameter / 2 * 3f;
// slider ball
if (GameImage.SLIDER_BALL.hasSkinImages() ||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
if (GameImage.SLIDER_BALL.hasBeatmapSkinImages() ||
(!GameImage.SLIDER_BALL.hasBeatmapSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
sliderBallImages = GameImage.SLIDER_BALL.getImages();
else
sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
@@ -160,7 +155,7 @@ public class Slider implements GameObject {
updatePosition();
// slider time calculations
this.sliderTime = game.getBeatLength() * (hitObject.getPixelLength() / sliderMultiplier) / 100f;
this.sliderTime = hitObject.getSliderTime(sliderMultiplier, game.getBeatLength());
this.sliderTimeTotal = sliderTime * hitObject.getRepeatCount();
// ticks
@@ -178,36 +173,46 @@ public class Slider implements GameObject {
@Override
public void draw(Graphics g, int trackPosition) {
int timeDiff = hitObject.getTime() - trackPosition;
float scale = timeDiff / (float) game.getApproachTime();
float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
final int approachTime = game.getApproachTime();
final int fadeInTime = game.getFadeInTime();
float scale = timeDiff / (float) approachTime;
float approachScale = 1 + scale * 3;
float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime;
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
Utils.COLOR_WHITE_FADE.a = color.a = alpha;
float oldAlpha = Colors.WHITE_FADE.a;
Colors.WHITE_FADE.a = color.a = alpha;
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
Image hitCircle = GameImage.HITCIRCLE.getImage();
float[] endPos = curve.pointAt(1);
Vec2f endPos = curve.pointAt(1);
curve.draw(color);
color.a = alpha;
// end circle
hitCircle.drawCentered(endPos[0], endPos[1], color);
hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE);
hitCircle.drawCentered(endPos.x, endPos.y, color);
hitCircleOverlay.drawCentered(endPos.x, endPos.y, Colors.WHITE_FADE);
// start circle
hitCircle.drawCentered(x, y, color);
if (!overlayAboveNumber)
hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
// ticks
if (ticksT != null) {
Image tick = GameImage.SLIDER_TICK.getImage();
for (int i = 0; i < ticksT.length; i++) {
float[] c = curve.pointAt(ticksT[i]);
tick.drawCentered(c[0], c[1], Utils.COLOR_WHITE_FADE);
Vec2f c = curve.pointAt(ticksT[i]);
tick.drawCentered(c.x, c.y, Colors.WHITE_FADE);
}
}
if (GameMod.HIDDEN.isActive()) {
final int hiddenDecayTime = game.getHiddenDecayTime();
final int hiddenTimeDiff = game.getHiddenTimeDiff();
if (fadeinScale <= 0f && timeDiff < hiddenTimeDiff + hiddenDecayTime) {
float hiddenAlpha = (timeDiff < hiddenTimeDiff) ? 0f : (timeDiff - hiddenTimeDiff) / (float) hiddenDecayTime;
alpha = Math.min(alpha, hiddenAlpha);
}
}
if (sliderClickedInitial)
@@ -216,7 +221,7 @@ public class Slider implements GameObject {
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
if (overlayAboveNumber)
hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
// repeats
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) {
@@ -232,7 +237,7 @@ public class Slider implements GameObject {
if (tcurRepeat % 2 == 0) {
// last circle
arrow.setRotation(curve.getEndAngle());
arrow.drawCentered(endPos[0], endPos[1]);
arrow.drawCentered(endPos.x, endPos.y);
} else {
// first circle
arrow.setRotation(curve.getStartAngle());
@@ -243,40 +248,41 @@ public class Slider implements GameObject {
if (timeDiff >= 0) {
// approach circle
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
if (!GameMod.HIDDEN.isActive())
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
} else {
// Since update() might not have run before drawing during a replay, the
// slider time may not have been calculated, which causes NAN numbers and flicker.
if (sliderTime == 0)
return;
float[] c = curve.pointAt(getT(trackPosition, false));
float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
Vec2f c = curve.pointAt(getT(trackPosition, false));
Vec2f c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
float t = getT(trackPosition, false);
// float dis = hitObject.getPixelLength() * HitObject.getXMultiplier() * (t - (int) t);
// Image sliderBallFrame = sliderBallImages[(int) (dis / (diameter * Math.PI) * 30) % sliderBallImages.length];
Image sliderBallFrame = sliderBallImages[(int) (t * sliderTime * 60 / 1000) % sliderBallImages.length];
float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI);
float angle = (float) (Math.atan2(c2.y - c.y, c2.x - c.x) * 180 / Math.PI);
sliderBallFrame.setRotation(angle);
sliderBallFrame.drawCentered(c[0], c[1]);
sliderBallFrame.drawCentered(c.x, c.y);
// follow circle
if (followCircleActive) {
GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c[0], c[1]);
GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c.x, c.y);
// "flashlight" mod: dim the screen
if (GameMod.FLASHLIGHT.isActive()) {
float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a = 0.75f;
g.setColor(Utils.COLOR_BLACK_ALPHA);
float oldAlphaBlack = Colors.BLACK_ALPHA.a;
Colors.BLACK_ALPHA.a = 0.75f;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, containerWidth, containerHeight);
Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack;
Colors.BLACK_ALPHA.a = oldAlphaBlack;
}
}
}
Utils.COLOR_WHITE_FADE.a = oldAlpha;
Colors.WHITE_FADE.a = oldAlpha;
}
/**
@@ -346,9 +352,9 @@ public class Slider implements GameObject {
float cx, cy;
HitObjectType type;
if (currentRepeats % 2 == 0) { // last circle
float[] lastPos = curve.pointAt(1);
cx = lastPos[0];
cy = lastPos[1];
Vec2f lastPos = curve.pointAt(1);
cx = lastPos.x;
cy = lastPos.y;
type = HitObjectType.SLIDER_LAST;
} else { // first circle
cx = x;
@@ -429,8 +435,8 @@ public class Slider implements GameObject {
// check if cursor pressed and within end circle
if (keyPressed || GameMod.RELAX.isActive()) {
float[] c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
Vec2f c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c.x - mouseX, c.y - mouseY);
if (distance < followRadius)
sliderHeldToEnd = true;
}
@@ -473,12 +479,11 @@ public class Slider implements GameObject {
}
// holding slider...
float[] c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
Vec2f c = curve.pointAt(getT(trackPosition, false));
double distance = Math.hypot(c.x - mouseX, c.y - mouseY);
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followRadius) || isAutoMod) {
// mouse pressed and within follow circle
followCircleActive = true;
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
// held during new repeat
if (isNewRepeat) {
@@ -489,14 +494,14 @@ public class Slider implements GameObject {
curve.getX(lastIndex), curve.getY(lastIndex), hitObject, currentRepeats);
} else // first circle
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
c[0], c[1], hitObject, currentRepeats);
c.x, c.y, hitObject, currentRepeats);
}
// held during new tick
if (isNewTick) {
ticksHit++;
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER10,
c[0], c[1], hitObject, currentRepeats);
c.x, c.y, hitObject, currentRepeats);
}
// held near end of slider
@@ -518,22 +523,16 @@ public class Slider implements GameObject {
public void updatePosition() {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
if (hitObject.getSliderType() == HitObject.SLIDER_PASSTHROUGH && hitObject.getSliderX().length == 2)
this.curve = new CircumscribedCircle(hitObject, color);
else if (hitObject.getSliderType() == HitObject.SLIDER_CATMULL)
this.curve = new CatmullCurve(hitObject, color);
else
this.curve = new LinearBezier(hitObject, color, hitObject.getSliderType() == HitObject.SLIDER_LINEAR);
this.curve = hitObject.getSliderCurve(true);
}
@Override
public float[] getPointAt(int trackPosition) {
public Vec2f getPointAt(int trackPosition) {
if (trackPosition <= hitObject.getTime())
return new float[] { x, y };
return new Vec2f(x, y);
else if (trackPosition >= hitObject.getTime() + sliderTimeTotal) {
if (hitObject.getRepeatCount() % 2 == 0)
return new float[] { x, y };
return new Vec2f(x, y);
else
return curve.pointAt(1);
} else

View File

@@ -27,7 +27,9 @@ import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.states.Game;
import itdelatrisu.opsu.ui.Colors;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
@@ -50,9 +52,6 @@ public class Spinner implements GameObject {
/** The amount of time, in milliseconds, before another velocity is stored. */
private static final float DELTA_UPDATE_TIME = 1000 / 60f;
/** The amount of time, in milliseconds, to fade in the spinner. */
private static final int FADE_IN_TIME = 500;
/** Angle mod multipliers: "auto" (477rpm), "spun out" (287rpm) */
private static final float
AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI;
@@ -69,6 +68,9 @@ public class Spinner implements GameObject {
/** The associated HitObject. */
private HitObject hitObject;
/** The associated Game object. */
private Game game;
/** The associated GameData object. */
private GameData data;
@@ -124,6 +126,7 @@ public class Spinner implements GameObject {
*/
public Spinner(HitObject hitObject, Game game, GameData data) {
this.hitObject = hitObject;
this.game = game;
this.data = data;
/*
@@ -162,7 +165,7 @@ public class Spinner implements GameObject {
final int maxVel = 48;
final int minTime = 2000;
final int maxTime = 5000;
maxStoredDeltaAngles = (int) Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime)
maxStoredDeltaAngles = Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime)
* (maxVel - minVel) / (maxTime - minTime) + minVel, minVel, maxVel);
storedDeltaAngle = new float[maxStoredDeltaAngles];
@@ -175,20 +178,21 @@ public class Spinner implements GameObject {
public void draw(Graphics g, int trackPosition) {
// only draw spinners shortly before start time
int timeDiff = hitObject.getTime() - trackPosition;
if (timeDiff - FADE_IN_TIME > 0)
final int fadeInTime = game.getFadeInTime();
if (timeDiff - fadeInTime > 0)
return;
boolean spinnerComplete = (rotations >= rotationsNeeded);
float alpha = Utils.clamp(1 - (float) timeDiff / FADE_IN_TIME, 0f, 1f);
float alpha = Utils.clamp(1 - (float) timeDiff / fadeInTime, 0f, 1f);
// darken screen
if (Options.getSkin().isSpinnerFadePlayfield()) {
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
float oldAlpha = Colors.BLACK_ALPHA.a;
if (timeDiff > 0)
Utils.COLOR_BLACK_ALPHA.a *= alpha;
g.setColor(Utils.COLOR_BLACK_ALPHA);
Colors.BLACK_ALPHA.a *= alpha;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
Colors.BLACK_ALPHA.a = oldAlpha;
}
// rpm
@@ -210,13 +214,15 @@ public class Spinner implements GameObject {
spinnerMetreSub.draw(0, height - spinnerMetreSub.getHeight());
// main spinner elements
float approachScale = 1 - Utils.clamp(((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime())), 0f, 1f);
GameImage.SPINNER_CIRCLE.getImage().setAlpha(alpha);
GameImage.SPINNER_CIRCLE.getImage().setRotation(drawRotation * 360f);
GameImage.SPINNER_CIRCLE.getImage().drawCentered(width / 2, height / 2);
Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale);
approachCircleScaled.setAlpha(alpha);
approachCircleScaled.drawCentered(width / 2, height / 2);
if (!GameMod.HIDDEN.isActive()) {
float approachScale = 1 - Utils.clamp(((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime())), 0f, 1f);
Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale);
approachCircleScaled.setAlpha(alpha);
approachCircleScaled.drawCentered(width / 2, height / 2);
}
GameImage.SPINNER_SPIN.getImage().setAlpha(alpha);
GameImage.SPINNER_SPIN.getImage().drawCentered(width / 2, height * 3 / 4);
@@ -342,7 +348,7 @@ public class Spinner implements GameObject {
public void updatePosition() {}
@Override
public float[] getPointAt(int trackPosition) {
public Vec2f getPointAt(int trackPosition) {
// get spinner time
int timeDiff;
float x = hitObject.getScaledX(), y = hitObject.getScaledY();
@@ -357,10 +363,7 @@ public class Spinner implements GameObject {
float multiplier = (GameMod.AUTO.isActive()) ? AUTO_MULTIPLIER : SPUN_OUT_MULTIPLIER;
float angle = (timeDiff * multiplier) - HALF_PI;
final float r = height / 10f;
return new float[] {
(float) (x + r * Math.cos(angle)),
(float) (y + r * Math.sin(angle))
};
return new Vec2f((float) (x + r * Math.cos(angle)), (float) (y + r * Math.sin(angle)));
}
@Override

View File

@@ -18,14 +18,10 @@
package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.beatmap.HitObject;
import java.util.LinkedList;
import org.newdawn.slick.Color;
import org.newdawn.slick.SlickException;
/**
* Representation of Catmull Curve with equidistant points.
*
@@ -35,10 +31,18 @@ public class CatmullCurve extends EqualDistanceMultiCurve {
/**
* Constructor.
* @param hitObject the associated HitObject
* @param color the color of this curve
*/
public CatmullCurve(HitObject hitObject, Color color) {
super(hitObject, color);
public CatmullCurve(HitObject hitObject) {
this(hitObject, true);
}
/**
* Constructor.
* @param hitObject the associated HitObject
* @param scaled whether to use scaled coordinates
*/
public CatmullCurve(HitObject hitObject, boolean scaled) {
super(hitObject, scaled);
LinkedList<CurveType> catmulls = new LinkedList<CurveType>();
int ncontrolPoints = hitObject.getSliderX().length + 1;
LinkedList<Vec2f> points = new LinkedList<Vec2f>(); // temporary list of points to separate different curves
@@ -53,24 +57,15 @@ public class CatmullCurve extends EqualDistanceMultiCurve {
for (int i = 0; i < ncontrolPoints; i++) {
points.addLast(new Vec2f(getX(i), getY(i)));
if (points.size() >= 4) {
try {
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
} catch (SlickException e) {
ErrorHandler.error(null, e, true);
}
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
points.removeFirst();
}
}
if (getX(ncontrolPoints - 1) != getX(ncontrolPoints - 2)
||getY(ncontrolPoints - 1) != getY(ncontrolPoints - 2))
points.addLast(new Vec2f(getX(ncontrolPoints - 1), getY(ncontrolPoints - 1)));
if (points.size() >= 4) {
try {
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
} catch (SlickException e) {
ErrorHandler.error(null, e, true);
}
}
if (getX(ncontrolPoints - 1) != getX(ncontrolPoints - 2) ||
getY(ncontrolPoints - 1) != getY(ncontrolPoints - 2))
points.addLast(new Vec2f(getX(ncontrolPoints - 1), getY(ncontrolPoints - 1)));
if (points.size() >= 4)
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
init(catmulls);
}

View File

@@ -18,8 +18,6 @@
package itdelatrisu.opsu.objects.curves;
import org.newdawn.slick.SlickException;
/**
* Representation of a Centripetal CatmullRom spline.
* (Currently not technically Centripetal CatmullRom.)
@@ -37,11 +35,10 @@ public class CentripetalCatmullRom extends CurveType {
/**
* Constructor.
* @param points the control points of the curve
* @throws SlickException
*/
protected CentripetalCatmullRom(Vec2f[] points) throws SlickException {
protected CentripetalCatmullRom(Vec2f[] points) {
if (points.length != 4)
throw new SlickException(String.format("Need exactly 4 points to initialize CentripetalCatmullRom, %d provided.", points.length));
throw new RuntimeException(String.format("Need exactly 4 points to initialize CentripetalCatmullRom, %d provided.", points.length));
this.points = points;
time = new float[4];

View File

@@ -18,11 +18,9 @@
package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import org.newdawn.slick.Color;
/**
* Representation of a curve along a Circumscribed Circle of three points.
* http://en.wikipedia.org/wiki/Circumscribed_circle
@@ -53,10 +51,18 @@ public class CircumscribedCircle extends Curve {
/**
* Constructor.
* @param hitObject the associated HitObject
* @param color the color of this curve
*/
public CircumscribedCircle(HitObject hitObject, Color color) {
super(hitObject, color);
public CircumscribedCircle(HitObject hitObject) {
this(hitObject, true);
}
/**
* Constructor.
* @param hitObject the associated HitObject
* @param scaled whether to use scaled coordinates
*/
public CircumscribedCircle(HitObject hitObject, boolean scaled) {
super(hitObject, scaled);
// construct the three points
this.start = new Vec2f(getX(0), getY(0));
@@ -70,8 +76,6 @@ public class CircumscribedCircle extends Curve {
Vec2f norb = mid.cpy().sub(end).nor();
this.circleCenter = intersect(mida, nora, midb, norb);
if (circleCenter == null)
return;
// find the angles relative to the circle center
Vec2f startAngPoint = start.cpy().sub(circleCenter);
@@ -92,13 +96,8 @@ public class CircumscribedCircle extends Curve {
startAng -= TWO_PI;
else if (Math.abs(startAng - (endAng - TWO_PI)) < TWO_PI && isIn(startAng, midAng, endAng - (TWO_PI)))
endAng -= TWO_PI;
else {
ErrorHandler.error(
String.format("Cannot find angles between midAng (%.3f %.3f %.3f).",
startAng, midAng, endAng), null, true
);
return;
}
else
throw new RuntimeException(String.format("Cannot find angles between midAng (%.3f %.3f %.3f).", startAng, midAng, endAng));
}
// find an angle with an arc length of pixelLength along this circle
@@ -116,10 +115,8 @@ public class CircumscribedCircle extends Curve {
// calculate points
float step = hitObject.getPixelLength() / CURVE_POINTS_SEPERATION;
curve = new Vec2f[(int) step + 1];
for (int i = 0; i < curve.length; i++) {
float[] xy = pointAt(i / step);
curve[i] = new Vec2f(xy[0], xy[1]);
}
for (int i = 0; i < curve.length; i++)
curve[i] = pointAt(i / step);
}
/**
@@ -151,21 +148,19 @@ public class CircumscribedCircle extends Curve {
//u = ((b.y-a.y)ta.x +(a.x-b.x)ta.y) / (tb.x*ta.y - tb.y*ta.x);
float des = tb.x * ta.y - tb.y * ta.x;
if (Math.abs(des) < 0.00001f) {
ErrorHandler.error("Vectors are parallel.", null, true);
return null;
}
if (Math.abs(des) < 0.00001f)
throw new RuntimeException("Vectors are parallel.");
float u = ((b.y - a.y) * ta.x + (a.x - b.x) * ta.y) / des;
return b.cpy().add(tb.x * u, tb.y * u);
}
@Override
public float[] pointAt(float t) {
float ang = lerp(startAng, endAng, t);
return new float[] {
public Vec2f pointAt(float t) {
float ang = Utils.lerp(startAng, endAng, t);
return new Vec2f(
(float) (Math.cos(ang) * radius + circleCenter.x),
(float) (Math.sin(ang) * radius + circleCenter.y)
};
);
}
@Override

View File

@@ -20,10 +20,10 @@ package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.render.CurveRenderState;
import itdelatrisu.opsu.skins.Skin;
import itdelatrisu.opsu.ui.Colors;
import org.lwjgl.opengl.ContextCapabilities;
import org.lwjgl.opengl.GLContext;
@@ -64,14 +64,21 @@ public abstract class Curve {
/**
* Constructor.
* @param hitObject the associated HitObject
* @param color the color of this curve
* @param scaled whether to use scaled coordinates
*/
protected Curve(HitObject hitObject, Color color) {
protected Curve(HitObject hitObject, boolean scaled) {
this.hitObject = hitObject;
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
this.sliderX = hitObject.getScaledSliderX();
this.sliderY = hitObject.getScaledSliderY();
if (scaled) {
this.x = hitObject.getScaledX();
this.y = hitObject.getScaledY();
this.sliderX = hitObject.getScaledSliderX();
this.sliderY = hitObject.getScaledSliderY();
} else {
this.x = hitObject.getX();
this.y = hitObject.getY();
this.sliderX = hitObject.getSliderX();
this.sliderY = hitObject.getSliderY();
}
this.renderState = null;
}
@@ -80,28 +87,28 @@ public abstract class Curve {
* Should be called before any curves are drawn.
* @param width the container width
* @param height the container height
* @param circleSize the circle size
* @param circleDiameter the circle diameter
* @param borderColor the curve border color
*/
public static void init(int width, int height, float circleSize, Color borderColor) {
public static void init(int width, int height, float circleDiameter, Color borderColor) {
Curve.borderColor = borderColor;
ContextCapabilities capabilities = GLContext.getCapabilities();
mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL32;
mmsliderSupported = capabilities.GL_EXT_framebuffer_object;
if (mmsliderSupported)
CurveRenderState.init(width, height, circleSize);
CurveRenderState.init(width, height, circleDiameter);
else {
if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER)
Log.warn("New slider style requires FBO support and OpenGL 3.2.");
Log.warn("New slider style requires FBO support.");
}
}
/**
* Returns the point on the curve at a value t.
* @param t the t value [0, 1]
* @return the point [x, y]
* @return the position vector
*/
public abstract float[] pointAt(float t);
public abstract Vec2f pointAt(float t);
/**
* Draws the full curve to the graphics context.
@@ -116,7 +123,7 @@ public abstract class Curve {
Image hitCircle = GameImage.HITCIRCLE.getImage();
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
for (int i = 0; i < curve.length; i++)
hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE);
hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Colors.WHITE_FADE);
for (int i = 0; i < curve.length; i++)
hitCircle.drawCentered(curve[i].x, curve[i].y, color);
}
@@ -151,13 +158,6 @@ public abstract class Curve {
*/
public float getY(int i) { return (i == 0) ? y : sliderY[i - 1]; }
/**
* Linear interpolation of a and b at t.
*/
protected float lerp(float a, float b, float t) {
return a * (1 - t) + b * t;
}
/**
* Discards the slider cache (only used for mmsliders).
*/

View File

@@ -18,13 +18,12 @@
package itdelatrisu.opsu.objects.curves;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import java.util.Iterator;
import java.util.LinkedList;
import org.newdawn.slick.Color;
/**
* Representation of multiple curve with equidistant points.
* http://pomax.github.io/bezierinfo/#tracing
@@ -41,10 +40,18 @@ public abstract class EqualDistanceMultiCurve extends Curve {
/**
* Constructor.
* @param hitObject the associated HitObject
* @param color the color of this curve
*/
public EqualDistanceMultiCurve(HitObject hitObject, Color color) {
super(hitObject, color);
public EqualDistanceMultiCurve(HitObject hitObject) {
this(hitObject, true);
}
/**
* Constructor.
* @param hitObject the associated HitObject
* @param scaled whether to use scaled coordinates
*/
public EqualDistanceMultiCurve(HitObject hitObject, boolean scaled) {
super(hitObject, scaled);
}
/**
@@ -94,7 +101,7 @@ public abstract class EqualDistanceMultiCurve extends Curve {
// interpolate the point between the two closest distances
if (distanceAt - lastDistanceAt > 1) {
float t = (prefDistance - lastDistanceAt) / (distanceAt - lastDistanceAt);
curve[i] = new Vec2f(lerp(lastCurve.x, thisCurve.x, t), lerp(lastCurve.y, thisCurve.y, t));
curve[i] = new Vec2f(Utils.lerp(lastCurve.x, thisCurve.x, t), Utils.lerp(lastCurve.y, thisCurve.y, t));
} else
curve[i] = thisCurve;
}
@@ -117,20 +124,19 @@ public abstract class EqualDistanceMultiCurve extends Curve {
}
@Override
public float[] pointAt(float t) {
public Vec2f pointAt(float t) {
float indexF = t * ncurve;
int index = (int) indexF;
if (index >= ncurve) {
Vec2f poi = curve[ncurve];
return new float[] { poi.x, poi.y };
} else {
if (index >= ncurve)
return curve[ncurve].cpy();
else {
Vec2f poi = curve[index];
Vec2f poi2 = curve[index + 1];
float t2 = indexF - index;
return new float[] {
lerp(poi.x, poi2.x, t2),
lerp(poi.y, poi2.y, t2)
};
return new Vec2f(
Utils.lerp(poi.x, poi2.x, t2),
Utils.lerp(poi.y, poi2.y, t2)
);
}
}

View File

@@ -22,8 +22,6 @@ import itdelatrisu.opsu.beatmap.HitObject;
import java.util.LinkedList;
import org.newdawn.slick.Color;
/**
* Representation of Bezier curve with equidistant points.
* http://pomax.github.io/bezierinfo/#tracing
@@ -34,11 +32,20 @@ public class LinearBezier extends EqualDistanceMultiCurve {
/**
* Constructor.
* @param hitObject the associated HitObject
* @param color the color of this curve
* @param line whether a new curve should be generated for each sequential pair
*/
public LinearBezier(HitObject hitObject, Color color, boolean line) {
super(hitObject, color);
public LinearBezier(HitObject hitObject, boolean line) {
this(hitObject, line, true);
}
/**
* Constructor.
* @param hitObject the associated HitObject
* @param line whether a new curve should be generated for each sequential pair
* @param scaled whether to use scaled coordinates
*/
public LinearBezier(HitObject hitObject, boolean line, boolean scaled) {
super(hitObject, scaled);
LinkedList<CurveType> beziers = new LinkedList<CurveType>();

View File

@@ -40,6 +40,16 @@ public class Vec2f {
*/
public Vec2f() {}
/**
* Sets the x and y components of this vector.
* @return itself (for chaining)
*/
public Vec2f set(float nx, float ny) {
x = nx;
y = ny;
return this;
}
/**
* Finds the midpoint between this vector and another vector.
* @param o the other vector
@@ -93,6 +103,17 @@ public class Vec2f {
return this;
}
/**
* Turns this vector into a unit vector.
* @return itself (for chaining)
*/
public Vec2f normalize() {
float len = len();
x /= len;
y /= len;
return this;
}
/**
* Returns a copy of this vector.
*/

View File

@@ -18,20 +18,21 @@
package itdelatrisu.opsu.render;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.beatmap.HitObject;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.ui.Colors;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.EXTFramebufferObject;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL14;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
import org.newdawn.slick.util.Log;
@@ -63,15 +64,14 @@ public class CurveRenderState {
* Should be called before any curves are drawn.
* @param width the container width
* @param height the container height
* @param circleSize the circle size
* @param circleDiameter the circle diameter
*/
public static void init(int width, int height, float circleSize) {
public static void init(int width, int height, float circleDiameter) {
containerWidth = width;
containerHeight = height;
// equivalent to what happens in Slider.init()
scale = (int) (104 - (circleSize * 8));
scale = (int) (scale * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
scale = (int) (circleDiameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
//scale = scale * 118 / 128; //for curves exactly as big as the sliderball
FrameBufferCache.init(width, height);
}
@@ -114,20 +114,25 @@ public class CurveRenderState {
mapping = cache.insert(hitObject);
fbo = mapping;
int old_fb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING);
int old_tex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
int oldFb = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT);
int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID());
//glGetInteger requires a buffer of size 16, even though just 4
//values are returned in this specific case
IntBuffer oldViewport = BufferUtils.createIntBuffer(16);
GL11.glGetInteger(GL11.GL_VIEWPORT, oldViewport);
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fbo.getID());
GL11.glViewport(0, 0, fbo.width, fbo.height);
GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
Utils.COLOR_WHITE_FADE.a = 1.0f;
Colors.WHITE_FADE.a = 1.0f;
this.draw_curve(color, borderColor, curve);
color.a = 1f;
GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_tex);
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_fb);
Utils.COLOR_WHITE_FADE.a = alpha;
GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex);
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFb);
GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3));
Colors.WHITE_FADE.a = alpha;
}
// draw a fullscreen quad with the texture that contains the curve
@@ -389,7 +394,7 @@ public class CurveRenderState {
buff.flip();
GL11.glBindTexture(GL11.GL_TEXTURE_1D, gradientTexture);
GL11.glTexImage1D(GL11.GL_TEXTURE_1D, 0, GL11.GL_RGBA, slider.getWidth(), 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buff);
GL30.glGenerateMipmap(GL11.GL_TEXTURE_1D);
EXTFramebufferObject.glGenerateMipmapEXT(GL11.GL_TEXTURE_1D);
}
}
@@ -402,12 +407,12 @@ public class CurveRenderState {
program = GL20.glCreateProgram();
int vtxShdr = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
int frgShdr = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
GL20.glShaderSource(vtxShdr, "#version 330\n"
GL20.glShaderSource(vtxShdr, "#version 110\n"
+ "\n"
+ "layout(location = 0) in vec4 in_position;\n"
+ "layout(location = 1) in vec2 in_tex_coord;\n"
+ "attribute vec4 in_position;\n"
+ "attribute vec2 in_tex_coord;\n"
+ "\n"
+ "out vec2 tex_coord;\n"
+ "varying vec2 tex_coord;\n"
+ "void main()\n"
+ "{\n"
+ " gl_Position = in_position;\n"
@@ -419,22 +424,21 @@ public class CurveRenderState {
String error = GL20.glGetShaderInfoLog(vtxShdr, 1024);
Log.error("Vertex Shader compilation failed.", new Exception(error));
}
GL20.glShaderSource(frgShdr, "#version 330\n"
GL20.glShaderSource(frgShdr, "#version 110\n"
+ "\n"
+ "uniform sampler1D tex;\n"
+ "uniform vec2 tex_size;\n"
+ "uniform vec3 col_tint;\n"
+ "uniform vec4 col_border;\n"
+ "\n"
+ "in vec2 tex_coord;\n"
+ "layout(location = 0) out vec4 out_colour;\n"
+ "varying vec2 tex_coord;\n"
+ "\n"
+ "void main()\n"
+ "{\n"
+ " vec4 in_color = texture(tex, tex_coord.x);\n"
+ " vec4 in_color = texture1D(tex, tex_coord.x);\n"
+ " float blend_factor = in_color.r-in_color.b;\n"
+ " vec4 new_color = vec4(mix(in_color.xyz*col_border.xyz,col_tint,blend_factor),in_color.w);\n"
+ " out_colour = new_color;\n"
+ " gl_FragColor = new_color;\n"
+ "}");
GL20.glCompileShader(frgShdr);
res = GL20.glGetShaderi(frgShdr, GL20.GL_COMPILE_STATUS);

View File

@@ -19,10 +19,8 @@ package itdelatrisu.opsu.render;
import java.nio.ByteBuffer;
import org.lwjgl.opengl.EXTFramebufferObject;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.GL32;
/**
* Represents a rendertarget. For now this maps to an OpenGL FBO via LWJGL.
@@ -50,16 +48,16 @@ public class Rendertarget {
private Rendertarget(int width, int height) {
this.width = width;
this.height = height;
fboID = GL30.glGenFramebuffers();
fboID = EXTFramebufferObject.glGenFramebuffersEXT();
textureID = GL11.glGenTextures();
depthBufferID = GL30.glGenRenderbuffers();
depthBufferID = EXTFramebufferObject.glGenRenderbuffersEXT();
}
/**
* Bind this rendertarget as the primary framebuffer.
*/
public void bind() {
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fboID);
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fboID);
}
/**
@@ -83,7 +81,7 @@ public class Rendertarget {
* Bind the default framebuffer.
*/
public static void unbind() {
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, 0);
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);
}
/**
@@ -93,7 +91,7 @@ public class Rendertarget {
* @param height the height
*/
public static Rendertarget createRTTFramebuffer(int width, int height) {
int old_framebuffer = GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING);
int old_framebuffer = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT);
int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
Rendertarget buffer = new Rendertarget(width,height);
buffer.bind();
@@ -104,15 +102,14 @@ public class Rendertarget {
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, buffer.depthBufferID);
GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL11.GL_DEPTH_COMPONENT, width, height);
GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, buffer.depthBufferID);
EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID);
EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, GL11.GL_DEPTH_COMPONENT, width, height);
EXTFramebufferObject.glFramebufferRenderbufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID);
GL32.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, fboTexture, 0);
GL20.glDrawBuffers(GL30.GL_COLOR_ATTACHMENT0);
EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, fboTexture, 0);
GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture);
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_framebuffer);
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, old_framebuffer);
return buffer;
}
@@ -122,8 +119,8 @@ public class Rendertarget {
* to use this rendertarget with OpenGL after calling this method.
*/
public void destroyRTT() {
GL30.glDeleteFramebuffers(fboID);
GL30.glDeleteRenderbuffers(depthBufferID);
EXTFramebufferObject.glDeleteFramebuffersEXT(fboID);
EXTFramebufferObject.glDeleteRenderbuffersEXT(depthBufferID);
GL11.glDeleteTextures(textureID);
}
}

View File

@@ -25,10 +25,10 @@ package itdelatrisu.opsu.replay;
*/
public class LifeFrame {
/** Time. */
private int time;
private final int time;
/** Percentage. */
private float percentage;
private final float percentage;
/**
* Constructor.

View File

@@ -35,14 +35,14 @@ public enum PlaybackSpeed {
HALF (GameImage.REPLAY_PLAYBACK_HALF, 0.5f);
/** The button image. */
private GameImage gameImage;
private final GameImage gameImage;
/** The playback speed modifier. */
private final float modifier;
/** The button. */
private MenuButton button;
/** The playback speed modifier. */
private float modifier;
/** Enum values. */
private static PlaybackSpeed[] values = PlaybackSpeed.values();

View File

@@ -41,11 +41,11 @@ import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import lzma.streams.LzmaOutputStream;
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
import org.newdawn.slick.util.Log;
import lzma.streams.LzmaOutputStream;
/**
* Captures osu! replay data.
* https://osu.ppy.sh/wiki/Osr_%28file_format%29

View File

@@ -38,13 +38,13 @@ public class ReplayFrame {
private int timeDiff;
/** Time, in milliseconds. */
private int time;
private final int time;
/** Cursor coordinates (in OsuPixels). */
private float x, y;
private final float x, y;
/** Keys pressed (bitmask). */
private int keys;
private final int keys;
/**
* Returns the start frame.
@@ -81,7 +81,8 @@ public class ReplayFrame {
public int getTimeDiff() { return timeDiff; }
/**
* Sets the time since the previous action, in milliseconds.
* Sets the time since the previous action.
* @param diff the time difference, in milliseconds
*/
public void setTimeDiff(int diff) { this.timeDiff = diff; }

View File

@@ -29,8 +29,11 @@ import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.util.ArrayList;
import java.util.List;
@@ -187,15 +190,15 @@ public class ButtonMenu extends BasicGameState {
float mult = GameMod.getScoreMultiplier();
String multString = String.format("Score Multiplier: %.2fx", mult);
Color multColor = (mult == 1f) ? Color.white : (mult > 1f) ? Color.green : Color.red;
float multY = Utils.FONT_LARGE.getLineHeight() * 2 + height * 0.06f;
Utils.FONT_LARGE.drawString(
(width - Utils.FONT_LARGE.getWidth(multString)) / 2f,
float multY = Fonts.LARGE.getLineHeight() * 2 + height * 0.06f;
Fonts.LARGE.drawString(
(width - Fonts.LARGE.getWidth(multString)) / 2f,
multY, multString, multColor);
// category text
for (GameMod.Category category : GameMod.Category.values()) {
Utils.FONT_LARGE.drawString(category.getX(),
category.getY() - Utils.FONT_LARGE.getLineHeight() / 2f,
Fonts.LARGE.drawString(category.getX(),
category.getY() - Fonts.LARGE.getLineHeight() / 2f,
category.getName(), category.getColor());
}
@@ -217,7 +220,7 @@ public class ButtonMenu extends BasicGameState {
}
// tooltips
if (hoverMod != null && hoverMod.isImplemented())
if (hoverMod != null)
UI.updateTooltip(delta, hoverMod.getDescription(), true);
}
@@ -253,7 +256,7 @@ public class ButtonMenu extends BasicGameState {
};
/** The buttons in the state. */
private Button[] buttons;
private final Button[] buttons;
/** The associated MenuButton objects. */
private MenuButton[] menuButtons;
@@ -261,8 +264,11 @@ public class ButtonMenu extends BasicGameState {
/** The actual title string list, generated upon entering the state. */
private List<String> actualTitle;
/** The horizontal center offset, used for the initial button animation. */
private AnimatedValue centerOffset;
/** Initial x coordinate offsets left/right of center (for shifting animation), times width. (TODO) */
private static final float OFFSET_WIDTH_RATIO = 1 / 18f;
private static final float OFFSET_WIDTH_RATIO = 1 / 25f;
/**
* Constructor.
@@ -288,7 +294,7 @@ public class ButtonMenu extends BasicGameState {
menuButtons = new MenuButton[buttons.length];
for (int i = 0; i < buttons.length; i++) {
MenuButton b = new MenuButton(button, buttonL, buttonR, center, baseY + (i * offsetY));
b.setText(String.format("%d. %s", i + 1, buttons[i].getText()), Utils.FONT_XLARGE, Color.white);
b.setText(String.format("%d. %s", i + 1, buttons[i].getText()), Fonts.XLARGE, Color.white);
b.setHoverFade();
menuButtons[i] = b;
}
@@ -301,7 +307,7 @@ public class ButtonMenu extends BasicGameState {
*/
protected float getBaseY(GameContainer container, StateBasedGame game) {
float baseY = container.getHeight() * 0.2f;
baseY += ((getTitle(container, game).length - 1) * Utils.FONT_LARGE.getLineHeight());
baseY += ((getTitle(container, game).length - 1) * Fonts.LARGE.getLineHeight());
return baseY;
}
@@ -315,9 +321,9 @@ public class ButtonMenu extends BasicGameState {
// draw title
if (actualTitle != null) {
float marginX = container.getWidth() * 0.015f, marginY = container.getHeight() * 0.01f;
int lineHeight = Utils.FONT_LARGE.getLineHeight();
int lineHeight = Fonts.LARGE.getLineHeight();
for (int i = 0, size = actualTitle.size(); i < size; i++)
Utils.FONT_LARGE.drawString(marginX, marginY + (i * lineHeight), actualTitle.get(i), Color.white);
Fonts.LARGE.drawString(marginX, marginY + (i * lineHeight), actualTitle.get(i), Color.white);
}
// draw buttons
@@ -336,18 +342,14 @@ public class ButtonMenu extends BasicGameState {
*/
public void update(GameContainer container, int delta, int mouseX, int mouseY) {
float center = container.getWidth() / 2f;
boolean centerOffsetUpdated = centerOffset.update(delta);
float centerOffsetX = centerOffset.getValue();
for (int i = 0; i < buttons.length; i++) {
menuButtons[i].hoverUpdate(delta, mouseX, mouseY);
// move button to center
float x = menuButtons[i].getX();
if (i % 2 == 0) {
if (x < center)
menuButtons[i].setX(Math.min(x + (delta / 5f), center));
} else {
if (x > center)
menuButtons[i].setX(Math.max(x - (delta / 5f), center));
}
if (centerOffsetUpdated)
menuButtons[i].setX((i % 2 == 0) ? center + centerOffsetX : center - centerOffsetX);
}
}
@@ -404,9 +406,10 @@ public class ButtonMenu extends BasicGameState {
*/
public void enter(GameContainer container, StateBasedGame game) {
float center = container.getWidth() / 2f;
float centerOffset = container.getWidth() * OFFSET_WIDTH_RATIO;
float centerOffsetX = container.getWidth() * OFFSET_WIDTH_RATIO;
centerOffset = new AnimatedValue(700, centerOffsetX, 0, AnimationEquation.OUT_BOUNCE);
for (int i = 0; i < buttons.length; i++) {
menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffset * -1 : centerOffset));
menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffsetX : centerOffsetX * -1));
menuButtons[i].resetHover();
}
@@ -416,8 +419,8 @@ public class ButtonMenu extends BasicGameState {
int maxLineWidth = (int) (container.getWidth() * 0.96f);
for (int i = 0; i < title.length; i++) {
// wrap text if too long
if (Utils.FONT_LARGE.getWidth(title[i]) > maxLineWidth) {
List<String> list = Utils.wrap(title[i], Utils.FONT_LARGE, maxLineWidth);
if (Fonts.LARGE.getWidth(title[i]) > maxLineWidth) {
List<String> list = Fonts.wrap(Fonts.LARGE, title[i], maxLineWidth);
actualTitle.addAll(list);
} else
actualTitle.add(title[i]);
@@ -545,10 +548,10 @@ public class ButtonMenu extends BasicGameState {
};
/** The text to show on the button. */
private String text;
private final String text;
/** The button color. */
private Color color;
private final Color color;
/**
* Constructor.
@@ -591,7 +594,7 @@ public class ButtonMenu extends BasicGameState {
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
public ButtonMenu(int state) {
this.state = state;

View File

@@ -21,7 +21,6 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
@@ -29,13 +28,19 @@ import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.downloads.Download;
import itdelatrisu.opsu.downloads.DownloadList;
import itdelatrisu.opsu.downloads.DownloadNode;
import itdelatrisu.opsu.downloads.servers.BloodcatServer;
import itdelatrisu.opsu.downloads.servers.DownloadServer;
import itdelatrisu.opsu.downloads.servers.HexideServer;
import itdelatrisu.opsu.downloads.servers.OsuMirrorServer;
import itdelatrisu.opsu.downloads.servers.MengSkyServer;
import itdelatrisu.opsu.downloads.servers.MnetworkServer;
import itdelatrisu.opsu.downloads.servers.YaSOnlineServer;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.DropdownMenu;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.KinecticScrolling;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
@@ -75,10 +80,10 @@ public class DownloadsMenu extends BasicGameState {
private static final int MIN_REQUEST_INTERVAL = 300;
/** Available beatmap download servers. */
private static final DownloadServer[] SERVERS = { new BloodcatServer(), new OsuMirrorServer(), new HexideServer() };
/** The beatmap download server index. */
private int serverIndex = 0;
private static final DownloadServer[] SERVERS = {
new BloodcatServer(), new HexideServer(), new YaSOnlineServer(),
new MnetworkServer(), new MengSkyServer()
};
/** The current list of search results. */
private DownloadNode[] resultList;
@@ -137,14 +142,14 @@ public class DownloadsMenu extends BasicGameState {
/** Page direction for last query. */
private Page lastQueryDir = Page.RESET;
/** Number of active requests. */
private int activeRequests = 0;
/** Previous and next page buttons. */
private MenuButton prevPage, nextPage;
/** Buttons. */
private MenuButton clearButton, importButton, resetButton, rankedButton, serverButton;
private MenuButton clearButton, importButton, resetButton, rankedButton;
/** Dropdown menu. */
private DropdownMenu<DownloadServer> serverMenu;
/** Beatmap importing thread. */
private Thread importThread;
@@ -155,11 +160,97 @@ public class DownloadsMenu extends BasicGameState {
/** The bar notification to send upon entering the state. */
private String barNotificationOnLoad;
/** Search query, executed in {@code queryThread}. */
private SearchQuery searchQuery;
/** Search query helper class. */
private class SearchQuery implements Runnable {
/** The search query. */
private final String query;
/** The download server. */
private final DownloadServer server;
/** Whether the query was interrupted. */
private boolean interrupted = false;
/** Whether the query has completed execution. */
private boolean complete = false;
/**
* Constructor.
* @param query the search query
* @param server the download server
*/
public SearchQuery(String query, DownloadServer server) {
this.query = query;
this.server = server;
}
/** Interrupt the query and prevent the results from being processed, if not already complete. */
public void interrupt() { interrupted = true; }
/** Returns whether the query has completed execution. */
public boolean isComplete() { return complete; }
@Override
public void run() {
// check page direction
Page lastPageDir = pageDir;
pageDir = Page.RESET;
int lastPageSize = (resultList != null) ? resultList.length : 0;
int newPage = page;
if (lastPageDir == Page.RESET)
newPage = 1;
else if (lastPageDir == Page.NEXT)
newPage++;
else if (lastPageDir == Page.PREVIOUS)
newPage--;
try {
DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
if (!interrupted) {
// update page total
page = newPage;
if (nodes != null) {
if (lastPageDir == Page.NEXT)
pageResultTotal += nodes.length;
else if (lastPageDir == Page.PREVIOUS)
pageResultTotal -= lastPageSize;
else if (lastPageDir == Page.RESET)
pageResultTotal = nodes.length;
} else
pageResultTotal = 0;
resultList = nodes;
totalResults = server.totalResults();
focusResult = -1;
startResultPos.setPosition(0);
if (nodes == null)
searchResultString = "An error has occurred.";
else {
if (query.isEmpty())
searchResultString = "Type to search!";
else if (totalResults == 0 || resultList.length == 0)
searchResultString = "No results found.";
else
searchResultString = String.format("%d result%s found!",
totalResults, (totalResults == 1) ? "" : "s");
}
}
} catch (IOException e) {
if (!interrupted)
searchResultString = "Could not establish connection to server.";
} finally {
complete = true;
}
}
}
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
public DownloadsMenu(int state) {
this.state = state;
@@ -175,17 +266,17 @@ public class DownloadsMenu extends BasicGameState {
int width = container.getWidth();
int height = container.getHeight();
float baseX = width * 0.024f;
float searchY = (height * 0.04f) + Utils.FONT_LARGE.getLineHeight();
float searchY = (height * 0.04f) + Fonts.LARGE.getLineHeight();
float searchWidth = width * 0.3f;
// search
searchTimer = SEARCH_DELAY;
searchResultString = "Loading data from server...";
search = new TextField(
container, Utils.FONT_DEFAULT, (int) baseX, (int) searchY,
(int) searchWidth, Utils.FONT_MEDIUM.getLineHeight()
container, Fonts.DEFAULT, (int) baseX, (int) searchY,
(int) searchWidth, Fonts.MEDIUM.getLineHeight()
);
search.setBackgroundColor(DownloadNode.BG_NORMAL);
search.setBackgroundColor(Colors.BLACK_BG_NORMAL);
search.setBorderColor(Color.white);
search.setTextColor(Color.white);
search.setConsumeEvents(false);
@@ -208,9 +299,8 @@ public class DownloadsMenu extends BasicGameState {
float buttonHeight = height * 0.038f;
float resetWidth = width * 0.085f;
float rankedWidth = width * 0.15f;
float serverWidth = width * 0.12f;
float lowerWidth = width * 0.12f;
float topButtonY = searchY + Utils.FONT_MEDIUM.getLineHeight() / 2f;
float topButtonY = searchY + Fonts.MEDIUM.getLineHeight() / 2f;
float lowerButtonY = height * 0.995f - searchY - buttonHeight / 2f;
Image button = GameImage.MENU_BUTTON_MID.getImage();
Image buttonL = GameImage.MENU_BUTTON_LEFT.getImage();
@@ -220,11 +310,9 @@ public class DownloadsMenu extends BasicGameState {
int lrButtonWidth = buttonL.getWidth() + buttonR.getWidth();
Image resetButtonImage = button.getScaledCopy((int) resetWidth - lrButtonWidth, (int) buttonHeight);
Image rankedButtonImage = button.getScaledCopy((int) rankedWidth - lrButtonWidth, (int) buttonHeight);
Image serverButtonImage = button.getScaledCopy((int) serverWidth - lrButtonWidth, (int) buttonHeight);
Image lowerButtonImage = button.getScaledCopy((int) lowerWidth - lrButtonWidth, (int) buttonHeight);
float resetButtonWidth = resetButtonImage.getWidth() + lrButtonWidth;
float rankedButtonWidth = rankedButtonImage.getWidth() + lrButtonWidth;
float serverButtonWidth = serverButtonImage.getWidth() + lrButtonWidth;
float lowerButtonWidth = lowerButtonImage.getWidth() + lrButtonWidth;
clearButton = new MenuButton(lowerButtonImage, buttonL, buttonR,
width * 0.75f + buttonMarginX + lowerButtonWidth / 2f, lowerButtonY);
@@ -234,16 +322,48 @@ public class DownloadsMenu extends BasicGameState {
baseX + searchWidth + buttonMarginX + resetButtonWidth / 2f, topButtonY);
rankedButton = new MenuButton(rankedButtonImage, buttonL, buttonR,
baseX + searchWidth + buttonMarginX * 2f + resetButtonWidth + rankedButtonWidth / 2f, topButtonY);
serverButton = new MenuButton(serverButtonImage, buttonL, buttonR,
baseX + searchWidth + buttonMarginX * 3f + resetButtonWidth + rankedButtonWidth + serverButtonWidth / 2f, topButtonY);
clearButton.setText("Clear", Utils.FONT_MEDIUM, Color.white);
importButton.setText("Import All", Utils.FONT_MEDIUM, Color.white);
resetButton.setText("Reset", Utils.FONT_MEDIUM, Color.white);
clearButton.setText("Clear", Fonts.MEDIUM, Color.white);
importButton.setText("Import All", Fonts.MEDIUM, Color.white);
resetButton.setText("Reset", Fonts.MEDIUM, Color.white);
clearButton.setHoverFade();
importButton.setHoverFade();
resetButton.setHoverFade();
rankedButton.setHoverFade();
serverButton.setHoverFade();
// dropdown menu
int serverWidth = (int) (width * 0.12f);
serverMenu = new DropdownMenu<DownloadServer>(container, SERVERS,
baseX + searchWidth + buttonMarginX * 3f + resetButtonWidth + rankedButtonWidth, searchY, serverWidth) {
@Override
public void itemSelected(int index, DownloadServer item) {
resultList = null;
startResultPos.setPosition(0);
focusResult = -1;
totalResults = 0;
page = 0;
pageResultTotal = 1;
pageDir = Page.RESET;
searchResultString = "Loading data from server...";
lastQuery = null;
pageDir = Page.RESET;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
}
@Override
public boolean menuClicked(int index) {
// block input during beatmap importing
if (importThread != null)
return false;
SoundController.playSound(SoundEffect.MENUCLICK);
return true;
}
};
serverMenu.setBackgroundColor(Colors.BLACK_BG_HOVER);
serverMenu.setBorderColor(Color.black);
serverMenu.setChevronRightColor(Color.white);
}
@Override
@@ -252,18 +372,19 @@ public class DownloadsMenu extends BasicGameState {
int width = container.getWidth();
int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
boolean inDropdownMenu = serverMenu.contains(mouseX, mouseY);
// background
GameImage.SEARCH_BG.getImage().draw();
// title
Utils.FONT_LARGE.drawString(width * 0.024f, height * 0.03f, "Download Beatmaps!", Color.white);
Fonts.LARGE.drawString(width * 0.024f, height * 0.03f, "Download Beatmaps!", Color.white);
// search
g.setColor(Color.white);
g.setLineWidth(2f);
search.render(container, g);
Utils.FONT_BOLD.drawString(
Fonts.BOLD.drawString(
search.getX() + search.getWidth() * 0.01f, search.getY() + search.getHeight() * 1.3f,
searchResultString, Color.white
);
@@ -283,7 +404,7 @@ public class DownloadsMenu extends BasicGameState {
if (index >= nodes.length)
break;
nodes[index].drawResult(g, offset + i * DownloadNode.getButtonOffset(),
DownloadNode.resultContains(mouseX, mouseY - offset, i),
DownloadNode.resultContains(mouseX, mouseY - offset, i) && !inDropdownMenu,
(index == focusResult), (previewID == nodes[index].getID()));
}
g.clearClip();
@@ -297,9 +418,9 @@ public class DownloadsMenu extends BasicGameState {
float baseX = width * 0.024f;
float buttonY = height * 0.2f;
float buttonWidth = width * 0.7f;
Utils.FONT_BOLD.drawString(
baseX + (buttonWidth - Utils.FONT_BOLD.getWidth("Page 1")) / 2f,
buttonY - Utils.FONT_BOLD.getLineHeight() * 1.3f,
Fonts.BOLD.drawString(
baseX + (buttonWidth - Fonts.BOLD.getWidth("Page 1")) / 2f,
buttonY - Fonts.BOLD.getLineHeight() * 1.3f,
String.format("Page %d", page), Color.white
);
if (page > 1)
@@ -311,10 +432,10 @@ public class DownloadsMenu extends BasicGameState {
// downloads
float downloadsX = width * 0.75f, downloadsY = search.getY();
g.setColor(DownloadNode.BG_NORMAL);
g.setColor(Colors.BLACK_BG_NORMAL);
g.fillRect(downloadsX, downloadsY,
width * 0.25f, height - downloadsY * 2f);
Utils.FONT_LARGE.drawString(downloadsX + width * 0.015f, downloadsY + height * 0.015f, "Downloads", Color.white);
Fonts.LARGE.drawString(downloadsX + width * 0.015f, downloadsY + height * 0.015f, "Downloads", Color.white);
int downloadsSize = DownloadList.get().size();
if (downloadsSize > 0) {
int maxDownloadsShown = DownloadNode.maxDownloadsShown();
@@ -344,15 +465,16 @@ public class DownloadsMenu extends BasicGameState {
clearButton.draw(Color.gray);
importButton.draw(Color.orange);
resetButton.draw(Color.red);
rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Utils.FONT_MEDIUM, Color.white);
rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Fonts.MEDIUM, Color.white);
rankedButton.draw(Color.magenta);
serverButton.setText(SERVERS[serverIndex].getName(), Utils.FONT_MEDIUM, Color.white);
serverButton.draw(Color.blue);
// dropdown menu
serverMenu.render(container, g);
// importing beatmaps
if (importThread != null) {
// darken the screen
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
UI.drawLoadingProgress(g);
@@ -379,7 +501,6 @@ public class DownloadsMenu extends BasicGameState {
importButton.hoverUpdate(delta, mouseX, mouseY);
resetButton.hoverUpdate(delta, mouseX, mouseY);
rankedButton.hoverUpdate(delta, mouseX, mouseY);
serverButton.hoverUpdate(delta, mouseX, mouseY);
if (DownloadList.get() != null)
startDownloadIndexPos.setMinMax(0, DownloadNode.getInfoHeight() * (DownloadList.get().size() - DownloadNode.maxDownloadsShown()));
@@ -399,72 +520,22 @@ public class DownloadsMenu extends BasicGameState {
searchTimer = 0;
searchTimerReset = false;
final String query = search.getText().trim().toLowerCase();
final DownloadServer server = SERVERS[serverIndex];
String query = search.getText().trim().toLowerCase();
DownloadServer server = serverMenu.getSelectedItem();
if ((lastQuery == null || !query.equals(lastQuery)) &&
(query.length() == 0 || query.length() >= server.minQueryLength())) {
lastQuery = query;
lastQueryDir = pageDir;
if (queryThread != null && queryThread.isAlive())
if (queryThread != null && queryThread.isAlive()) {
queryThread.interrupt();
if (searchQuery != null)
searchQuery.interrupt();
}
// execute query
queryThread = new Thread() {
@Override
public void run() {
activeRequests++;
// check page direction
Page lastPageDir = pageDir;
pageDir = Page.RESET;
int lastPageSize = (resultList != null) ? resultList.length : 0;
int newPage = page;
if (lastPageDir == Page.RESET)
newPage = 1;
else if (lastPageDir == Page.NEXT)
newPage++;
else if (lastPageDir == Page.PREVIOUS)
newPage--;
try {
DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
if (activeRequests - 1 == 0) {
// update page total
page = newPage;
if (nodes != null) {
if (lastPageDir == Page.NEXT)
pageResultTotal += nodes.length;
else if (lastPageDir == Page.PREVIOUS)
pageResultTotal -= lastPageSize;
else if (lastPageDir == Page.RESET)
pageResultTotal = nodes.length;
} else
pageResultTotal = 0;
resultList = nodes;
totalResults = server.totalResults();
focusResult = -1;
startResultPos.setPosition(0);
if (nodes == null)
searchResultString = "An error has occurred.";
else {
if (query.isEmpty())
searchResultString = "Type to search!";
else if (totalResults == 0 || resultList.length == 0)
searchResultString = "No results found.";
else
searchResultString = String.format("%d result%s found!",
totalResults, (totalResults == 1) ? "" : "s");
}
}
} catch (IOException e) {
searchResultString = "Could not establish connection to server.";
} finally {
activeRequests--;
queryThread = null;
}
}
};
searchQuery = new SearchQuery(query, server);
queryThread = new Thread(searchQuery);
queryThread.start();
}
}
@@ -474,7 +545,7 @@ public class DownloadsMenu extends BasicGameState {
UI.updateTooltip(delta, "Reset the current search.", false);
else if (rankedButton.contains(mouseX, mouseY))
UI.updateTooltip(delta, "Toggle the display of unranked maps.\nSome download servers may not support this option.", true);
else if (serverButton.contains(mouseX, mouseY))
else if (serverMenu.baseContains(mouseX, mouseY))
UI.updateTooltip(delta, "Select a download server.", false);
}
@@ -534,7 +605,7 @@ public class DownloadsMenu extends BasicGameState {
} else {
// play preview
try {
final URL url = new URL(SERVERS[serverIndex].getPreviewURL(node.getID()));
final URL url = new URL(serverMenu.getSelectedItem().getPreviewURL(node.getID()));
MusicController.pause();
new Thread() {
@Override
@@ -578,7 +649,7 @@ public class DownloadsMenu extends BasicGameState {
} else {
// start download
if (!DownloadList.get().contains(node.getID())) {
node.createDownload(SERVERS[serverIndex]);
node.createDownload(serverMenu.getSelectedItem());
if (node.getDownload() == null)
UI.sendBarNotification("The download could not be started.");
else {
@@ -601,23 +672,27 @@ public class DownloadsMenu extends BasicGameState {
// pages
if (nodes.length > 0) {
if (page > 1 && prevPage.contains(x, y)) {
if (lastQueryDir == Page.PREVIOUS && queryThread != null && queryThread.isAlive())
if (lastQueryDir == Page.PREVIOUS && searchQuery != null && !searchQuery.isComplete())
; // don't send consecutive requests
else {
SoundController.playSound(SoundEffect.MENUCLICK);
pageDir = Page.PREVIOUS;
lastQuery = null;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
}
return;
}
if (pageResultTotal < totalResults && nextPage.contains(x, y)) {
if (lastQueryDir == Page.NEXT && queryThread != null && queryThread.isAlive())
if (lastQueryDir == Page.NEXT && searchQuery != null && !searchQuery.isComplete())
; // don't send consecutive requests
else {
SoundController.playSound(SoundEffect.MENUCLICK);
pageDir = Page.NEXT;
lastQuery = null;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
return;
}
@@ -670,6 +745,8 @@ public class DownloadsMenu extends BasicGameState {
search.setText("");
lastQuery = null;
pageDir = Page.RESET;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
return;
}
@@ -678,22 +755,8 @@ public class DownloadsMenu extends BasicGameState {
rankedOnly = !rankedOnly;
lastQuery = null;
pageDir = Page.RESET;
resetSearchTimer();
return;
}
if (serverButton.contains(x, y)) {
SoundController.playSound(SoundEffect.MENUCLICK);
resultList = null;
startResultPos.setPosition(0);
focusResult = -1;
totalResults = 0;
page = 0;
pageResultTotal = 1;
pageDir = Page.RESET;
searchResultString = "Loading data from server...";
serverIndex = (serverIndex + 1) % SERVERS.length;
lastQuery = null;
pageDir = Page.RESET;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
return;
}
@@ -806,6 +869,8 @@ public class DownloadsMenu extends BasicGameState {
SoundController.playSound(SoundEffect.MENUCLICK);
lastQuery = null;
pageDir = Page.CURRENT;
if (searchQuery != null)
searchQuery.interrupt();
resetSearchTimer();
break;
case Input.KEY_F7:
@@ -837,7 +902,8 @@ public class DownloadsMenu extends BasicGameState {
importButton.resetHover();
resetButton.resetHover();
rankedButton.resetHover();
serverButton.resetHover();
serverMenu.activate();
serverMenu.reset();
focusResult = -1;
startResultPos.setPosition(0);
startDownloadIndexPos.setPosition(0);
@@ -853,6 +919,7 @@ public class DownloadsMenu extends BasicGameState {
public void leave(GameContainer container, StateBasedGame game)
throws SlickException {
search.setFocus(false);
serverMenu.deactivate();
SoundController.stopTrack();
MusicController.resume();
}

View File

@@ -42,12 +42,16 @@ import itdelatrisu.opsu.objects.GameObject;
import itdelatrisu.opsu.objects.Slider;
import itdelatrisu.opsu.objects.Spinner;
import itdelatrisu.opsu.objects.curves.Curve;
import itdelatrisu.opsu.objects.curves.Vec2f;
import itdelatrisu.opsu.render.FrameBufferCache;
import itdelatrisu.opsu.replay.PlaybackSpeed;
import itdelatrisu.opsu.replay.Replay;
import itdelatrisu.opsu.replay.ReplayFrame;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
import java.util.LinkedList;
@@ -116,6 +120,15 @@ public class Game extends BasicGameState {
/** Hit object approach time, in milliseconds. */
private int approachTime;
/** The amount of time for hit objects to fade in, in milliseconds. */
private int fadeInTime;
/** Decay time for hit objects in the "Hidden" mod, in milliseconds. */
private int hiddenDecayTime;
/** Time before the hit object time by which the objects have completely faded in the "Hidden" mod, in milliseconds. */
private int hiddenTimeDiff;
/** Time offsets for obtaining each hit result (indexed by HIT_* constants). */
private int[] hitResultOffset;
@@ -146,7 +159,7 @@ public class Game extends BasicGameState {
countdown2Sound, countdownGoSound;
/** Mouse coordinates before game paused. */
private int pausedMouseX = -1, pausedMouseY = -1;
private Vec2f pausedMousePosition;
/** Track position when game paused. */
private int pauseTime = -1;
@@ -209,7 +222,7 @@ public class Game extends BasicGameState {
private int flashlightRadius;
/** The cursor coordinates using the "auto" or "relax" mods. */
private int autoMouseX = 0, autoMouseY = 0;
private Vec2f autoMousePosition;
/** Whether or not the cursor should be pressed using the "auto" mod. */
private boolean autoMousePressed;
@@ -220,11 +233,20 @@ public class Game extends BasicGameState {
/** Whether the game is currently seeking to a replay position. */
private boolean isSeeking;
/** Music position bar coordinates and dimensions (for replay seeking). */
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
/** Music position bar background colors. */
private static final Color
MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f),
MUSICBAR_HOVER = new Color(12, 9, 10, 0.35f),
MUSICBAR_FILL = new Color(255, 255, 255, 0.75f);
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
public Game(int state) {
this.state = state;
@@ -245,6 +267,12 @@ public class Game extends BasicGameState {
gOffscreen = offscreen.getGraphics();
gOffscreen.setBackground(Color.black);
// initialize music position bar location
musicBarX = width * 0.01f;
musicBarY = height * 0.05f;
musicBarWidth = Math.max(width * 0.005f, 7);
musicBarHeight = height * 0.9f;
// create the associated GameData object
data = new GameData(width, height);
}
@@ -278,7 +306,7 @@ public class Game extends BasicGameState {
else
dimLevel = 1f;
}
if (Options.isDefaultPlayfieldForced() || !beatmap.drawBG(width, height, dimLevel, false)) {
if (Options.isDefaultPlayfieldForced() || !beatmap.drawBackground(width, height, dimLevel, false)) {
Image playfield = GameImage.PLAYFIELD.getImage();
playfield.setAlpha(dimLevel);
playfield.draw();
@@ -290,32 +318,31 @@ public class Game extends BasicGameState {
// "auto" and "autopilot" mods: move cursor automatically
// TODO: this should really be in update(), not render()
autoMouseX = width / 2;
autoMouseY = height / 2;
autoMousePosition.set(width / 2, height / 2);
autoMousePressed = false;
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
float[] autoXY = null;
Vec2f autoPoint = null;
if (isLeadIn()) {
// lead-in
float progress = Math.max((float) (leadInTime - beatmap.audioLeadIn) / approachTime, 0f);
autoMouseY = (int) (height / (2f - progress));
autoMousePosition.y = height / (2f - progress);
} else if (objectIndex == 0 && trackPosition < firstObjectTime) {
// before first object
timeDiff = firstObjectTime - trackPosition;
if (timeDiff < approachTime) {
float[] xy = gameObjects[0].getPointAt(trackPosition);
autoXY = getPointAt(autoMouseX, autoMouseY, xy[0], xy[1], 1f - ((float) timeDiff / approachTime));
Vec2f point = gameObjects[0].getPointAt(trackPosition);
autoPoint = getPointAt(autoMousePosition.x, autoMousePosition.y, point.x, point.y, 1f - ((float) timeDiff / approachTime));
}
} else if (objectIndex < beatmap.objects.length) {
// normal object
int objectTime = beatmap.objects[objectIndex].getTime();
if (trackPosition < objectTime) {
float[] xyStart = gameObjects[objectIndex - 1].getPointAt(trackPosition);
Vec2f startPoint = gameObjects[objectIndex - 1].getPointAt(trackPosition);
int startTime = gameObjects[objectIndex - 1].getEndTime();
if (beatmap.breaks != null && breakIndex < beatmap.breaks.size()) {
// starting a break: keep cursor at previous hit object position
if (breakTime > 0 || objectTime > beatmap.breaks.get(breakIndex))
autoXY = xyStart;
autoPoint = startPoint;
// after a break ends: move startTime to break end time
else if (breakIndex > 1) {
@@ -324,10 +351,10 @@ public class Game extends BasicGameState {
startTime = lastBreakEndTime;
}
}
if (autoXY == null) {
float[] xyEnd = gameObjects[objectIndex].getPointAt(trackPosition);
if (autoPoint == null) {
Vec2f endPoint = gameObjects[objectIndex].getPointAt(trackPosition);
int totalTime = objectTime - startTime;
autoXY = getPointAt(xyStart[0], xyStart[1], xyEnd[0], xyEnd[1], (float) (trackPosition - startTime) / totalTime);
autoPoint = getPointAt(startPoint.x, startPoint.y, endPoint.x, endPoint.y, (float) (trackPosition - startTime) / totalTime);
// hit circles: show a mouse press
int offset300 = hitResultOffset[GameData.HIT_300];
@@ -336,19 +363,17 @@ public class Game extends BasicGameState {
autoMousePressed = true;
}
} else {
autoXY = gameObjects[objectIndex].getPointAt(trackPosition);
autoPoint = gameObjects[objectIndex].getPointAt(trackPosition);
autoMousePressed = true;
}
} else {
// last object
autoXY = gameObjects[objectIndex - 1].getPointAt(trackPosition);
autoPoint = gameObjects[objectIndex - 1].getPointAt(trackPosition);
}
// set mouse coordinates
if (autoXY != null) {
autoMouseX = (int) autoXY[0];
autoMouseY = (int) autoXY[1];
}
if (autoPoint != null)
autoMousePosition.set(autoPoint.x, autoPoint.y);
}
// "flashlight" mod: restricted view of hit objects around cursor
@@ -366,12 +391,12 @@ public class Game extends BasicGameState {
g.setDrawMode(Graphics.MODE_ALPHA_MAP);
g.clearAlphaMap();
int mouseX, mouseY;
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
mouseX = pausedMouseX;
mouseY = pausedMouseY;
if (pauseTime > -1 && pausedMousePosition != null) {
mouseX = (int) pausedMousePosition.x;
mouseY = (int) pausedMousePosition.y;
} else if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
mouseX = autoMouseX;
mouseY = autoMouseY;
mouseX = (int) autoMousePosition.x;
mouseY = (int) autoMousePosition.y;
} else if (isReplay) {
mouseX = replayX;
mouseY = replayY;
@@ -455,15 +480,15 @@ public class Game extends BasicGameState {
GameImage.SCOREBAR_BG.getImage().getHeight(),
GameImage.SCOREBAR_KI.getImage().getHeight()
);
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
float oldAlpha = Colors.WHITE_FADE.a;
if (timeDiff < -500)
Utils.COLOR_WHITE_FADE.a = (1000 + timeDiff) / 500f;
Utils.FONT_MEDIUM.drawString(
Colors.WHITE_FADE.a = (1000 + timeDiff) / 500f;
Fonts.MEDIUM.drawString(
2 + (width / 100), retryHeight,
String.format("%d retries and counting...", retries),
Utils.COLOR_WHITE_FADE
Colors.WHITE_FADE
);
Utils.COLOR_WHITE_FADE.a = oldAlpha;
Colors.WHITE_FADE.a = oldAlpha;
}
if (isLeadIn())
@@ -525,28 +550,40 @@ public class Game extends BasicGameState {
if (isReplay || GameMod.AUTO.isActive())
playbackSpeed.getButton().draw();
// draw music position bar (for replay seeking)
if (isReplay && Options.isReplaySeekingEnabled()) {
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
g.setColor((musicPositionBarContains(mouseX, mouseY)) ? MUSICBAR_HOVER : MUSICBAR_NORMAL);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
if (!isLeadIn()) {
g.setColor(MUSICBAR_FILL);
float musicBarPosition = Math.min((float) trackPosition / beatmap.endTime, 1f);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight * musicBarPosition, 4);
}
}
// returning from pause screen
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
if (pauseTime > -1 && pausedMousePosition != null) {
// darken the screen
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
// draw glowing hit select circle and pulse effect
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth();
Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleRadius, circleRadius);
int circleDiameter = GameImage.HITCIRCLE.getImage().getWidth();
Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleDiameter, circleDiameter);
cursorCircle.setAlpha(1.0f);
cursorCircle.drawCentered(pausedMouseX, pausedMouseY);
cursorCircle.drawCentered(pausedMousePosition.x, pausedMousePosition.y);
Image cursorCirclePulse = cursorCircle.getScaledCopy(1f + pausePulse);
cursorCirclePulse.setAlpha(1f - pausePulse);
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
cursorCirclePulse.drawCentered(pausedMousePosition.x, pausedMousePosition.y);
}
if (isReplay)
UI.draw(g, replayX, replayY, replayKeyPressed);
else if (GameMod.AUTO.isActive())
UI.draw(g, autoMouseX, autoMouseY, autoMousePressed);
UI.draw(g, (int) autoMousePosition.x, (int) autoMousePosition.y, autoMousePressed);
else if (GameMod.AUTOPILOT.isActive())
UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed());
UI.draw(g, (int) autoMousePosition.x, (int) autoMousePosition.y, Utils.isGameKeyPressed());
else
UI.draw(g);
}
@@ -564,8 +601,7 @@ public class Game extends BasicGameState {
// returning from pause screen: must click previous mouse position
if (pauseTime > -1) {
// paused during lead-in or break, or "relax" or "autopilot": continue immediately
if ((pausedMouseX < 0 && pausedMouseY < 0) ||
(GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
if (pausedMousePosition == null || (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
pauseTime = -1;
if (!isLeadIn())
MusicController.resume();
@@ -603,6 +639,17 @@ public class Game extends BasicGameState {
return;
}
// "Easy" mod: multiple "lives"
if (GameMod.EASY.isActive() && deathTime > -1) {
if (data.getHealth() < 99f) {
data.changeHealth(delta / 10f);
data.updateDisplays(delta);
return;
}
MusicController.resume();
deathTime = -1;
}
// normal game update
if (!isReplay)
addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
@@ -613,7 +660,7 @@ public class Game extends BasicGameState {
if (replayIndex >= replay.frames.length)
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
//TODO probably should to disable sounds then reseek to the new position
// seeking to a position earlier than original track position
if (isSeeking && replayIndex - 1 >= 1 && replayIndex < replay.frames.length &&
trackPosition < replay.frames[replayIndex - 1].getTime()) {
replayIndex = 0;
@@ -633,7 +680,6 @@ public class Game extends BasicGameState {
timingPointIndex++;
}
}
isSeeking = false;
}
// update and run replay frames
@@ -648,6 +694,12 @@ public class Game extends BasicGameState {
}
mouseX = replayX;
mouseY = replayY;
// unmute sounds
if (isSeeking) {
isSeeking = false;
SoundController.mute(false);
}
}
data.updateDisplays(delta);
@@ -662,16 +714,6 @@ public class Game extends BasicGameState {
* @param keys the keys that are pressed
*/
private void updateGame(int mouseX, int mouseY, int delta, int trackPosition, int keys) {
// "Easy" mod: multiple "lives"
if (GameMod.EASY.isActive() && deathTime > -1) {
if (data.getHealth() < 99f)
data.changeHealth(delta / 10f);
else {
MusicController.resume();
deathTime = -1;
}
}
// map complete!
if (objectIndex >= gameObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
// track ended before last object was processed: force a hit result
@@ -699,6 +741,7 @@ public class Game extends BasicGameState {
r.save();
}
ScoreData score = data.getScoreData(beatmap);
data.setGameplay(!isReplay);
// add score to database
if (!unranked && !isReplay)
@@ -745,8 +788,7 @@ public class Game extends BasicGameState {
// pause game if focus lost
if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
if (pauseTime < 0) {
pausedMouseX = mouseX;
pausedMouseY = mouseY;
pausedMousePosition = new Vec2f(mouseX, mouseY);
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
@@ -819,8 +861,7 @@ public class Game extends BasicGameState {
// pause game
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
pausedMouseX = mouseX;
pausedMouseY = mouseY;
pausedMousePosition = new Vec2f(mouseX, mouseY);
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
@@ -888,6 +929,13 @@ public class Game extends BasicGameState {
}
}
break;
case Input.KEY_F:
// change playback speed
if (isReplay || GameMod.AUTO.isActive()) {
playbackSpeed = playbackSpeed.next();
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
}
break;
case Input.KEY_UP:
UI.changeVolume(1);
break;
@@ -923,9 +971,10 @@ public class Game extends BasicGameState {
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
}
// TODO
else if (!GameMod.AUTO.isActive() && y < 50) {
float pos = (float) x / container.getWidth() * beatmap.endTime;
// replay seeking
else if (Options.isReplaySeekingEnabled() && !GameMod.AUTO.isActive() && musicPositionBarContains(x, y)) {
SoundController.mute(true); // mute sounds while seeking
float pos = (y - musicBarY) / musicBarHeight * beatmap.endTime;
MusicController.setPosition((int) pos);
isSeeking = true;
}
@@ -939,8 +988,7 @@ public class Game extends BasicGameState {
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
int trackPosition = MusicController.getPosition();
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
pausedMouseX = x;
pausedMouseY = y;
pausedMousePosition = new Vec2f(x, y);
pausePulse = 0f;
}
if (MusicController.isPlaying() || isLeadIn())
@@ -969,13 +1017,12 @@ public class Game extends BasicGameState {
private void gameKeyPressed(int keys, int x, int y, int trackPosition) {
// returning from pause screen
if (pauseTime > -1) {
double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y);
double distance = Math.hypot(pausedMousePosition.x - x, pausedMousePosition.y - y);
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
if (distance < circleRadius) {
// unpause the game
pauseTime = -1;
pausedMouseX = -1;
pausedMouseY = -1;
pausedMousePosition = null;
if (!isLeadIn())
MusicController.resume();
}
@@ -1065,6 +1112,15 @@ public class Game extends BasicGameState {
// restart the game
if (restart != Restart.FALSE) {
// load mods
if (isReplay) {
previousMods = GameMod.getModState();
GameMod.loadModState(replay.mods);
}
data.setGameplay(true);
// check restart state
if (restart == Restart.NEW) {
// new game
loadImages();
@@ -1149,10 +1205,6 @@ public class Game extends BasicGameState {
// load replay frames
if (isReplay) {
// load mods
previousMods = GameMod.getModState();
GameMod.loadModState(replay.mods);
// load initial data
replayX = container.getWidth() / 2;
replayY = container.getHeight() / 2;
@@ -1188,6 +1240,8 @@ public class Game extends BasicGameState {
MusicController.setPosition(0);
MusicController.setPitch(GameMod.getSpeedMultiplier());
MusicController.pause();
SoundController.mute(false);
}
skipButton.resetHover();
@@ -1242,10 +1296,10 @@ public class Game extends BasicGameState {
final int followPointInterval = container.getHeight() / 14;
int lastObjectEndTime = gameObjects[lastObjectIndex].getEndTime() + 1;
int objectStartTime = beatmap.objects[index].getTime();
float[] startXY = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
float[] endXY = gameObjects[index].getPointAt(objectStartTime);
float xDiff = endXY[0] - startXY[0];
float yDiff = endXY[1] - startXY[1];
Vec2f startPoint = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
Vec2f endPoint = gameObjects[index].getPointAt(objectStartTime);
float xDiff = endPoint.x - startPoint.x;
float yDiff = endPoint.y - startPoint.y;
float dist = (float) Math.hypot(xDiff, yDiff);
int numPoints = (int) ((dist - GameImage.HITCIRCLE.getImage().getWidth()) / followPointInterval);
if (numPoints > 0) {
@@ -1266,8 +1320,8 @@ public class Game extends BasicGameState {
float step = 1f / (numPoints + 1);
float t = step;
for (int i = 0; i < numPoints; i++) {
float x = startXY[0] + xDiff * t;
float y = startXY[1] + yDiff * t;
float x = startPoint.x + xDiff * t;
float y = startPoint.y + yDiff * t;
float nextT = t + step;
if (lastObjectIndex < objectIndex) { // fade the previous trail
if (progress < nextT) {
@@ -1321,8 +1375,7 @@ public class Game extends BasicGameState {
timingPointIndex = 0;
beatLengthBase = beatLength = 1;
pauseTime = -1;
pausedMouseX = -1;
pausedMouseY = -1;
pausedMousePosition = null;
countdownReadySound = false;
countdown3Sound = false;
countdown1Sound = false;
@@ -1333,8 +1386,7 @@ public class Game extends BasicGameState {
deathTime = -1;
replayFrames = null;
lastReplayTime = 0;
autoMouseX = 0;
autoMouseY = 0;
autoMousePosition = new Vec2f();
autoMousePressed = false;
flashlightRadius = container.getHeight() * 2 / 3;
@@ -1376,9 +1428,9 @@ public class Game extends BasicGameState {
// set images
File parent = beatmap.getFile().getParentFile();
for (GameImage img : GameImage.values()) {
if (img.isSkinnable()) {
if (img.isBeatmapSkinnable()) {
img.setDefaultImage();
img.setSkinImage(parent);
img.setBeatmapSkinImage(parent);
}
}
@@ -1390,6 +1442,8 @@ public class Game extends BasicGameState {
Image skip = GameImage.SKIP.getImage();
skipButton = new MenuButton(skip, width - skip.getWidth() / 2f, height - (skip.getHeight() / 2f));
}
skipButton.setHoverAnimationDuration(350);
skipButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
skipButton.setHoverExpand(1.1f, MenuButton.Expand.UP_LEFT);
// load other images...
@@ -1420,14 +1474,15 @@ public class Game extends BasicGameState {
// Stack modifier scales with hit object size
// StackOffset = HitObjectRadius / 10
int diameter = (int) (104 - (circleSize * 8));
//int diameter = (int) (104 - (circleSize * 8));
float diameter = 108.848f - (circleSize * 8.9646f);
HitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER);
// initialize objects
Circle.init(container, circleSize);
Slider.init(container, circleSize, beatmap);
Circle.init(container, diameter);
Slider.init(container, diameter, beatmap);
Spinner.init(container, overallDifficulty);
Curve.init(container.getWidth(), container.getHeight(), circleSize, (Options.isBeatmapSkinIgnored()) ?
Curve.init(container.getWidth(), container.getHeight(), diameter, (Options.isBeatmapSkinIgnored()) ?
Options.getSkin().getSliderBorderColor() : beatmap.getSliderBorderColor());
// approachRate (hit object approach time)
@@ -1438,9 +1493,9 @@ public class Game extends BasicGameState {
// overallDifficulty (hit result time offsets)
hitResultOffset = new int[GameData.HIT_MAX];
hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6));
hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8));
hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10));
hitResultOffset[GameData.HIT_300] = (int) (79.5f - (overallDifficulty * 6));
hitResultOffset[GameData.HIT_100] = (int) (139.5f - (overallDifficulty * 8));
hitResultOffset[GameData.HIT_50] = (int) (199.5f - (overallDifficulty * 10));
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
//final float mult = 0.608f;
//hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6)) * mult);
@@ -1454,6 +1509,14 @@ public class Game extends BasicGameState {
// difficulty multiplier (scoring)
data.calculateDifficultyMultiplier(beatmap.HPDrainRate, beatmap.circleSize, beatmap.overallDifficulty);
// hit object fade-in time (TODO: formula)
fadeInTime = Math.min(375, (int) (approachTime / 2.5f));
// fade times ("Hidden" mod)
// TODO: find the actual formulas for this
hiddenDecayTime = (int) (approachTime / 3.6f);
hiddenTimeDiff = (int) (approachTime / 3.3f);
}
/**
@@ -1477,6 +1540,22 @@ public class Game extends BasicGameState {
*/
public int getApproachTime() { return approachTime; }
/**
* Returns the amount of time for hit objects to fade in, in milliseconds.
*/
public int getFadeInTime() { return fadeInTime; }
/**
* Returns the object decay time in the "Hidden" mod, in milliseconds.
*/
public int getHiddenDecayTime() { return hiddenDecayTime; }
/**
* Returns the time before the hit object time by which the objects have
* completely faded in the "Hidden" mod, in milliseconds.
*/
public int getHiddenTimeDiff() { return hiddenTimeDiff; }
/**
* Returns an array of hit result offset times, in milliseconds (indexed by GameData.HIT_* constants).
*/
@@ -1536,8 +1615,8 @@ public class Game extends BasicGameState {
public synchronized void addReplayFrameAndRun(int x, int y, int keys, int time){
// "auto" and "autopilot" mods: use automatic cursor coordinates
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
x = autoMouseX;
y = autoMouseY;
x = (int) autoMousePosition.x;
y = (int) autoMousePosition.y;
}
ReplayFrame frame = addReplayFrame(x, y, keys, time);
@@ -1611,17 +1690,13 @@ public class Game extends BasicGameState {
* @param endX the ending x coordinate
* @param endY the ending y coordinate
* @param t the t value [0, 1]
* @return the [x,y] coordinates
* @return the position vector
*/
private float[] getPointAt(float startX, float startY, float endX, float endY, float t) {
private Vec2f getPointAt(float startX, float startY, float endX, float endY, float t) {
// "autopilot" mod: move quicker between objects
if (GameMod.AUTOPILOT.isActive())
t = Utils.clamp(t * 2f, 0f, 1f);
float[] xy = new float[2];
xy[0] = startX + (endX - startX) * t;
xy[1] = startY + (endY - startY) * t;
return xy;
return new Vec2f(startX + (endX - startX) * t, startY + (endY - startY) * t);
}
/**
@@ -1715,9 +1790,9 @@ public class Game extends BasicGameState {
// possible special case: if slider end in the stack,
// all next hit objects in stack move right down
if (hitObjectN.isSlider()) {
float[] p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
float[] p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
float distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
Vec2f p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
Vec2f p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
float distance = Utils.distance(p1.x, p1.y, p2.x, p2.y);
// check if hit object part of this stack
if (distance < STACK_LENIENCE * HitObject.getXMultiplier()) {
@@ -1725,7 +1800,7 @@ public class Game extends BasicGameState {
for (int j = n + 1; j <= i; j++) {
HitObject hitObjectJ = beatmap.objects[j];
p1 = gameObjects[j].getPointAt(hitObjectJ.getTime());
distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
distance = Utils.distance(p1.x, p1.y, p2.x, p2.y);
// hit object below slider end
if (distance < STACK_LENIENCE * HitObject.getXMultiplier())
@@ -1753,4 +1828,14 @@ public class Game extends BasicGameState {
gameObjects[i].updatePosition();
}
}
/**
* Returns true if the coordinates are within the music position bar bounds.
* @param cx the x coordinate
* @param cy the y coordinate
*/
private boolean musicPositionBarContains(float cx, float cy) {
return ((cx > musicBarX && cx < musicBarX + musicBarWidth) &&
(cy > musicBarY && cy < musicBarY + musicBarHeight));
}
}

View File

@@ -27,6 +27,7 @@ import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.lwjgl.input.Keyboard;
import org.newdawn.slick.Color;
@@ -61,7 +62,7 @@ public class GamePauseMenu extends BasicGameState {
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
private Game gameState;
public GamePauseMenu(int state) {
@@ -86,10 +87,10 @@ public class GamePauseMenu extends BasicGameState {
// don't draw default background if button skinned and background unskinned
boolean buttonsSkinned =
GameImage.PAUSE_CONTINUE.hasSkinImage() ||
GameImage.PAUSE_RETRY.hasSkinImage() ||
GameImage.PAUSE_BACK.hasSkinImage();
if (!buttonsSkinned || bg.hasSkinImage())
GameImage.PAUSE_CONTINUE.hasBeatmapSkinImage() ||
GameImage.PAUSE_RETRY.hasBeatmapSkinImage() ||
GameImage.PAUSE_BACK.hasBeatmapSkinImage();
if (!buttonsSkinned || bg.hasBeatmapSkinImage())
bg.getImage().draw();
else
g.setBackground(Color.black);
@@ -133,7 +134,7 @@ public class GamePauseMenu extends BasicGameState {
SoundController.playSound(SoundEffect.MENUBACK);
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
if (UI.getCursor().isSkinned())
if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
} else {
@@ -187,7 +188,7 @@ public class GamePauseMenu extends BasicGameState {
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
else
MusicController.resume();
if (UI.getCursor().isSkinned())
if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}
@@ -227,6 +228,14 @@ public class GamePauseMenu extends BasicGameState {
continueButton = new MenuButton(GameImage.PAUSE_CONTINUE.getImage(), width / 2f, height * 0.25f);
retryButton = new MenuButton(GameImage.PAUSE_RETRY.getImage(), width / 2f, height * 0.5f);
backButton = new MenuButton(GameImage.PAUSE_BACK.getImage(), width / 2f, height * 0.75f);
final int buttonAnimationDuration = 300;
continueButton.setHoverAnimationDuration(buttonAnimationDuration);
retryButton.setHoverAnimationDuration(buttonAnimationDuration);
backButton.setHoverAnimationDuration(buttonAnimationDuration);
final AnimationEquation buttonAnimationEquation = AnimationEquation.IN_OUT_BACK;
continueButton.setHoverAnimationEquation(buttonAnimationEquation);
retryButton.setHoverAnimationEquation(buttonAnimationEquation);
backButton.setHoverAnimationEquation(buttonAnimationEquation);
continueButton.setHoverExpand();
retryButton.setHoverExpand();
backButton.setHoverExpand();

View File

@@ -20,6 +20,7 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameData;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
@@ -68,7 +69,7 @@ public class GameRanking extends BasicGameState {
// game-related variables
private GameContainer container;
private StateBasedGame game;
private int state;
private final int state;
private Input input;
public GameRanking(int state) {
@@ -105,7 +106,7 @@ public class GameRanking extends BasicGameState {
Beatmap beatmap = MusicController.getBeatmap();
// background
if (!beatmap.drawBG(width, height, 0.7f, true))
if (!beatmap.drawBackground(width, height, 0.7f, true))
GameImage.PLAYFIELD.getImage().draw(0,0);
// ranking screen elements
@@ -113,7 +114,7 @@ public class GameRanking extends BasicGameState {
// buttons
replayButton.draw();
if (data.isGameplay())
if (data.isGameplay() && !GameMod.AUTO.isActive())
retryButton.draw();
UI.getBackButton().draw();
@@ -175,7 +176,8 @@ public class GameRanking extends BasicGameState {
// replay
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
boolean returnToGame = false;
if (replayButton.contains(x, y)) {
boolean replayButtonPressed = replayButton.contains(x, y);
if (replayButtonPressed && !(data.isGameplay() && GameMod.AUTO.isActive())) {
Replay r = data.getReplay(null, null);
if (r != null) {
try {
@@ -194,7 +196,9 @@ public class GameRanking extends BasicGameState {
}
// retry
else if (data.isGameplay() && retryButton.contains(x, y)) {
else if (data.isGameplay() &&
(!GameMod.AUTO.isActive() && retryButton.contains(x, y)) ||
(GameMod.AUTO.isActive() && replayButtonPressed)) {
gameState.setReplay(null);
gameState.setRestart(Game.Restart.MANUAL);
returnToGame = true;
@@ -221,7 +225,7 @@ public class GameRanking extends BasicGameState {
} else {
SoundController.playSound(SoundEffect.APPLAUSE);
retryButton.resetHover();
replayButton.setY(replayY);
replayButton.setY(!GameMod.AUTO.isActive() ? replayY : retryY);
}
replayButton.resetHover();
}
@@ -239,12 +243,11 @@ public class GameRanking extends BasicGameState {
*/
private void returnToSongMenu() {
SoundController.playSound(SoundEffect.MENUBACK);
if (data.isGameplay()) {
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
songMenu.resetGameDataOnLoad();
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
if (data.isGameplay())
songMenu.resetTrackOnLoad();
}
if (UI.getCursor().isSkinned())
songMenu.resetGameDataOnLoad();
if (UI.getCursor().isBeatmapSkinned())
UI.getCursor().reset();
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
}

View File

@@ -31,9 +31,13 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.downloads.Updater;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.MenuButton.Expand;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.awt.Desktop;
import java.io.IOException;
@@ -61,7 +65,7 @@ import org.newdawn.slick.state.transition.FadeOutTransition;
*/
public class MainMenu extends BasicGameState {
/** Idle time, in milliseconds, before returning the logo to its original position. */
private static final short MOVE_DELAY = 5000;
private static final short LOGO_IDLE_DELAY = 10000;
/** Max alpha level of the menu background. */
private static final float BG_MAX_ALPHA = 0.9f;
@@ -69,12 +73,21 @@ public class MainMenu extends BasicGameState {
/** Logo button that reveals other buttons on click. */
private MenuButton logo;
/** Whether or not the logo has been clicked. */
private boolean logoClicked = false;
/** Logo states. */
private enum LogoState { DEFAULT, OPENING, OPEN, CLOSING }
/** Current logo state. */
private LogoState logoState = LogoState.DEFAULT;
/** Delay timer, in milliseconds, before starting to move the logo back to the center. */
private int logoTimer = 0;
/** Logo horizontal offset for opening and closing actions. */
private AnimatedValue logoOpen, logoClose;
/** Logo button alpha levels. */
private AnimatedValue logoButtonAlpha;
/** Main "Play" and "Exit" buttons. */
private MenuButton playButton, exitButton;
@@ -87,8 +100,8 @@ public class MainMenu extends BasicGameState {
/** Button linking to repository. */
private MenuButton repoButton;
/** Button for installing updates. */
private MenuButton updateButton;
/** Buttons for installing updates. */
private MenuButton updateButton, restartButton;
/** Application start time, for drawing the total running time. */
private long programStartTime;
@@ -97,7 +110,7 @@ public class MainMenu extends BasicGameState {
private Stack<Integer> previous;
/** Background alpha level (for fade-in effect). */
private float bgAlpha = 0f;
private AnimatedValue bgAlpha = new AnimatedValue(1100, 0f, BG_MAX_ALPHA, AnimationEquation.LINEAR);
/** Whether or not a notification was already sent upon entering. */
private boolean enterNotification = false;
@@ -105,16 +118,11 @@ public class MainMenu extends BasicGameState {
/** Music position bar coordinates and dimensions. */
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
/** Music position bar background colors. */
private static final Color
BG_NORMAL = new Color(0, 0, 0, 0.25f),
BG_HOVER = new Color(0, 0, 0, 0.5f);
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
public MainMenu(int state) {
this.state = state;
@@ -145,9 +153,18 @@ public class MainMenu extends BasicGameState {
exitButton = new MenuButton(exitImg,
width * 0.75f - exitOffset, (height / 2) + (exitImg.getHeight() / 2f)
);
logo.setHoverExpand(1.05f);
playButton.setHoverExpand(1.05f);
exitButton.setHoverExpand(1.05f);
final int logoAnimationDuration = 350;
logo.setHoverAnimationDuration(logoAnimationDuration);
playButton.setHoverAnimationDuration(logoAnimationDuration);
exitButton.setHoverAnimationDuration(logoAnimationDuration);
final AnimationEquation logoAnimationEquation = AnimationEquation.IN_OUT_BACK;
logo.setHoverAnimationEquation(logoAnimationEquation);
playButton.setHoverAnimationEquation(logoAnimationEquation);
exitButton.setHoverAnimationEquation(logoAnimationEquation);
final float logoHoverScale = 1.08f;
logo.setHoverExpand(logoHoverScale);
playButton.setHoverExpand(logoHoverScale);
exitButton.setHoverExpand(logoHoverScale);
// initialize music buttons
int musicWidth = GameImage.MUSIC_PLAY.getImage().getWidth();
@@ -170,24 +187,40 @@ public class MainMenu extends BasicGameState {
// initialize downloads button
Image dlImg = GameImage.DOWNLOADS.getImage();
downloadsButton = new MenuButton(dlImg, width - dlImg.getWidth() / 2f, height / 2f);
downloadsButton.setHoverAnimationDuration(350);
downloadsButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
downloadsButton.setHoverExpand(1.03f, Expand.LEFT);
// initialize repository button
float startX = width * 0.997f, startY = height * 0.997f;
if (Desktop.isDesktopSupported()) { // only if a webpage can be opened
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { // only if a webpage can be opened
Image repoImg = GameImage.REPOSITORY.getImage();
repoButton = new MenuButton(repoImg,
startX - repoImg.getWidth(), startY - repoImg.getHeight()
);
repoButton.setHoverAnimationDuration(350);
repoButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
repoButton.setHoverExpand();
startX -= repoImg.getWidth() * 1.75f;
} else
startX -= width * 0.005f;
}
// initialize update button
Image bangImg = GameImage.BANG.getImage();
updateButton = new MenuButton(bangImg, startX - bangImg.getWidth(), startY - bangImg.getHeight());
updateButton.setHoverExpand(1.15f);
// initialize update buttons
float updateX = width / 2f, updateY = height * 17 / 18f;
Image downloadImg = GameImage.DOWNLOAD.getImage();
updateButton = new MenuButton(downloadImg, updateX, updateY);
updateButton.setHoverAnimationDuration(400);
updateButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_QUAD);
updateButton.setHoverExpand(1.1f);
Image updateImg = GameImage.UPDATE.getImage();
restartButton = new MenuButton(updateImg, updateX, updateY);
restartButton.setHoverAnimationDuration(2000);
restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR);
restartButton.setHoverRotate(360);
// logo animations
float centerOffsetX = width / 5f;
logoOpen = new AnimatedValue(400, 0, centerOffsetX, AnimationEquation.OUT_QUAD);
logoClose = new AnimatedValue(2200, centerOffsetX, 0, AnimationEquation.OUT_QUAD);
logoButtonAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);
reset();
}
@@ -201,27 +234,27 @@ public class MainMenu extends BasicGameState {
// draw background
Beatmap beatmap = MusicController.getBeatmap();
if (Options.isDynamicBackgroundEnabled() &&
beatmap != null && beatmap.drawBG(width, height, bgAlpha, true))
beatmap != null && beatmap.drawBackground(width, height, bgAlpha.getValue(), true))
;
else {
Image bg = GameImage.MENU_BG.getImage();
bg.setAlpha(bgAlpha);
bg.setAlpha(bgAlpha.getValue());
bg.draw();
}
// top/bottom horizontal bars
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a = 0.2f;
g.setColor(Utils.COLOR_BLACK_ALPHA);
float oldAlpha = Colors.BLACK_ALPHA.a;
Colors.BLACK_ALPHA.a = 0.2f;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height / 9f);
g.fillRect(0, height * 8 / 9f, width, height / 9f);
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
Colors.BLACK_ALPHA.a = oldAlpha;
// draw downloads button
downloadsButton.draw();
// draw buttons
if (logoTimer > 0) {
if (logoState == LogoState.OPEN || logoState == LogoState.CLOSING) {
playButton.draw();
exitButton.draw();
}
@@ -237,7 +270,7 @@ public class MainMenu extends BasicGameState {
// draw music position bar
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
g.setColor((musicPositionBarContains(mouseX, mouseY)) ? BG_HOVER : BG_NORMAL);
g.setColor((musicPositionBarContains(mouseX, mouseY)) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
g.setColor(Color.white);
if (!MusicController.isTrackLoading() && beatmap != null) {
@@ -251,35 +284,26 @@ public class MainMenu extends BasicGameState {
// draw update button
if (Updater.get().showButton()) {
Color updateColor = null;
switch (Updater.get().getStatus()) {
case UPDATE_AVAILABLE:
updateColor = Color.red;
break;
case UPDATE_DOWNLOADED:
updateColor = Color.green;
break;
case UPDATE_DOWNLOADING:
updateColor = Color.yellow;
break;
default:
updateColor = Color.white;
break;
}
updateButton.draw(updateColor);
Updater.Status status = Updater.get().getStatus();
if (status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING)
updateButton.draw();
else if (status == Updater.Status.UPDATE_DOWNLOADED)
restartButton.draw();
}
// draw text
float marginX = width * 0.015f, topMarginY = height * 0.01f, bottomMarginY = height * 0.015f;
g.setFont(Utils.FONT_MEDIUM);
float lineHeight = Utils.FONT_MEDIUM.getLineHeight() * 0.925f;
g.setFont(Fonts.MEDIUM);
float lineHeight = Fonts.MEDIUM.getLineHeight() * 0.925f;
g.drawString(String.format("Loaded %d songs and %d beatmaps.",
BeatmapSetList.get().getMapSetCount(), BeatmapSetList.get().getMapCount()), marginX, topMarginY);
if (MusicController.isTrackLoading())
g.drawString("Track loading...", marginX, topMarginY + lineHeight);
else if (MusicController.trackExists()) {
if (Options.useUnicodeMetadata()) // load glyphs
Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, beatmap.artistUnicode);
if (Options.useUnicodeMetadata()) { // load glyphs
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.titleUnicode);
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.artistUnicode);
}
g.drawString((MusicController.isPlaying()) ? "Now Playing:" : "Paused:", marginX, topMarginY + lineHeight);
g.drawString(String.format("%s: %s", beatmap.getArtist(), beatmap.getTitle()), marginX + 25, topMarginY + (lineHeight * 2));
}
@@ -305,7 +329,10 @@ public class MainMenu extends BasicGameState {
exitButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
if (repoButton != null)
repoButton.hoverUpdate(delta, mouseX, mouseY);
updateButton.hoverUpdate(delta, mouseX, mouseY);
if (Updater.get().showButton()) {
updateButton.autoHoverUpdate(delta, true);
restartButton.autoHoverUpdate(delta, false);
}
downloadsButton.hoverUpdate(delta, mouseX, mouseY);
// ensure only one button is in hover state at once
boolean noHoverUpdate = musicPositionBarContains(mouseX, mouseY);
@@ -322,46 +349,46 @@ public class MainMenu extends BasicGameState {
MusicController.toggleTrackDimmed(0.33f);
// fade in background
if (bgAlpha < BG_MAX_ALPHA) {
bgAlpha += delta / 1000f;
if (bgAlpha > BG_MAX_ALPHA)
bgAlpha = BG_MAX_ALPHA;
}
Beatmap beatmap = MusicController.getBeatmap();
if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading()))
bgAlpha.update(delta);
// buttons
if (logoClicked) {
if (logoTimer == 0) { // shifting to left
if (logo.getX() > container.getWidth() / 3.3f)
logo.setX(logo.getX() - delta);
else
logoTimer = 1;
} else if (logoTimer >= MOVE_DELAY) // timer over: shift back to center
logoClicked = false;
else { // increment timer
int centerX = container.getWidth() / 2;
float currentLogoButtonAlpha;
switch (logoState) {
case DEFAULT:
break;
case OPENING:
if (logoOpen.update(delta)) // shifting to left
logo.setX(centerX - logoOpen.getValue());
else {
logoState = LogoState.OPEN;
logoTimer = 0;
logoButtonAlpha.setTime(0);
}
break;
case OPEN:
if (logoButtonAlpha.update(delta)) { // fade in buttons
currentLogoButtonAlpha = logoButtonAlpha.getValue();
playButton.getImage().setAlpha(currentLogoButtonAlpha);
exitButton.getImage().setAlpha(currentLogoButtonAlpha);
} else if (logoTimer >= LOGO_IDLE_DELAY) { // timer over: shift back to center
logoState = LogoState.CLOSING;
logoClose.setTime(0);
logoTimer = 0;
} else // increment timer
logoTimer += delta;
if (logoTimer <= 500) {
// fade in buttons
playButton.getImage().setAlpha(logoTimer / 400f);
exitButton.getImage().setAlpha(logoTimer / 400f);
}
}
} else {
// fade out buttons
if (logoTimer > 0) {
float alpha = playButton.getImage().getAlpha();
if (alpha > 0f) {
playButton.getImage().setAlpha(alpha - (delta / 200f));
exitButton.getImage().setAlpha(alpha - (delta / 200f));
} else
logoTimer = 0;
}
// move back to original location
if (logo.getX() < container.getWidth() / 2) {
logo.setX(logo.getX() + (delta / 3f));
if (logo.getX() > container.getWidth() / 2)
logo.setX(container.getWidth() / 2);
break;
case CLOSING:
if (logoButtonAlpha.update(-delta)) { // fade out buttons
currentLogoButtonAlpha = logoButtonAlpha.getValue();
playButton.getImage().setAlpha(currentLogoButtonAlpha);
exitButton.getImage().setAlpha(currentLogoButtonAlpha);
}
if (logoClose.update(delta)) // shifting to right
logo.setX(centerX - logoClose.getValue());
break;
}
// tooltips
@@ -373,8 +400,12 @@ public class MainMenu extends BasicGameState {
UI.updateTooltip(delta, "Next track", false);
else if (musicPrevious.contains(mouseX, mouseY))
UI.updateTooltip(delta, "Previous track", false);
else if (Updater.get().showButton() && updateButton.contains(mouseX, mouseY))
UI.updateTooltip(delta, Updater.get().getStatus().getDescription(), true);
else if (Updater.get().showButton()) {
Updater.Status status = Updater.get().getStatus();
if (((status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING) && updateButton.contains(mouseX, mouseY)) ||
(status == Updater.Status.UPDATE_DOWNLOADED && restartButton.contains(mouseX, mouseY)))
UI.updateTooltip(delta, status.getDescription(), true);
}
}
@Override
@@ -412,8 +443,8 @@ public class MainMenu extends BasicGameState {
musicPrevious.resetHover();
if (repoButton != null && !repoButton.contains(mouseX, mouseY))
repoButton.resetHover();
if (!updateButton.contains(mouseX, mouseY))
updateButton.resetHover();
updateButton.resetHover();
restartButton.resetHover();
if (!downloadsButton.contains(mouseX, mouseY))
downloadsButton.resetHover();
}
@@ -449,71 +480,85 @@ public class MainMenu extends BasicGameState {
MusicController.resume();
UI.sendBarNotification("Play");
}
return;
} else if (musicNext.contains(x, y)) {
nextTrack();
UI.sendBarNotification(">> Next");
return;
} else if (musicPrevious.contains(x, y)) {
if (!previous.isEmpty()) {
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false);
if (Options.isDynamicBackgroundEnabled())
bgAlpha = 0f;
bgAlpha.setTime(0);
} else
MusicController.setPosition(0);
UI.sendBarNotification("<< Previous");
return;
}
// downloads button actions
else if (downloadsButton.contains(x, y)) {
if (downloadsButton.contains(x, y)) {
SoundController.playSound(SoundEffect.MENUHIT);
game.enterState(Opsu.STATE_DOWNLOADSMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
return;
}
// repository button actions
else if (repoButton != null && repoButton.contains(x, y)) {
if (repoButton != null && repoButton.contains(x, y)) {
try {
Desktop.getDesktop().browse(Options.REPOSITORY_URI);
} catch (UnsupportedOperationException e) {
UI.sendBarNotification("The repository web page could not be opened.");
} catch (IOException e) {
ErrorHandler.error("Could not browse to repository URI.", e, false);
}
return;
}
// update button actions
else if (Updater.get().showButton() && updateButton.contains(x, y)) {
switch (Updater.get().getStatus()) {
case UPDATE_AVAILABLE:
if (Updater.get().showButton()) {
Updater.Status status = Updater.get().getStatus();
if (updateButton.contains(x, y) && status == Updater.Status.UPDATE_AVAILABLE) {
SoundController.playSound(SoundEffect.MENUHIT);
Updater.get().startDownload();
break;
case UPDATE_DOWNLOADED:
updateButton.removeHoverEffects();
updateButton.setHoverAnimationDuration(800);
updateButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_QUAD);
updateButton.setHoverFade(0.6f);
return;
} else if (restartButton.contains(x, y) && status == Updater.Status.UPDATE_DOWNLOADED) {
SoundController.playSound(SoundEffect.MENUHIT);
Updater.get().prepareUpdate();
container.setForceExit(false);
container.exit();
break;
default:
break;
return;
}
}
// start moving logo (if clicked)
else if (!logoClicked) {
if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) {
if (logo.contains(x, y, 0.25f)) {
logoClicked = true;
logoState = LogoState.OPENING;
logoOpen.setTime(0);
logoTimer = 0;
playButton.getImage().setAlpha(0f);
exitButton.getImage().setAlpha(0f);
SoundController.playSound(SoundEffect.MENUHIT);
return;
}
}
// other button actions (if visible)
else if (logoClicked) {
else if (logoState == LogoState.OPEN || logoState == LogoState.OPENING) {
if (logo.contains(x, y, 0.25f) || playButton.contains(x, y, 0.25f)) {
SoundController.playSound(SoundEffect.MENUHIT);
enterSongMenu();
} else if (exitButton.contains(x, y, 0.25f))
return;
} else if (exitButton.contains(x, y, 0.25f)) {
container.exit();
return;
}
}
}
@@ -532,8 +577,9 @@ public class MainMenu extends BasicGameState {
break;
case Input.KEY_P:
SoundController.playSound(SoundEffect.MENUHIT);
if (!logoClicked) {
logoClicked = true;
if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) {
logoState = LogoState.OPENING;
logoOpen.setTime(0);
logoTimer = 0;
playButton.getImage().setAlpha(0f);
exitButton.getImage().setAlpha(0f);
@@ -581,8 +627,11 @@ public class MainMenu extends BasicGameState {
public void reset() {
// reset logo
logo.setX(container.getWidth() / 2);
logoClicked = false;
logoOpen.setTime(0);
logoClose.setTime(0);
logoButtonAlpha.setTime(0);
logoTimer = 0;
logoState = LogoState.DEFAULT;
logo.resetHover();
playButton.resetHover();
@@ -594,6 +643,7 @@ public class MainMenu extends BasicGameState {
if (repoButton != null)
repoButton.resetHover();
updateButton.resetHover();
restartButton.resetHover();
downloadsButton.resetHover();
}
@@ -611,7 +661,7 @@ public class MainMenu extends BasicGameState {
previous.add(node.index);
}
if (Options.isDynamicBackgroundEnabled() && !sameAudio && !MusicController.isThemePlaying())
bgAlpha = 0f;
bgAlpha.setTime(0);
}
/**

View File

@@ -26,6 +26,8 @@ import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.UI;
@@ -86,14 +88,18 @@ public class OptionsMenu extends BasicGameState {
GameOption.DISABLE_MOUSE_WHEEL,
GameOption.DISABLE_MOUSE_BUTTONS,
GameOption.CURSOR_SIZE,
GameOption.NEW_CURSOR
GameOption.NEW_CURSOR,
GameOption.DISABLE_CURSOR
}),
CUSTOM ("Custom", new GameOption[] {
GameOption.FIXED_CS,
GameOption.FIXED_HP,
GameOption.FIXED_AR,
GameOption.FIXED_OD,
GameOption.CHECKPOINT
GameOption.CHECKPOINT,
GameOption.REPLAY_SEEKING,
GameOption.DISABLE_UPDATER,
GameOption.ENABLE_WATCH_SERVICE
});
/** Total number of tabs. */
@@ -110,10 +116,10 @@ public class OptionsMenu extends BasicGameState {
private static OptionTab[] values = values();
/** Tab name. */
private String name;
private final String name;
/** Options array. */
public GameOption[] options;
public final GameOption[] options;
/** Associated tab button. */
public MenuButton button;
@@ -163,7 +169,7 @@ public class OptionsMenu extends BasicGameState {
private StateBasedGame game;
private Input input;
private Graphics g;
private int state;
private final int state;
public OptionsMenu(int state) {
this.state = state;
@@ -182,8 +188,8 @@ public class OptionsMenu extends BasicGameState {
// option tabs
Image tabImage = GameImage.MENU_TAB.getImage();
float tabX = width * 0.032f + Utils.FONT_DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
float tabY = Utils.FONT_XLARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
float tabX = width * 0.032f + Fonts.DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
float tabY = Fonts.XLARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
height * 0.015f - (tabImage.getHeight() / 2f);
int tabOffset = Math.min(tabImage.getWidth(), width / OptionTab.SIZE);
for (OptionTab tab : OptionTab.values())
@@ -198,22 +204,19 @@ public class OptionsMenu extends BasicGameState {
@Override
public void render(GameContainer container, StateBasedGame game, Graphics g)
throws SlickException {
g.setBackground(Utils.COLOR_BLACK_ALPHA);
int width = container.getWidth();
int height = container.getHeight();
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
// background
GameImage.OPTIONS_BG.getImage().draw();
// title
float marginX = width * 0.015f, marginY = height * 0.01f;
Utils.FONT_XLARGE.drawString(marginX, marginY, "Options", Color.white);
Utils.FONT_DEFAULT.drawString(marginX, marginY + Utils.FONT_XLARGE.getLineHeight() * 0.92f,
Fonts.XLARGE.drawString(marginX, marginY, "Options", Color.white);
Fonts.DEFAULT.drawString(marginX, marginY + Fonts.XLARGE.getLineHeight() * 0.92f,
"Change the way opsu! behaves", Color.white);
// background
GameImage.OPTIONS_BG.getImage().draw(0, lineY);
// game options
g.setLineWidth(1f);
GameOption hoverOption = (keyEntryLeft) ? GameOption.KEY_LEFT :
@@ -241,6 +244,7 @@ public class OptionsMenu extends BasicGameState {
currentTab.getName(), true, false);
g.setColor(Color.white);
g.setLineWidth(2f);
float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
g.drawLine(0, lineY, width, lineY);
g.resetLineWidth();
@@ -248,15 +252,15 @@ public class OptionsMenu extends BasicGameState {
// key entry state
if (keyEntryLeft || keyEntryRight) {
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
g.setColor(Color.white);
String prompt = (keyEntryLeft) ?
"Please press the new left-click key." :
"Please press the new right-click key.";
Utils.FONT_LARGE.drawString(
(width / 2) - (Utils.FONT_LARGE.getWidth(prompt) / 2),
(height / 2) - Utils.FONT_LARGE.getLineHeight(), prompt
Fonts.LARGE.drawString(
(width / 2) - (Fonts.LARGE.getWidth(prompt) / 2),
(height / 2) - Fonts.LARGE.getLineHeight(), prompt
);
}
@@ -413,14 +417,14 @@ public class OptionsMenu extends BasicGameState {
*/
private void drawOption(GameOption option, int pos, boolean focus) {
int width = container.getWidth();
int textHeight = Utils.FONT_LARGE.getLineHeight();
int textHeight = Fonts.LARGE.getLineHeight();
float y = textY + (pos * offsetY);
Color color = (focus) ? Color.cyan : Color.white;
Utils.FONT_LARGE.drawString(width / 30, y, option.getName(), color);
Utils.FONT_LARGE.drawString(width / 2, y, option.getValueString(), color);
Utils.FONT_SMALL.drawString(width / 30, y + textHeight, option.getDescription(), color);
g.setColor(Utils.COLOR_WHITE_ALPHA);
Fonts.LARGE.drawString(width / 30, y, option.getName(), color);
Fonts.LARGE.drawString(width / 2, y, option.getValueString(), color);
Fonts.SMALL.drawString(width / 30, y + textHeight, option.getDescription(), color);
g.setColor(Colors.WHITE_ALPHA);
g.drawLine(0, y + textHeight, width, y + textHeight);
}
@@ -433,7 +437,7 @@ public class OptionsMenu extends BasicGameState {
if (y < textY || y > textY + (offsetY * maxOptionsScreen))
return null;
int index = (y - textY + Utils.FONT_LARGE.getLineHeight()) / offsetY;
int index = (y - textY + Fonts.LARGE.getLineHeight()) / offsetY;
if (index >= currentTab.options.length)
return null;

View File

@@ -24,7 +24,6 @@ import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.GameMod;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.ScoreData;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MultiClip;
@@ -32,18 +31,32 @@ import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.audio.SoundEffect;
import itdelatrisu.opsu.beatmap.Beatmap;
import itdelatrisu.opsu.beatmap.BeatmapDifficultyCalculator;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSet;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
import itdelatrisu.opsu.beatmap.BeatmapWatchService.BeatmapWatchServiceListener;
import itdelatrisu.opsu.beatmap.LRUCache;
import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.db.BeatmapDB;
import itdelatrisu.opsu.db.ScoreDB;
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
import itdelatrisu.opsu.ui.KinecticScrolling;
import itdelatrisu.opsu.ui.Colors;
import itdelatrisu.opsu.ui.Fonts;
import itdelatrisu.opsu.ui.MenuButton;
import itdelatrisu.opsu.ui.StarStream;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent.Kind;
import java.util.Map;
import java.util.Stack;
@@ -145,8 +158,8 @@ public class SongMenu extends BasicGameState {
/** Button coordinate values. */
private float buttonX, buttonY, buttonOffset, buttonWidth, buttonHeight;
/** Current x offset of song buttons for mouse hover, in pixels. */
private float hoverOffset = 0f;
/** Horizontal offset of song buttons for mouse hover, in pixels. */
private AnimatedValue hoverOffset = new AnimatedValue(250, 0, MAX_HOVER_OFFSET, AnimationEquation.OUT_QUART);
/** Current index of hovered song button. */
private BeatmapSetNode hoverIndex = null;
@@ -209,11 +222,52 @@ public class SongMenu extends BasicGameState {
/** The text length of the last string in the search TextField. */
private int lastSearchTextLength = -1;
/** Whether the song folder changed (notified via the watch service). */
private boolean songFolderChanged = false;
/** The last background image. */
private File lastBackgroundImage;
/** Background alpha level (for fade-in effect). */
private AnimatedValue bgAlpha = new AnimatedValue(800, 0f, 1f, AnimationEquation.OUT_QUAD);
/** Timer for animations when a new song node is selected. */
private AnimatedValue songChangeTimer = new AnimatedValue(900, 0f, 1f, AnimationEquation.LINEAR);
/** Timer for the music icon animation when a new song node is selected. */
private AnimatedValue musicIconBounceTimer = new AnimatedValue(350, 0f, 1f, AnimationEquation.LINEAR);
/**
* Beatmaps whose difficulties were recently computed (if flag is non-null).
* Unless the Boolean flag is null, then upon removal, the beatmap's objects will
* be cleared (to be garbage collected). If the flag is true, also clear the
* beatmap's array fields (timing points, etc.).
*/
@SuppressWarnings("serial")
private LRUCache<Beatmap, Boolean> beatmapsCalculated = new LRUCache<Beatmap, Boolean>(12) {
@Override
public void eldestRemoved(Map.Entry<Beatmap, Boolean> eldest) {
Boolean b = eldest.getValue();
if (b != null) {
Beatmap beatmap = eldest.getKey();
beatmap.objects = null;
if (b) {
beatmap.timingPoints = null;
beatmap.breaks = null;
beatmap.combo = null;
}
}
}
};
/** The star stream. */
private StarStream starStream;
// game-related variables
private GameContainer container;
private StateBasedGame game;
private Input input;
private int state;
private final int state;
public SongMenu(int state) {
this.state = state;
@@ -231,8 +285,8 @@ public class SongMenu extends BasicGameState {
// header/footer coordinates
headerY = height * 0.0075f + GameImage.MENU_MUSICNOTE.getImage().getHeight() +
Utils.FONT_BOLD.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
Utils.FONT_SMALL.getLineHeight();
Fonts.BOLD.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
Fonts.SMALL.getLineHeight();
footerY = height - GameImage.SELECTION_MODS.getImage().getHeight();
// initialize sorts
@@ -253,11 +307,11 @@ public class SongMenu extends BasicGameState {
buttonOffset = (footerY - headerY - DIVIDER_LINE_WIDTH) / MAX_SONG_BUTTONS;
// search
int textFieldX = (int) (width * 0.7125f + Utils.FONT_BOLD.getWidth("Search: "));
int textFieldY = (int) (headerY + Utils.FONT_BOLD.getLineHeight() / 2);
int textFieldX = (int) (width * 0.7125f + Fonts.BOLD.getWidth("Search: "));
int textFieldY = (int) (headerY + Fonts.BOLD.getLineHeight() / 2);
search = new TextField(
container, Utils.FONT_BOLD, textFieldX, textFieldY,
(int) (width * 0.99f) - textFieldX, Utils.FONT_BOLD.getLineHeight()
container, Fonts.BOLD, textFieldX, textFieldY,
(int) (width * 0.99f) - textFieldX, Fonts.BOLD.getLineHeight()
);
search.setBackgroundColor(Color.transparent);
search.setBorderColor(Color.transparent);
@@ -287,6 +341,22 @@ public class SongMenu extends BasicGameState {
int loaderDim = GameImage.MENU_MUSICNOTE.getImage().getWidth();
SpriteSheet spr = new SpriteSheet(GameImage.MENU_LOADER.getImage(), loaderDim, loaderDim);
loader = new Animation(spr, 50);
// beatmap watch service listener
final StateBasedGame game_ = game;
BeatmapWatchService.addListener(new BeatmapWatchServiceListener() {
@Override
public void eventReceived(Kind<?> kind, Path child) {
if (!songFolderChanged && kind != StandardWatchEventKinds.ENTRY_MODIFY) {
songFolderChanged = true;
if (game_.getCurrentStateID() == Opsu.STATE_SONGMENU)
UI.sendBarNotification("Changes in Songs folder detected. Hit F5 to refresh.");
}
}
});
// star stream
starStream = new StarStream(width, height);
}
@Override
@@ -300,23 +370,30 @@ public class SongMenu extends BasicGameState {
// background
if (focusNode != null) {
Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
if (!focusNodeBeatmap.drawBG(width, height, 1.0f, true))
Beatmap focusNodeBeatmap = focusNode.getSelectedBeatmap();
if (!focusNodeBeatmap.drawBackground(width, height, bgAlpha.getValue(), true))
GameImage.PLAYFIELD.getImage().draw();
}
// star stream
starStream.draw();
// song buttons
BeatmapSetNode node = startNode;
int starNodeOffsetoffset = 0;
if (node.prev != null) {
starNodeOffsetoffset = -1;
node = node.prev;
}
g.setClip(0, (int) (headerY + DIVIDER_LINE_WIDTH / 2), width, (int) (footerY - headerY));
for (int i = startNodeOffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) {
for (int i = startNodeOffset + starNodeOffsetoffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) {
// draw the node
float offset = (node == hoverIndex) ? hoverOffset : 0f;
float offset = (node == hoverIndex) ? hoverOffset.getValue() : 0f;
float ypos = buttonY + (i*buttonOffset) ;
float mid = height/2 - ypos - buttonOffset/2;
final float circleRadi = 1000 * GameImage.getUIscale();
//finds points along a very large circle
// x^2 = h^2 - y^2
float t = circleRadi*circleRadi - (mid*mid);
final float circleRadi = 700 * GameImage.getUIscale();
//finds points along a very large circle (x^2 = h^2 - y^2)
float t = circleRadi * circleRadi - (mid * mid);
float xpos = (float)(t>0?Math.sqrt(t):0) - circleRadi + 50 * GameImage.getUIscale();
ScoreData[] scores = getScoreDataForNode(node, false);
node.draw(buttonX - offset - xpos, ypos,
@@ -336,7 +413,7 @@ public class SongMenu extends BasicGameState {
MAX_SONG_BUTTONS * buttonOffset,
width, headerY + DIVIDER_LINE_WIDTH / 2,
0, MAX_SONG_BUTTONS * buttonOffset,
Utils.COLOR_BLACK_ALPHA, Color.white, true);
Colors.BLACK_ALPHA, Color.white, true);
}
}
@@ -345,14 +422,19 @@ public class SongMenu extends BasicGameState {
ScoreData.clipToDownloadArea(g);
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
for (int i = 0; i < MAX_SCORE_BUTTONS + 1; i++) {
int rank = startScore + i;
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS + 1);
float timerScale = 1f - (1 / 3f) * ((MAX_SCORE_BUTTONS - scoreButtons) / (float) (MAX_SCORE_BUTTONS - 1));
int duration = (int) (songChangeTimer.getDuration() * timerScale);
int segmentDuration = (int) ((2 / 3f) * songChangeTimer.getDuration());
int time = songChangeTimer.getTime();
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
if (rank < 0)
continue;
if (rank >= focusScores.length)
break;
long prevScore = (rank + 1 < focusScores.length) ? focusScores[rank + 1].score : -1;
focusScores[rank].draw(g, offset + i*ScoreData.getButtonOffset(), rank, prevScore, ScoreData.buttonContains(mouseX, mouseY-offset, i));
float t = Utils.clamp((time - (i * (duration - segmentDuration) / scoreButtons)) / (float) segmentDuration, 0f, 1f);
boolean focus = (t >= 0.9999f && ScoreData.buttonContains(mouseX, mouseY - offset, i));
focusScores[rank].draw(g, offset + i*ScoreData.getButtonOffset(), rank, prevScore, focus, t);
}
g.clearClip();
@@ -360,13 +442,12 @@ public class SongMenu extends BasicGameState {
if (focusScores.length > MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY))
ScoreData.drawScrollbar(g, startScorePos.getPosition() , focusScores.length * ScoreData.getButtonOffset());
}
// top/bottom bars
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, headerY);
g.fillRect(0, footerY, width, height - footerY);
g.setColor(Utils.COLOR_BLUE_DIVIDER);
g.setColor(Colors.BLUE_DIVIDER);
g.setLineWidth(DIVIDER_LINE_WIDTH);
g.drawLine(0, headerY, width, headerY);
g.drawLine(0, footerY, width, footerY);
@@ -379,8 +460,13 @@ public class SongMenu extends BasicGameState {
Image musicNote = GameImage.MENU_MUSICNOTE.getImage();
if (MusicController.isTrackLoading())
loader.draw(marginX, marginY);
else
musicNote.draw(marginX, marginY);
else {
float t = musicIconBounceTimer.getValue() * 2f;
if (t > 1)
t = 2f - t;
float musicNoteScale = 1f + 0.3f * t;
musicNote.getScaledCopy(musicNoteScale).drawCentered(marginX + musicNote.getWidth() / 2f, marginY + musicNote.getHeight() / 2f);
}
int iconWidth = musicNote.getWidth();
// song info text
@@ -388,26 +474,49 @@ public class SongMenu extends BasicGameState {
songInfo = focusNode.getInfo();
if (Options.useUnicodeMetadata()) { // load glyphs
Beatmap beatmap = focusNode.getBeatmapSet().get(0);
Utils.loadGlyphs(Utils.FONT_LARGE, beatmap.titleUnicode, beatmap.artistUnicode);
Fonts.loadGlyphs(Fonts.LARGE, beatmap.titleUnicode);
Fonts.loadGlyphs(Fonts.LARGE, beatmap.artistUnicode);
}
}
marginX += 5;
Color c = Colors.WHITE_FADE;
float oldAlpha = c.a;
float t = AnimationEquation.OUT_QUAD.calc(songChangeTimer.getValue());
float headerTextY = marginY * 0.2f;
Utils.FONT_LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], Color.white);
headerTextY += Utils.FONT_LARGE.getLineHeight() - 6;
Utils.FONT_DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], Color.white);
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 2;
float speedModifier = GameMod.getSpeedMultiplier();
Color color2 = (speedModifier == 1f) ? Color.white :
(speedModifier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
Utils.FONT_BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
headerTextY += Utils.FONT_BOLD.getLineHeight() - 4;
Utils.FONT_DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white);
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 4;
float multiplier = GameMod.getDifficultyMultiplier();
Color color4 = (multiplier == 1f) ? Color.white :
(multiplier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
Utils.FONT_SMALL.drawString(marginX, headerTextY, songInfo[4], color4);
c.a = Math.min(t * songInfo.length / 1.5f, 1f);
if (c.a > 0)
Fonts.LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], c);
headerTextY += Fonts.LARGE.getLineHeight() - 6;
c.a = Math.min((t - 1f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
if (c.a > 0)
Fonts.DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], c);
headerTextY += Fonts.DEFAULT.getLineHeight() - 2;
c.a = Math.min((t - 2f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
if (c.a > 0) {
float speedModifier = GameMod.getSpeedMultiplier();
Color color2 = (speedModifier == 1f) ? c :
(speedModifier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT;
float oldAlpha2 = color2.a;
color2.a = c.a;
Fonts.BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
color2.a = oldAlpha2;
}
headerTextY += Fonts.BOLD.getLineHeight() - 4;
c.a = Math.min((t - 3f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
if (c.a > 0)
Fonts.DEFAULT.drawString(marginX, headerTextY, songInfo[3], c);
headerTextY += Fonts.DEFAULT.getLineHeight() - 4;
c.a = Math.min((t - 4f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
if (c.a > 0) {
float multiplier = GameMod.getDifficultyMultiplier();
Color color4 = (multiplier == 1f) ? c :
(multiplier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT;
float oldAlpha4 = color4.a;
color4.a = c.a;
Fonts.SMALL.drawString(marginX, headerTextY, songInfo[4], color4);
color4.a = oldAlpha4;
}
c.a = oldAlpha;
}
// selection buttons
@@ -440,38 +549,41 @@ public class SongMenu extends BasicGameState {
int searchX = search.getX(), searchY = search.getY();
float searchBaseX = width * 0.7f;
float searchTextX = width * 0.7125f;
float searchRectHeight = Utils.FONT_BOLD.getLineHeight() * 2;
float searchExtraHeight = Utils.FONT_DEFAULT.getLineHeight() * 0.7f;
float searchRectHeight = Fonts.BOLD.getLineHeight() * 2;
float searchExtraHeight = Fonts.DEFAULT.getLineHeight() * 0.7f;
float searchProgress = (searchTransitionTimer < SEARCH_TRANSITION_TIME) ?
((float) searchTransitionTimer / SEARCH_TRANSITION_TIME) : 1f;
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
float oldAlpha = Colors.BLACK_ALPHA.a;
if (searchEmpty) {
searchRectHeight += (1f - searchProgress) * searchExtraHeight;
Utils.COLOR_BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f;
Colors.BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f;
} else {
searchRectHeight += searchProgress * searchExtraHeight;
Utils.COLOR_BLACK_ALPHA.a = 0.2f + searchProgress * 0.3f;
Colors.BLACK_ALPHA.a = 0.2f + searchProgress * 0.3f;
}
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(searchBaseX, headerY + DIVIDER_LINE_WIDTH / 2, width - searchBaseX, searchRectHeight);
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
Utils.FONT_BOLD.drawString(searchTextX, searchY, "Search:", Utils.COLOR_GREEN_SEARCH);
Colors.BLACK_ALPHA.a = oldAlpha;
Fonts.BOLD.drawString(searchTextX, searchY, "Search:", Colors.GREEN_SEARCH);
if (searchEmpty)
Utils.FONT_BOLD.drawString(searchX, searchY, "Type to search!", Color.white);
Fonts.BOLD.drawString(searchX, searchY, "Type to search!", Color.white);
else {
g.setColor(Color.white);
// TODO: why is this needed to correctly position the TextField?
search.setLocation(searchX - 3, searchY - 1);
search.render(container, g);
search.setLocation(searchX, searchY);
Utils.FONT_DEFAULT.drawString(searchTextX, searchY + Utils.FONT_BOLD.getLineHeight(),
Fonts.DEFAULT.drawString(searchTextX, searchY + Fonts.BOLD.getLineHeight(),
(searchResultString == null) ? "Searching..." : searchResultString, Color.white);
}
// reloading beatmaps
if (reloadThread != null) {
// darken the screen
g.setColor(Utils.COLOR_BLACK_ALPHA);
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, 0, width, height);
UI.drawLoadingProgress(g);
@@ -510,6 +622,21 @@ public class SongMenu extends BasicGameState {
}
}
if (focusNode != null) {
// fade in background
Beatmap focusNodeBeatmap = focusNode.getSelectedBeatmap();
if (!focusNodeBeatmap.isBackgroundLoading())
bgAlpha.update(delta);
// song change timers
songChangeTimer.update(delta);
if (!MusicController.isTrackLoading())
musicIconBounceTimer.update(delta);
}
// star stream
starStream.update(delta);
// search
search.setFocus(true);
searchTimer += delta;
@@ -574,14 +701,10 @@ public class SongMenu extends BasicGameState {
if ((mouseX > cx && mouseX < cx + buttonWidth) &&
(mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) {
if (node == hoverIndex) {
if (hoverOffset < MAX_HOVER_OFFSET) {
hoverOffset += delta / 3f;
if (hoverOffset > MAX_HOVER_OFFSET)
hoverOffset = MAX_HOVER_OFFSET;
}
hoverOffset.update(delta);
} else {
hoverIndex = node ;
hoverOffset = 0f;
hoverOffset.setTime(0);
}
isHover = true;
break;
@@ -589,21 +712,19 @@ public class SongMenu extends BasicGameState {
}
}
if (!isHover) {
hoverOffset = 0f;
hoverOffset.setTime(0);
hoverIndex = null;
} else
return;
// tooltips
if (focusScores != null) {
if (focusScores != null && ScoreData.areaContains(mouseX, mouseY)) {
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
for (int i = 0; i < MAX_SCORE_BUTTONS; i++) {
int rank = startScore + i;
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
if (rank < 0)
continue;
if (rank >= focusScores.length)
break;
if (ScoreData.buttonContains(mouseX, mouseY - offset, i)) {
UI.updateTooltip(delta, focusScores[rank].getTooltipString(), true);
break;
@@ -689,7 +810,7 @@ public class SongMenu extends BasicGameState {
float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX;
if ((x > cx && x < cx + buttonWidth) &&
(y > buttonY + (i * buttonOffset) && y < buttonY + (i * buttonOffset) + buttonHeight)) {
float oldHoverOffset = hoverOffset;
int oldHoverOffsetTime = hoverOffset.getTime();
BeatmapSetNode oldHoverIndex = hoverIndex;
// clicked node is already expanded
@@ -714,7 +835,7 @@ public class SongMenu extends BasicGameState {
}
// restore hover data
hoverOffset = oldHoverOffset;
hoverOffset.setTime(oldHoverOffsetTime);
hoverIndex = oldHoverIndex;
// open beatmap menu
@@ -728,12 +849,10 @@ public class SongMenu extends BasicGameState {
// score buttons
if (focusScores != null && ScoreData.areaContains(x, y)) {
for (int i = 0; i < MAX_SCORE_BUTTONS + 1; i++) {
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
int rank = startScore + i;
if (rank >= focusScores.length)
break;
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
if (ScoreData.buttonContains(x, y - offset, i)) {
SoundController.playSound(SoundEffect.MENUHIT);
if (button != Input.MOUSE_RIGHT_BUTTON) {
@@ -805,8 +924,12 @@ public class SongMenu extends BasicGameState {
break;
case Input.KEY_F5:
SoundController.playSound(SoundEffect.MENUHIT);
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD);
game.enterState(Opsu.STATE_BUTTONMENU);
if (songFolderChanged)
reloadBeatmaps(false);
else {
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD);
game.enterState(Opsu.STATE_BUTTONMENU);
}
break;
case Input.KEY_DELETE:
if (focusNode == null)
@@ -851,11 +974,11 @@ public class SongMenu extends BasicGameState {
if (next != null) {
SoundController.playSound(SoundEffect.MENUCLICK);
BeatmapSetNode oldStartNode = startNode;
float oldHoverOffset = hoverOffset;
int oldHoverOffsetTime = hoverOffset.getTime();
BeatmapSetNode oldHoverIndex = hoverIndex;
setFocus(next, 0, false, true);
if (startNode == oldStartNode) {
hoverOffset = oldHoverOffset;
hoverOffset.setTime(oldHoverOffsetTime);
hoverIndex = oldHoverIndex;
}
}
@@ -867,11 +990,11 @@ public class SongMenu extends BasicGameState {
if (prev != null) {
SoundController.playSound(SoundEffect.MENUCLICK);
BeatmapSetNode oldStartNode = startNode;
float oldHoverOffset = hoverOffset;
int oldHoverOffsetTime = hoverOffset.getTime();
BeatmapSetNode oldHoverIndex = hoverIndex;
setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.getBeatmapSet().size() - 1, false, true);
if (startNode == oldStartNode) {
hoverOffset = oldHoverOffset;
hoverOffset.setTime(oldHoverOffsetTime);
hoverIndex = oldHoverIndex;
}
}
@@ -965,18 +1088,26 @@ public class SongMenu extends BasicGameState {
selectRandomButton.resetHover();
selectMapOptionsButton.resetHover();
selectOptionsButton.resetHover();
hoverOffset = 0f;
hoverOffset.setTime(0);
hoverIndex = null;
startScorePos.setPosition(0);
beatmapMenuTimer = -1;
searchTransitionTimer = SEARCH_TRANSITION_TIME;
songInfo = null;
bgAlpha.setTime(bgAlpha.getDuration());
songChangeTimer.setTime(songChangeTimer.getDuration());
musicIconBounceTimer.setTime(musicIconBounceTimer.getDuration());
starStream.clear();
// reset song stack
randomStack = new Stack<SongNode>();
// reload beatmaps if song folder changed
if (songFolderChanged && stateAction != MenuState.RELOAD)
reloadBeatmaps(false);
// set focus node if not set (e.g. theme song playing)
if (focusNode == null && BeatmapSetList.get().size() > 0)
else if (focusNode == null && BeatmapSetList.get().size() > 0)
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
// reset music track
@@ -1003,13 +1134,13 @@ public class SongMenu extends BasicGameState {
// destroy skin images, if any
for (GameImage img : GameImage.values()) {
if (img.isSkinnable())
img.destroySkinImage();
if (img.isBeatmapSkinnable())
img.destroyBeatmapSkinImage();
}
// reload scores
if (focusNode != null) {
scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
scoreMap = ScoreDB.getMapSetScores(focusNode.getSelectedBeatmap());
focusScores = getScoreDataForNode(focusNode, true);
}
@@ -1022,7 +1153,7 @@ public class SongMenu extends BasicGameState {
case BEATMAP: // clear all scores
if (stateActionNode == null || stateActionNode.beatmapIndex == -1)
break;
Beatmap beatmap = stateActionNode.getBeatmapSet().get(stateActionNode.beatmapIndex);
Beatmap beatmap = stateActionNode.getSelectedBeatmap();
ScoreDB.deleteScore(beatmap);
if (stateActionNode == focusNode) {
focusScores = null;
@@ -1033,7 +1164,7 @@ public class SongMenu extends BasicGameState {
if (stateActionScore == null)
break;
ScoreDB.deleteScore(stateActionScore);
scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
scoreMap = ScoreDB.getMapSetScores(focusNode.getSelectedBeatmap());
focusScores = getScoreDataForNode(focusNode, true);
startScorePos.setPosition(0);
break;
@@ -1095,44 +1226,7 @@ public class SongMenu extends BasicGameState {
}
break;
case RELOAD: // reload beatmaps
// reset state and node references
MusicController.reset();
startNode = focusNode = null;
scoreMap = null;
focusScores = null;
oldFocusNode = null;
randomStack = new Stack<SongNode>();
songInfo = null;
hoverOffset = 0f;
hoverIndex = null;
search.setText("");
searchTimer = SEARCH_DELAY;
searchTransitionTimer = SEARCH_TRANSITION_TIME;
searchResultString = null;
// reload songs in new thread
reloadThread = new Thread() {
@Override
public void run() {
// clear the beatmap cache
BeatmapDB.clearDatabase();
// invoke unpacker and parser
File beatmapDir = Options.getBeatmapDir();
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
BeatmapParser.parseAllFiles(beatmapDir);
// initialize song list
if (BeatmapSetList.get().size() > 0) {
BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else
MusicController.playThemeSong();
reloadThread = null;
}
};
reloadThread.start();
reloadBeatmaps(true);
break;
default:
break;
@@ -1210,15 +1304,25 @@ public class SongMenu extends BasicGameState {
if (node == null)
return null;
hoverOffset = 0f;
hoverOffset.setTime(0);
hoverIndex = null;
songInfo = null;
songChangeTimer.setTime(0);
musicIconBounceTimer.setTime(0);
BeatmapSetNode oldFocus = focusNode;
// expand node before focusing it
int expandedIndex = BeatmapSetList.get().getExpandedIndex();
if (node.index != expandedIndex) {
node = BeatmapSetList.get().expand(node.index);
// calculate difficulties
calculateStarRatings(node.getBeatmapSet());
// if start node was previously expanded, move it
if (startNode != null && startNode.index == expandedIndex)
startNode = BeatmapSetList.get().getBaseNode(startNode.index);
}
// check beatmapIndex bounds
@@ -1227,7 +1331,7 @@ public class SongMenu extends BasicGameState {
beatmapIndex = (int) (Math.random() * length);
focusNode = BeatmapSetList.get().getNode(node, beatmapIndex);
Beatmap beatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
Beatmap beatmap = focusNode.getSelectedBeatmap();
MusicController.play(beatmap, false, preview);
// load scores
@@ -1286,6 +1390,14 @@ public class SongMenu extends BasicGameState {
songScrolling.scrollToPosition((focusNode.index + focusNode.getBeatmapSet().size() ) * buttonOffset - (footerY - headerY));
//*/
// load background image
beatmap.loadBackground();
boolean isBgNull = lastBackgroundImage == null || beatmap.bg == null;
if ((isBgNull && lastBackgroundImage != beatmap.bg) || (!isBgNull && !beatmap.bg.equals(lastBackgroundImage))) {
bgAlpha.setTime(0);
lastBackgroundImage = beatmap.bg;
}
return oldFocus;
}
@@ -1346,7 +1458,7 @@ public class SongMenu extends BasicGameState {
if (scoreMap == null || scoreMap.isEmpty() || node.beatmapIndex == -1) // node not expanded
return null;
Beatmap beatmap = node.getBeatmapSet().get(node.beatmapIndex);
Beatmap beatmap = node.getSelectedBeatmap();
ScoreData[] scores = scoreMap.get(beatmap.version);
if (scores == null || scores.length < 1) // no scores
return null;
@@ -1364,6 +1476,86 @@ public class SongMenu extends BasicGameState {
return null; // incorrect map
}
/**
* Reloads all beatmaps.
* @param fullReload if true, also clear the beatmap cache and invoke the unpacker
*/
private void reloadBeatmaps(final boolean fullReload) {
songFolderChanged = false;
// reset state and node references
MusicController.reset();
startNode = focusNode = null;
scoreMap = null;
focusScores = null;
oldFocusNode = null;
randomStack = new Stack<SongNode>();
songInfo = null;
hoverOffset.setTime(0);
hoverIndex = null;
search.setText("");
searchTimer = SEARCH_DELAY;
searchTransitionTimer = SEARCH_TRANSITION_TIME;
searchResultString = null;
lastBackgroundImage = null;
// reload songs in new thread
reloadThread = new Thread() {
@Override
public void run() {
File beatmapDir = Options.getBeatmapDir();
if (fullReload) {
// clear the beatmap cache
BeatmapDB.clearDatabase();
// invoke unpacker
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
}
// invoke parser
BeatmapParser.parseAllFiles(beatmapDir);
// initialize song list
if (BeatmapSetList.get().size() > 0) {
BeatmapSetList.get().init();
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
} else
MusicController.playThemeSong();
reloadThread = null;
}
};
reloadThread.start();
}
/**
* Calculates all star ratings for a beatmap set.
* @param beatmapSet the set of beatmaps
*/
private void calculateStarRatings(BeatmapSet beatmapSet) {
for (Beatmap beatmap : beatmapSet) {
if (beatmap.starRating >= 0) { // already calculated
beatmapsCalculated.put(beatmap, beatmapsCalculated.get(beatmap));
continue;
}
// if timing points are already loaded before this (for whatever reason),
// don't clear the array fields to be safe
boolean hasTimingPoints = (beatmap.timingPoints != null);
BeatmapDifficultyCalculator diffCalc = new BeatmapDifficultyCalculator(beatmap);
diffCalc.calculate();
if (diffCalc.getStarRating() == -1)
continue; // calculations failed
// save star rating
beatmap.starRating = diffCalc.getStarRating();
BeatmapDB.setStars(beatmap);
beatmapsCalculated.put(beatmap, !hasTimingPoints);
}
}
/**
* Starts the game.
*/
@@ -1372,8 +1564,12 @@ public class SongMenu extends BasicGameState {
return;
SoundController.playSound(SoundEffect.MENUHIT);
MultiClip.destroyExtraClips();
Beatmap beatmap = MusicController.getBeatmap();
if (focusNode == null || beatmap != focusNode.getSelectedBeatmap()) {
UI.sendBarNotification("Unable to load the beatmap audio.");
return;
}
MultiClip.destroyExtraClips();
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
gameState.loadBeatmap(beatmap);
gameState.setRestart(Game.Restart.NEW);

View File

@@ -21,21 +21,23 @@ package itdelatrisu.opsu.states;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.MusicController;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.BeatmapSetList;
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.replay.ReplayImporter;
import itdelatrisu.opsu.ui.UI;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.io.File;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.BasicGameState;
@@ -47,6 +49,9 @@ import org.newdawn.slick.state.StateBasedGame;
* Loads game resources and enters "Main Menu" state.
*/
public class Splash extends BasicGameState {
/** Minimum time, in milliseconds, to display the splash screen (and fade in the logo). */
private static final int MIN_SPLASH_TIME = 400;
/** Whether or not loading has completed. */
private boolean finished = false;
@@ -59,8 +64,14 @@ public class Splash extends BasicGameState {
/** Whether the skin being loaded is a new skin (for program restarts). */
private boolean newSkin = false;
/** Whether the watch service is newly enabled (for program restarts). */
private boolean watchServiceChange = false;
/** Logo alpha level. */
private AnimatedValue logoAlpha;
// game-related variables
private int state;
private final int state;
private GameContainer container;
private boolean init = false;
@@ -77,9 +88,14 @@ public class Splash extends BasicGameState {
if (Options.getSkin() != null)
this.newSkin = (Options.getSkin().getDirectory() != Options.getSkinDir());
// check if watch service newly enabled
this.watchServiceChange = Options.isWatchServiceEnabled() && BeatmapWatchService.get() == null;
// load Utils class first (needed in other 'init' methods)
Utils.init(container, game);
// fade in logo
this.logoAlpha = new AnimatedValue(MIN_SPLASH_TIME, 0f, 1f, AnimationEquation.LINEAR);
GameImage.MENU_LOGO.getImage().setAlpha(0f);
}
@@ -99,13 +115,18 @@ public class Splash extends BasicGameState {
// resources already loaded (from application restart)
if (BeatmapSetList.get() != null) {
// reload sounds if skin changed
if (newSkin) {
if (newSkin || watchServiceChange) { // need to reload resources
thread = new Thread() {
@Override
public void run() {
// reload beatmaps if watch service newly enabled
if (watchServiceChange)
BeatmapParser.parseAllFiles(Options.getBeatmapDir());
// reload sounds if skin changed
// TODO: only reload each sound if actually needed?
SoundController.init();
if (newSkin)
SoundController.init();
finished = true;
thread = null;
@@ -144,13 +165,11 @@ public class Splash extends BasicGameState {
}
// fade in logo
Image logo = GameImage.MENU_LOGO.getImage();
float alpha = logo.getAlpha();
if (alpha < 1f)
logo.setAlpha(alpha + (delta / 500f));
if (logoAlpha.update(delta))
GameImage.MENU_LOGO.getImage().setAlpha(logoAlpha.getValue());
// change states when loading complete
if (finished && alpha >= 1f) {
if (finished && logoAlpha.getValue() >= 1f) {
// initialize song list
if (BeatmapSetList.get().size() > 0) {
BeatmapSetList.get().init();

View File

@@ -0,0 +1,51 @@
/*
* 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.ui;
import org.newdawn.slick.Color;
/**
* Colors used for drawing.
*/
public class Colors {
public static final Color
BLACK_ALPHA = new Color(0, 0, 0, 0.5f),
WHITE_ALPHA = new Color(255, 255, 255, 0.5f),
BLUE_DIVIDER = new Color(49, 94, 237),
BLUE_BACKGROUND = new Color(74, 130, 255),
BLUE_BUTTON = new Color(40, 129, 237),
ORANGE_BUTTON = new Color(200, 90, 3),
YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
WHITE_FADE = new Color(255, 255, 255, 1f),
RED_HOVER = new Color(255, 112, 112),
GREEN = new Color(137, 201, 79),
LIGHT_ORANGE = new Color(255, 192, 128),
LIGHT_GREEN = new Color(128, 255, 128),
LIGHT_BLUE = new Color(128, 128, 255),
GREEN_SEARCH = new Color(173, 255, 47),
DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f),
RED_HIGHLIGHT = new Color(246, 154, 161),
BLUE_HIGHLIGHT = new Color(173, 216, 230),
BLACK_BG_NORMAL = new Color(0, 0, 0, 0.25f),
BLACK_BG_HOVER = new Color(0, 0, 0, 0.5f),
BLACK_BG_FOCUS = new Color(0, 0, 0, 0.75f);
// This class should not be instantiated.
private Colors() {}
}

View File

@@ -24,9 +24,10 @@ import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.skins.Skin;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.awt.Point;
import java.nio.IntBuffer;
import java.util.Iterator;
import java.util.LinkedList;
import org.lwjgl.BufferUtils;
@@ -45,13 +46,25 @@ public class Cursor {
private static org.lwjgl.input.Cursor emptyCursor;
/** Last cursor coordinates. */
private int lastX = -1, lastY = -1;
private Point lastPosition;
/** Cursor rotation angle. */
private float cursorAngle = 0f;
/** The time in milliseconds when the cursor was last pressed, used for the scaling animation. */
private long lastCursorPressTime = 0L;
/** Whether or not the cursor was pressed in the last frame, used for the scaling animation. */
private boolean lastCursorPressState = false;
/** The amount the cursor scale increases, if enabled, when pressed. */
private static final float CURSOR_SCALE_CHANGE = 0.25f;
/** The time it takes for the cursor to scale, in milliseconds. */
private static final float CURSOR_SCALE_TIME = 125;
/** Stores all previous cursor locations to display a trail. */
private LinkedList<Integer> cursorX, cursorY;
private LinkedList<Point> trail = new LinkedList<Point>();
// game-related variables
private static GameContainer container;
@@ -81,10 +94,7 @@ public class Cursor {
/**
* Constructor.
*/
public Cursor() {
cursorX = new LinkedList<Integer>();
cursorY = new LinkedList<Integer>();
}
public Cursor() {}
/**
* Draws the cursor.
@@ -105,85 +115,90 @@ public class Cursor {
* @param mousePressed whether or not the mouse button is pressed
*/
public void draw(int mouseX, int mouseY, boolean mousePressed) {
if (Options.isCursorDisabled())
return;
// determine correct cursor image
Image cursor = null, cursorMiddle = null, cursorTrail = null;
boolean skinned = GameImage.CURSOR.hasSkinImage();
boolean beatmapSkinned = GameImage.CURSOR.hasBeatmapSkinImage();
boolean newStyle, hasMiddle;
if (skinned) {
Skin skin = Options.getSkin();
if (beatmapSkinned) {
newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors
hasMiddle = GameImage.CURSOR_MIDDLE.hasSkinImage();
hasMiddle = GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage();
} else
newStyle = hasMiddle = Options.isNewCursorEnabled();
if (skinned || newStyle) {
if (newStyle || beatmapSkinned) {
cursor = GameImage.CURSOR.getImage();
cursorTrail = GameImage.CURSOR_TRAIL.getImage();
} else {
cursor = GameImage.CURSOR_OLD.getImage();
cursorTrail = GameImage.CURSOR_TRAIL_OLD.getImage();
cursor = GameImage.CURSOR.hasGameSkinImage() ? GameImage.CURSOR.getImage() : GameImage.CURSOR_OLD.getImage();
cursorTrail = GameImage.CURSOR_TRAIL.hasGameSkinImage() ? GameImage.CURSOR_TRAIL.getImage() : GameImage.CURSOR_TRAIL_OLD.getImage();
}
if (hasMiddle)
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
int removeCount = 0;
int FPSmod = (Options.getTargetFPS() / 60);
Skin skin = Options.getSkin();
// scale cursor
float cursorScale = Options.getCursorScale();
if (mousePressed && skin.isCursorExpanded())
cursorScale *= 1.25f; // increase the cursor size if pressed
float cursorScaleAnimated = 1f;
if (skin.isCursorExpanded()) {
if (lastCursorPressState != mousePressed) {
lastCursorPressState = mousePressed;
lastCursorPressTime = System.currentTimeMillis();
}
float cursorScaleChange = CURSOR_SCALE_CHANGE * AnimationEquation.IN_OUT_CUBIC.calc(
Utils.clamp(System.currentTimeMillis() - lastCursorPressTime, 0, CURSOR_SCALE_TIME) / CURSOR_SCALE_TIME);
cursorScaleAnimated = 1f + ((mousePressed) ? cursorScaleChange : CURSOR_SCALE_CHANGE - cursorScaleChange);
}
float cursorScale = cursorScaleAnimated * Options.getCursorScale();
if (cursorScale != 1f) {
cursor = cursor.getScaledCopy(cursorScale);
cursorTrail = cursorTrail.getScaledCopy(cursorScale);
if (hasMiddle)
cursorMiddle = cursorMiddle.getScaledCopy(cursorScale);
}
// TODO: use an image buffer
int removeCount = 0;
float FPSmod = Math.max(container.getFPS(), 1) / 60f;
if (newStyle) {
// new style: add all points between cursor movements
if (lastX < 0) {
lastX = mouseX;
lastY = mouseY;
if (lastPosition == null) {
lastPosition = new Point(mouseX, mouseY);
return;
}
addCursorPoints(lastX, lastY, mouseX, mouseY);
lastX = mouseX;
lastY = mouseY;
addCursorPoints(lastPosition.x, lastPosition.y, mouseX, mouseY);
lastPosition.move(mouseX, mouseY);
removeCount = (cursorX.size() / (6 * FPSmod)) + 1;
removeCount = (int) (trail.size() / (6 * FPSmod)) + 1;
} else {
// old style: sample one point at a time
cursorX.add(mouseX);
cursorY.add(mouseY);
trail.add(new Point(mouseX, mouseY));
int max = 10 * FPSmod;
if (cursorX.size() > max)
removeCount = cursorX.size() - max;
int max = (int) (10 * FPSmod);
if (trail.size() > max)
removeCount = trail.size() - max;
}
// remove points from the lists
for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) {
cursorX.remove();
cursorY.remove();
}
for (int i = 0; i < removeCount && !trail.isEmpty(); i++)
trail.remove();
// draw a fading trail
float alpha = 0f;
float t = 2f / cursorX.size();
if (skin.isCursorTrailRotated())
cursorTrail.setRotation(cursorAngle);
Iterator<Integer> iterX = cursorX.iterator();
Iterator<Integer> iterY = cursorY.iterator();
while (iterX.hasNext()) {
int cx = iterX.next();
int cy = iterY.next();
float t = 2f / trail.size();
int cursorTrailWidth = cursorTrail.getWidth(), cursorTrailHeight = cursorTrail.getHeight();
float cursorTrailRotation = (skin.isCursorTrailRotated()) ? cursorAngle : 0;
cursorTrail.startUse();
for (Point p : trail) {
alpha += t;
cursorTrail.setAlpha(alpha);
// if (cx != x || cy != y)
cursorTrail.drawCentered(cx, cy);
cursorTrail.setImageColor(1f, 1f, 1f, alpha);
cursorTrail.drawEmbedded(
p.x - (cursorTrailWidth / 2f), p.y - (cursorTrailHeight / 2f),
cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
}
cursorTrail.drawCentered(mouseX, mouseY);
cursorTrail.drawEmbedded(
mouseX - (cursorTrailWidth / 2f), mouseY - (cursorTrailHeight / 2f),
cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
cursorTrail.endUse();
// draw the other components
if (newStyle && skin.isCursorRotated())
@@ -212,8 +227,7 @@ public class Cursor {
if (dy <= dx) {
for (int i = 0; ; i++) {
if (i == k) {
cursorX.add(x1);
cursorY.add(y1);
trail.add(new Point(x1, y1));
i = 0;
}
if (x1 == x2)
@@ -228,8 +242,7 @@ public class Cursor {
} else {
for (int i = 0; ; i++) {
if (i == k) {
cursorX.add(x1);
cursorY.add(y1);
trail.add(new Point(x1, y1));
i = 0;
}
if (y1 == y2)
@@ -255,13 +268,13 @@ public class Cursor {
}
/**
* Resets all cursor data and skins.
* Resets all cursor data and beatmap skins.
*/
public void reset() {
// destroy skin images
GameImage.CURSOR.destroySkinImage();
GameImage.CURSOR_MIDDLE.destroySkinImage();
GameImage.CURSOR_TRAIL.destroySkinImage();
GameImage.CURSOR.destroyBeatmapSkinImage();
GameImage.CURSOR_MIDDLE.destroyBeatmapSkinImage();
GameImage.CURSOR_TRAIL.destroyBeatmapSkinImage();
// reset locations
resetLocations();
@@ -276,18 +289,17 @@ public class Cursor {
* Resets all cursor location data.
*/
public void resetLocations() {
lastX = lastY = -1;
cursorX.clear();
cursorY.clear();
lastPosition = null;
trail.clear();
}
/**
* Returns whether or not the cursor is skinned.
*/
public boolean isSkinned() {
return (GameImage.CURSOR.hasSkinImage() ||
GameImage.CURSOR_MIDDLE.hasSkinImage() ||
GameImage.CURSOR_TRAIL.hasSkinImage());
public boolean isBeatmapSkinned() {
return (GameImage.CURSOR.hasBeatmapSkinImage() ||
GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage() ||
GameImage.CURSOR_TRAIL.hasBeatmapSkinImage());
}
/**

View File

@@ -0,0 +1,424 @@
/*
* 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.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.newdawn.slick.Color;
import org.newdawn.slick.Font;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.gui.AbstractComponent;
import org.newdawn.slick.gui.GUIContext;
/**
* Simple dropdown menu.
* <p>
* Basic usage:
* <ul>
* <li>Override {@link #menuClicked(int)} to perform actions when the menu is clicked
* (e.g. play a sound effect, block input under certain conditions).
* <li>Override {@link #itemSelected(int, Object)} to perform actions when a new item is selected.
* <li>Call {@link #activate()}/{@link #deactivate()} whenever the component is needed
* (e.g. in a state's {@code enter} and {@code leave} events.
* </ul>
*
* @param <E> the type of the elements in the menu
*/
public class DropdownMenu<E> extends AbstractComponent {
/** Padding ratios for drawing. */
private static final float PADDING_Y = 0.1f, CHEVRON_X = 0.03f;
/** Whether this component is active. */
private boolean active;
/** The menu items. */
private E[] items;
/** The menu item names. */
private String[] itemNames;
/** The index of the selected item. */
private int itemIndex = 0;
/** Whether the menu is expanded. */
private boolean expanded = false;
/** The expanding animation progress. */
private AnimatedValue expandProgress = new AnimatedValue(300, 0f, 1f, AnimationEquation.LINEAR);
/** The last update time, in milliseconds. */
private long lastUpdateTime;
/** The top-left coordinates. */
private float x, y;
/** The width and height of the dropdown menu. */
private int width, height;
/** The height of the base item. */
private int baseHeight;
/** The vertical offset between items. */
private float offsetY;
/** The colors to use. */
private Color
textColor = Color.white, backgroundColor = Color.black,
highlightColor = Colors.BLUE_DIVIDER, borderColor = Colors.BLUE_DIVIDER,
chevronDownColor = textColor, chevronRightColor = backgroundColor;
/** The fonts to use. */
private UnicodeFont fontNormal = Fonts.MEDIUM, fontSelected = Fonts.MEDIUMBOLD;
/** The chevron images. */
private Image chevronDown, chevronRight;
/**
* Creates a new dropdown menu.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y) {
this(container, items, x, y, 0);
}
/**
* Creates a new dropdown menu with the given fonts.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
* @param normal the normal font
* @param selected the font for the selected item
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y, UnicodeFont normal, UnicodeFont selected) {
this(container, items, x, y, 0, normal, selected);
}
/**
* Creates a new dropdown menu with the given width.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
* @param width the menu width
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y, int width) {
super(container);
init(items, x, y, width);
}
/**
* Creates a new dropdown menu with the given width and fonts.
* @param container the container rendering this menu
* @param items the list of items (with names given as their {@code toString()} methods)
* @param x the top-left x coordinate
* @param y the top-left y coordinate
* @param width the menu width
* @param normal the normal font
* @param selected the font for the selected item
*/
public DropdownMenu(GUIContext container, E[] items, float x, float y, int width, UnicodeFont normal, UnicodeFont selected) {
super(container);
this.fontNormal = normal;
this.fontSelected = selected;
init(items, x, y, width);
}
/**
* Returns the maximum item width from the list.
*/
private int getMaxItemWidth() {
int maxWidth = 0;
for (int i = 0; i < itemNames.length; i++) {
int w = fontSelected.getWidth(itemNames[i]);
if (w > maxWidth)
maxWidth = w;
}
return maxWidth;
}
/**
* Initializes the component.
*/
private void init(E[] items, float x, float y, int width) {
this.items = items;
this.itemNames = new String[items.length];
for (int i = 0; i < itemNames.length; i++)
itemNames[i] = items[i].toString();
this.x = x;
this.y = y;
this.baseHeight = fontNormal.getLineHeight();
this.offsetY = baseHeight + baseHeight * PADDING_Y;
this.height = (int) (offsetY * (items.length + 1));
int chevronDownSize = baseHeight * 4 / 5;
this.chevronDown = GameImage.CHEVRON_DOWN.getImage().getScaledCopy(chevronDownSize, chevronDownSize);
int chevronRightSize = baseHeight * 2 / 3;
this.chevronRight = GameImage.CHEVRON_RIGHT.getImage().getScaledCopy(chevronRightSize, chevronRightSize);
int maxItemWidth = getMaxItemWidth();
int minWidth = maxItemWidth + chevronRight.getWidth() * 2;
this.width = Math.max(width, minWidth);
}
@Override
public void setLocation(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public int getX() { return (int) x; }
@Override
public int getY() { return (int) y; }
@Override
public int getWidth() { return width; }
@Override
public int getHeight() { return (expanded) ? height : baseHeight; }
/** Activates the component. */
public void activate() { this.active = true; }
/** Deactivates the component. */
public void deactivate() { this.active = false; }
/**
* Returns whether the dropdown menu is currently open.
* @return true if open, false otherwise
*/
public boolean isOpen() { return expanded; }
/**
* Opens or closes the dropdown menu.
* @param flag true to open, false to close
*/
public void open(boolean flag) { this.expanded = flag; }
/**
* Returns true if the coordinates are within the menu bounds.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public boolean contains(float cx, float cy) {
return (cx > x && cx < x + width && (
(cy > y && cy < y + baseHeight) ||
(expanded && cy > y + offsetY && cy < y + height)));
}
/**
* Returns true if the coordinates are within the base item bounds.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public boolean baseContains(float cx, float cy) {
return (cx > x && cx < x + width && cy > y && cy < y + baseHeight);
}
@Override
public void render(GUIContext container, Graphics g) throws SlickException {
// update animation
long time = container.getTime();
if (lastUpdateTime > 0) {
int delta = (int) (time - lastUpdateTime);
expandProgress.update((expanded) ? delta : -delta * 2);
}
this.lastUpdateTime = time;
// get parameters
Input input = container.getInput();
int idx = getIndexAt(input.getMouseX(), input.getMouseY());
float t = expandProgress.getValue();
if (expanded)
t = AnimationEquation.OUT_CUBIC.calc(t);
// background and border
Color oldGColor = g.getColor();
float oldLineWidth = g.getLineWidth();
final int cornerRadius = 6;
g.setLineWidth(1f);
g.setColor((idx == -1) ? highlightColor : backgroundColor);
g.fillRoundRect(x, y, width, baseHeight, cornerRadius);
g.setColor(borderColor);
g.drawRoundRect(x, y, width, baseHeight, cornerRadius);
if (expanded || t >= 0.0001) {
float oldBackgroundAlpha = backgroundColor.a;
backgroundColor.a *= t;
g.setColor(backgroundColor);
g.fillRoundRect(x, y + offsetY, width, (height - offsetY) * t, cornerRadius);
backgroundColor.a = oldBackgroundAlpha;
}
if (idx >= 0 && t >= 0.9999) {
g.setColor(highlightColor);
float yPos = y + offsetY + (offsetY * idx);
int yOff = 0, hOff = 0;
if (idx == 0 || idx == items.length - 1) {
g.fillRoundRect(x, yPos, width, offsetY, cornerRadius);
if (idx == 0)
yOff = cornerRadius;
hOff = cornerRadius;
}
g.fillRect(x, yPos + yOff, width, offsetY - hOff);
}
g.setColor(oldGColor);
g.setLineWidth(oldLineWidth);
// text
chevronDown.draw(x + width - chevronDown.getWidth() - width * CHEVRON_X, y + (baseHeight - chevronDown.getHeight()) / 2f, chevronDownColor);
fontNormal.drawString(x + (width * 0.03f), y + (fontNormal.getPaddingTop() + fontNormal.getPaddingBottom()) / 2f, itemNames[itemIndex], textColor);
float oldTextAlpha = textColor.a;
textColor.a *= t;
if (expanded || t >= 0.0001) {
for (int i = 0; i < itemNames.length; i++) {
Font f = (i == itemIndex) ? fontSelected : fontNormal;
if (i == idx && t >= 0.999)
chevronRight.draw(x, y + offsetY + (offsetY * i) + (offsetY - chevronRight.getHeight()) / 2f, chevronRightColor);
f.drawString(x + chevronRight.getWidth(), y + offsetY + (offsetY * i * t), itemNames[i], textColor);
}
}
textColor.a = oldTextAlpha;
}
/**
* Returns the index of the item at the given location, -1 for the base item,
* and -2 if there is no item at the location.
* @param cx the x coordinate
* @param cy the y coordinate
*/
private int getIndexAt(float cx, float cy) {
if (!contains(cx, cy))
return -2;
if (cy <= y + baseHeight)
return -1;
if (!expanded)
return -2;
return (int) ((cy - (y + offsetY)) / offsetY);
}
/**
* Resets the menu state.
*/
public void reset() {
this.expanded = false;
this.lastUpdateTime = 0;
expandProgress.setTime(0);
}
@Override
public void mousePressed(int button, int x, int y) {
if (!active)
return;
if (button == Input.MOUSE_MIDDLE_BUTTON)
return;
int idx = getIndexAt(x, y);
if (idx == -2) {
this.expanded = false;
return;
}
if (!menuClicked(idx))
return;
this.expanded = (idx == -1) ? !expanded : false;
if (idx >= 0 && itemIndex != idx) {
this.itemIndex = idx;
itemSelected(idx, items[idx]);
}
consumeEvent();
}
/**
* Notification that a new item was selected (via override).
* @param index the index of the item selected
* @param item the item selected
*/
public void itemSelected(int index, E item) {}
/**
* Notification that the menu was clicked (via override).
* @param index the index of the item clicked, or -1 for the base item
* @return true to process the click, or false to block/intercept it
*/
public boolean menuClicked(int index) { return true; }
@Override
public void setFocus(boolean focus) { /* does not currently use the "focus" concept */ }
@Override
public void mouseReleased(int button, int x, int y) { /* does not currently use the "focus" concept */ }
/**
* Selects the item at the given index.
* @param index the list item index
* @throws IllegalArgumentException if {@code index} is negative or greater than or equal to size
*/
public void setSelectedIndex(int index) {
if (index < 0 || index >= items.length)
throw new IllegalArgumentException();
this.itemIndex = index;
}
/**
* Returns the index of the selected item.
*/
public int getSelectedIndex() { return itemIndex; }
/**
* Returns the selected item.
*/
public E getSelectedItem() { return items[itemIndex]; }
/**
* Returns the item at the given index.
* @param index the list item index
*/
public E getItemAt(int index) { return items[index]; }
/**
* Returns the number of items in the list.
*/
public int getItemCount() { return items.length; }
/** Sets the text color. */
public void setTextColor(Color c) { this.textColor = c; }
/** Sets the background color. */
public void setBackgroundColor(Color c) { this.backgroundColor = c; }
/** Sets the highlight color. */
public void setHighlightColor(Color c) { this.highlightColor = c; }
/** Sets the border color. */
public void setBorderColor(Color c) { this.borderColor = c; }
/** Sets the down chevron color. */
public void setChevronDownColor(Color c) { this.chevronDownColor = c; }
/** Sets the right chevron color. */
public void setChevronRightColor(Color c) { this.chevronRightColor = c; }
}

View File

@@ -0,0 +1,156 @@
/*
* 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.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
import java.awt.Font;
import java.awt.FontFormatException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.UnicodeFont;
import org.newdawn.slick.font.effects.ColorEffect;
import org.newdawn.slick.font.effects.Effect;
import org.newdawn.slick.util.Log;
import org.newdawn.slick.util.ResourceLoader;
/**
* Fonts used for drawing.
*/
public class Fonts {
public static UnicodeFont DEFAULT, BOLD, XLARGE, LARGE, MEDIUM, MEDIUMBOLD, SMALL;
/** Set of all Unicode strings already loaded per font. */
private static HashMap<UnicodeFont, HashSet<String>> loadedGlyphs = new HashMap<UnicodeFont, HashSet<String>>();
// This class should not be instantiated.
private Fonts() {}
/**
* Initializes all fonts.
* @throws SlickException if ASCII glyphs could not be loaded
* @throws FontFormatException if any font stream data does not contain the required font tables
* @throws IOException if a font stream cannot be completely read
*/
public static void init() throws SlickException, FontFormatException, IOException {
float fontBase = 12f * GameImage.getUIscale();
Font javaFont = Font.createFont(Font.TRUETYPE_FONT, ResourceLoader.getResourceAsStream(Options.FONT_NAME));
Font font = javaFont.deriveFont(Font.PLAIN, (int) (fontBase * 4 / 3));
DEFAULT = new UnicodeFont(font);
BOLD = new UnicodeFont(font.deriveFont(Font.BOLD));
XLARGE = new UnicodeFont(font.deriveFont(fontBase * 3));
LARGE = new UnicodeFont(font.deriveFont(fontBase * 2));
MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2));
MEDIUMBOLD = new UnicodeFont(font.deriveFont(Font.BOLD, fontBase * 3 / 2));
SMALL = new UnicodeFont(font.deriveFont(fontBase));
ColorEffect colorEffect = new ColorEffect();
loadFont(DEFAULT, colorEffect);
loadFont(BOLD, colorEffect);
loadFont(XLARGE, colorEffect);
loadFont(LARGE, colorEffect);
loadFont(MEDIUM, colorEffect);
loadFont(MEDIUMBOLD, colorEffect);
loadFont(SMALL, colorEffect);
}
/**
* Loads a Unicode font and its ASCII glyphs.
* @param font the font to load
* @param effect the font effect
* @throws SlickException if the glyphs could not be loaded
*/
@SuppressWarnings("unchecked")
private static void loadFont(UnicodeFont font, Effect effect) throws SlickException {
font.addAsciiGlyphs();
font.getEffects().add(effect);
font.loadGlyphs();
}
/**
* Adds and loads glyphs for a font.
* @param font the font to add the glyphs to
* @param s the string containing the glyphs to load
*/
public static void loadGlyphs(UnicodeFont font, String s) {
if (s == null || s.isEmpty())
return;
// get set of added strings
HashSet<String> set = loadedGlyphs.get(font);
if (set == null) {
set = new HashSet<String>();
loadedGlyphs.put(font, set);
} else if (set.contains(s))
return; // string already in set
// load glyphs
font.addGlyphs(s);
set.add(s);
try {
font.loadGlyphs();
} catch (SlickException e) {
Log.warn(String.format("Failed to load glyphs for string '%s'.", s), e);
}
}
/**
* Wraps the given string into a list of split lines based on the width.
* @param font the font used to draw the string
* @param text the text to split
* @param width the maximum width of a line
* @return the list of split strings
* @author davedes (http://slick.ninjacave.com/forum/viewtopic.php?t=3778)
*/
public static List<String> wrap(org.newdawn.slick.Font font, String text, int width) {
List<String> list = new ArrayList<String>();
String str = text;
String line = "";
int i = 0;
int lastSpace = -1;
while (i < str.length()) {
char c = str.charAt(i);
if (Character.isWhitespace(c))
lastSpace = i;
String append = line + c;
if (font.getWidth(append) > width) {
int split = (lastSpace != -1) ? lastSpace : i;
int splitTrimmed = split;
if (lastSpace != -1 && split < str.length() - 1)
splitTrimmed++;
list.add(str.substring(0, split));
str = str.substring(splitTrimmed);
line = "";
i = 0;
lastSpace = -1;
} else {
line = append;
i++;
}
}
if (str.length() != 0)
list.add(str);
return list;
}
}

View File

@@ -19,6 +19,8 @@
package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import org.newdawn.slick.Animation;
import org.newdawn.slick.Color;
@@ -63,11 +65,26 @@ public class MenuButton {
/** The hover actions for this button. */
private int hoverEffect = 0;
/** The current and max scale of the button. */
private float scale = 1f, hoverScale = 1.25f;
/** The hover animation duration, in milliseconds. */
private int animationDuration = 100;
/** The current and base alpha level of the button. */
private float alpha = 1f, baseAlpha = 0.75f;
/** The hover animation equation. */
private AnimationEquation animationEqn = AnimationEquation.LINEAR;
/** Whether the animation is advancing forwards (if advancing automatically). */
private boolean autoAnimationForward = true;
/** The scale of the button. */
private AnimatedValue scale;
/** The default max scale of the button. */
private static final float DEFAULT_SCALE_MAX = 1.25f;
/** The alpha level of the button. */
private AnimatedValue alpha;
/** The default base alpha level of the button. */
private static final float DEFAULT_ALPHA_BASE = 0.75f;
/** The scaled expansion direction for the button. */
private Expand dir = Expand.CENTER;
@@ -75,8 +92,11 @@ public class MenuButton {
/** Scaled expansion directions. */
public enum Expand { CENTER, UP, RIGHT, LEFT, DOWN, UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT; }
/** The current and max rotation angles of the button. */
private float angle = 0f, maxAngle = 30f;
/** The rotation angle of the button. */
private AnimatedValue angle;
/** The default max rotation angle of the button. */
private static final float DEFAULT_ANGLE_MAX = 30f;
/**
* Creates a new button from an Image.
@@ -126,11 +146,13 @@ public class MenuButton {
/**
* Sets a new center x coordinate.
* @param x the x coordinate
*/
public void setX(float x) { this.x = x; }
/**
* Sets a new center y coordinate.
* @param y the y coordinate
*/
public void setY(float y) { this.y = y; }
@@ -192,15 +214,15 @@ public class MenuButton {
float oldAlpha = image.getAlpha();
float oldAngle = image.getRotation();
if ((hoverEffect & EFFECT_EXPAND) > 0) {
if (scale != 1f) {
image = image.getScaledCopy(scale);
if (scale.getValue() != 1f) {
image = image.getScaledCopy(scale.getValue());
image.setAlpha(oldAlpha);
}
}
if ((hoverEffect & EFFECT_FADE) > 0)
image.setAlpha(alpha);
image.setAlpha(alpha.getValue());
if ((hoverEffect & EFFECT_ROTATE) > 0)
image.setRotation(angle);
image.setRotation(angle.getValue());
image.draw(x - xRadius, y - yRadius, filter);
if (image == this.img) {
image.setAlpha(oldAlpha);
@@ -217,9 +239,10 @@ public class MenuButton {
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
} else if ((hoverEffect & EFFECT_FADE) > 0) {
float a = image.getAlpha(), aL = imgL.getAlpha(), aR = imgR.getAlpha();
image.setAlpha(alpha);
imgL.setAlpha(alpha);
imgR.setAlpha(alpha);
float currentAlpha = alpha.getValue();
image.setAlpha(currentAlpha);
imgL.setAlpha(currentAlpha);
imgR.setAlpha(currentAlpha);
image.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter);
imgL.draw(x - xRadius, y - yRadius, filter);
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
@@ -267,28 +290,63 @@ public class MenuButton {
*/
public void resetHover() {
if ((hoverEffect & EFFECT_EXPAND) > 0) {
this.scale = 1f;
scale.setTime(0);
setHoverRadius();
}
if ((hoverEffect & EFFECT_FADE) > 0)
this.alpha = baseAlpha;
alpha.setTime(0);
if ((hoverEffect & EFFECT_ROTATE) > 0)
this.angle = 0f;
angle.setTime(0);
autoAnimationForward = true;
}
/**
* Removes all hover effects that have been set for the button.
*/
public void removeHoverEffects() { hoverEffect = 0; }
public void removeHoverEffects() {
this.hoverEffect = 0;
this.scale = null;
this.alpha = null;
this.angle = null;
autoAnimationForward = true;
}
/**
* Sets the hover animation duration.
* @param duration the duration, in milliseconds
*/
public void setHoverAnimationDuration(int duration) {
this.animationDuration = duration;
if (scale != null)
scale.setDuration(duration);
if (alpha != null)
alpha.setDuration(duration);
if (angle != null)
angle.setDuration(duration);
}
/**
* Sets the hover animation equation.
* @param eqn the equation to use
*/
public void setHoverAnimationEquation(AnimationEquation eqn) {
this.animationEqn = eqn;
if (scale != null)
scale.setEquation(eqn);
if (alpha != null)
alpha.setEquation(eqn);
if (angle != null)
angle.setEquation(eqn);
}
/**
* Sets the "expand" hover effect.
*/
public void setHoverExpand() { hoverEffect |= EFFECT_EXPAND; }
public void setHoverExpand() { setHoverExpand(DEFAULT_SCALE_MAX, this.dir); }
/**
* Sets the "expand" hover effect.
* @param scale the maximum scale factor (default 1.25f)
* @param scale the maximum scale factor
*/
public void setHoverExpand(float scale) { setHoverExpand(scale, this.dir); }
@@ -296,45 +354,45 @@ public class MenuButton {
* Sets the "expand" hover effect.
* @param dir the expansion direction
*/
public void setHoverExpand(Expand dir) { setHoverExpand(this.hoverScale, dir); }
public void setHoverExpand(Expand dir) { setHoverExpand(DEFAULT_SCALE_MAX, dir); }
/**
* Sets the "expand" hover effect.
* @param scale the maximum scale factor (default 1.25f)
* @param scale the maximum scale factor
* @param dir the expansion direction
*/
public void setHoverExpand(float scale, Expand dir) {
hoverEffect |= EFFECT_EXPAND;
this.hoverScale = scale;
this.scale = new AnimatedValue(animationDuration, 1f, scale, animationEqn);
this.dir = dir;
}
/**
* Sets the "fade" hover effect.
*/
public void setHoverFade() { hoverEffect |= EFFECT_FADE; }
public void setHoverFade() { setHoverFade(DEFAULT_ALPHA_BASE); }
/**
* Sets the "fade" hover effect.
* @param baseAlpha the base alpha level to fade in from (default 0.7f)
* @param baseAlpha the base alpha level to fade in from
*/
public void setHoverFade(float baseAlpha) {
hoverEffect |= EFFECT_FADE;
this.baseAlpha = baseAlpha;
this.alpha = new AnimatedValue(animationDuration, baseAlpha, 1f, animationEqn);
}
/**
* Sets the "rotate" hover effect.
*/
public void setHoverRotate() { hoverEffect |= EFFECT_ROTATE; }
public void setHoverRotate() { setHoverRotate(DEFAULT_ANGLE_MAX); }
/**
* Sets the "rotate" hover effect.
* @param maxAngle the maximum rotation angle, in degrees (default 30f)
* @param maxAngle the maximum rotation angle, in degrees
*/
public void setHoverRotate(float maxAngle) {
hoverEffect |= EFFECT_ROTATE;
this.maxAngle = maxAngle;
this.angle = new AnimatedValue(animationDuration, 0f, maxAngle, animationEqn);
}
/**
@@ -371,45 +429,53 @@ public class MenuButton {
if (hoverEffect == 0)
return;
int d = delta * (isHover ? 1 : -1);
// scale the button
if ((hoverEffect & EFFECT_EXPAND) > 0) {
int sign = 0;
if (isHover && scale < hoverScale)
sign = 1;
else if (!isHover && scale > 1f)
sign = -1;
if (sign != 0) {
scale = Utils.getBoundedValue(scale, sign * (hoverScale - 1f) * delta / 100f, 1, hoverScale);
if (scale.update(d))
setHoverRadius();
}
}
// fade the button
if ((hoverEffect & EFFECT_FADE) > 0) {
int sign = 0;
if (isHover && alpha < 1f)
sign = 1;
else if (!isHover && alpha > baseAlpha)
sign = -1;
if (sign != 0)
alpha = Utils.getBoundedValue(alpha, sign * (1f - baseAlpha) * delta / 200f, baseAlpha, 1f);
}
if ((hoverEffect & EFFECT_FADE) > 0)
alpha.update(d);
// rotate the button
if ((hoverEffect & EFFECT_ROTATE) > 0) {
int sign = 0;
boolean right = (maxAngle > 0);
if (isHover && angle != maxAngle)
sign = (right) ? 1 : -1;
else if (!isHover && angle != 0)
sign = (right) ? -1 : 1;
if (sign != 0) {
float diff = sign * Math.abs(maxAngle) * delta / 125f;
angle = (right) ?
Utils.getBoundedValue(angle, diff, 0, maxAngle) :
Utils.getBoundedValue(angle, diff, maxAngle, 0);
if ((hoverEffect & EFFECT_ROTATE) > 0)
angle.update(d);
}
/**
* Automatically advances the hover animation in a loop.
* @param delta the delta interval
* @param reverseAtEnd whether to reverse or restart the animation upon reaching the end
*/
public void autoHoverUpdate(int delta, boolean reverseAtEnd) {
if (hoverEffect == 0)
return;
int time = ((hoverEffect & EFFECT_EXPAND) > 0) ? scale.getTime() :
((hoverEffect & EFFECT_FADE) > 0) ? alpha.getTime() :
((hoverEffect & EFFECT_ROTATE) > 0) ? angle.getTime() : -1;
if (time == -1)
return;
int d = delta * (autoAnimationForward ? 1 : -1);
if (Utils.clamp(time + d, 0, animationDuration) == time) {
if (reverseAtEnd)
autoAnimationForward = !autoAnimationForward;
else {
if ((hoverEffect & EFFECT_EXPAND) > 0)
scale.setTime(0);
if ((hoverEffect & EFFECT_FADE) > 0)
alpha.setTime(0);
if ((hoverEffect & EFFECT_ROTATE) > 0)
angle.setTime(0);
}
}
hoverUpdate(delta, autoAnimationForward);
}
/**
@@ -422,10 +488,11 @@ public class MenuButton {
image = anim.getCurrentFrame();
int xOffset = 0, yOffset = 0;
float currentScale = scale.getValue();
if (dir != Expand.CENTER) {
// offset by difference between normal/scaled image dimensions
xOffset = (int) ((scale - 1f) * image.getWidth());
yOffset = (int) ((scale - 1f) * image.getHeight());
xOffset = (int) ((currentScale - 1f) * image.getWidth());
yOffset = (int) ((currentScale - 1f) * image.getHeight());
if (dir == Expand.UP || dir == Expand.DOWN)
xOffset = 0; // no horizontal offset
if (dir == Expand.RIGHT || dir == Expand.LEFT)
@@ -435,7 +502,7 @@ public class MenuButton {
if (dir == Expand.DOWN || dir == Expand.DOWN_LEFT || dir == Expand.DOWN_RIGHT)
yOffset *= -1; // flip y for down
}
this.xRadius = ((image.getWidth() * scale) + xOffset) / 2f;
this.yRadius = ((image.getHeight() * scale) + yOffset) / 2f;
this.xRadius = ((image.getWidth() * currentScale) + xOffset) / 2f;
this.yRadius = ((image.getHeight() * currentScale) + yOffset) / 2f;
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.ui;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import org.newdawn.slick.Image;
/**
* Horizontal star stream.
*/
public class StarStream {
/** The container dimensions. */
private final int containerWidth, containerHeight;
/** The star image. */
private final Image starImg;
/** The current list of stars. */
private final List<Star> stars;
/** The maximum number of stars to draw at once. */
private static final int MAX_STARS = 20;
/** Random number generator instance. */
private final Random random;
/** Contains data for a single star. */
private class Star {
/** The star animation progress. */
private final AnimatedValue animatedValue;
/** The star properties. */
private final int distance, yOffset, angle;
/**
* Creates a star with the given properties.
* @param duration the time, in milliseconds, to show the star
* @param distance the distance for the star to travel in {@code duration}
* @param yOffset the vertical offset from the center of the container
* @param angle the rotation angle
* @param eqn the animation equation to use
*/
public Star(int duration, int distance, int yOffset, int angle, AnimationEquation eqn) {
this.animatedValue = new AnimatedValue(duration, 0f, 1f, eqn);
this.distance = distance;
this.yOffset = yOffset;
this.angle = angle;
}
/**
* Draws the star.
*/
public void draw() {
float t = animatedValue.getValue();
starImg.setImageColor(1f, 1f, 1f, Math.min((1 - t) * 5f, 1f));
starImg.drawEmbedded(
containerWidth - (distance * t), ((containerHeight - starImg.getHeight()) / 2) + yOffset,
starImg.getWidth(), starImg.getHeight(), angle);
}
/**
* Updates the animation by a delta interval.
* @param delta the delta interval since the last call
* @return true if an update was applied, false if the animation was not updated
*/
public boolean update(int delta) { return animatedValue.update(delta); }
}
/**
* Initializes the star stream.
* @param width the container width
* @param height the container height
*/
public StarStream(int width, int height) {
this.containerWidth = width;
this.containerHeight = height;
this.starImg = GameImage.STAR2.getImage().copy();
this.stars = new ArrayList<Star>();
this.random = new Random();
}
/**
* Draws the star stream.
*/
public void draw() {
if (stars.isEmpty())
return;
starImg.startUse();
for (Star star : stars)
star.draw();
starImg.endUse();
}
/**
* Updates the stars in the stream by a delta interval.
* @param delta the delta interval since the last call
*/
public void update(int delta) {
// update current stars
Iterator<Star> iter = stars.iterator();
while (iter.hasNext()) {
Star star = iter.next();
if (!star.update(delta))
iter.remove();
}
// create new stars
for (int i = stars.size(); i < MAX_STARS; i++) {
if (Math.random() < ((i < 5) ? 0.25 : 0.66))
break;
// generate star properties
float distanceRatio = Utils.clamp((float) getGaussian(0.65, 0.25), 0.2f, 0.925f);
int distance = (int) (containerWidth * distanceRatio);
int duration = (int) (distanceRatio * getGaussian(1300, 300));
int yOffset = (int) getGaussian(0, containerHeight / 20);
int angle = (int) getGaussian(0, 22.5);
AnimationEquation eqn = random.nextBoolean() ? AnimationEquation.IN_OUT_QUAD : AnimationEquation.OUT_QUAD;
stars.add(new Star(duration, distance, angle, yOffset, eqn));
}
}
/**
* Clears the stars currently in the stream.
*/
public void clear() { stars.clear(); }
/**
* Returns the next pseudorandom, Gaussian ("normally") distributed {@code double} value
* with the given mean and standard deviation.
* @param mean the mean
* @param stdDev the standard deviation
*/
private double getGaussian(double mean, double stdDev) {
return mean + random.nextGaussian() * stdDev;
}
}

View File

@@ -21,11 +21,13 @@ package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.ErrorHandler;
import itdelatrisu.opsu.GameImage;
import itdelatrisu.opsu.Options;
import itdelatrisu.opsu.OszUnpacker;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.replay.ReplayImporter;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import javax.swing.JOptionPane;
import javax.swing.UIManager;
@@ -36,7 +38,6 @@ import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.state.StateBasedGame;
/**
@@ -62,7 +63,7 @@ public class UI {
private static int barNotifTimer = -1;
/** Duration, in milliseconds, to display bar notifications. */
private static final int BAR_NOTIFICATION_TIME = 1250;
private static final int BAR_NOTIFICATION_TIME = 1500;
/** The current tooltip. */
private static String tooltip;
@@ -70,11 +71,8 @@ public class UI {
/** Whether or not to check the current tooltip for line breaks. */
private static boolean tooltipNewlines;
/** The current tooltip timer. */
private static int tooltipTimer = -1;
/** Duration, in milliseconds, to fade tooltips. */
private static final int TOOLTIP_FADE_TIME = 200;
/** The alpha level of the current tooltip (if any). */
private static AnimatedValue tooltipAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);
// game-related variables
private static GameContainer container;
@@ -87,10 +85,8 @@ public class UI {
* Initializes UI data.
* @param container the game container
* @param game the game object
* @throws SlickException
*/
public static void init(GameContainer container, StateBasedGame game)
throws SlickException {
public static void init(GameContainer container, StateBasedGame game) {
UI.container = container;
UI.input = container.getInput();
@@ -106,6 +102,8 @@ public class UI {
Image back = GameImage.MENU_BACK.getImage();
backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f));
}
backButton.setHoverAnimationDuration(350);
backButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
backButton.setHoverExpand(MenuButton.Expand.UP_RIGHT);
}
@@ -117,12 +115,11 @@ public class UI {
cursor.update(delta);
updateVolumeDisplay(delta);
updateBarNotification(delta);
if (tooltipTimer > 0)
tooltipTimer -= delta;
tooltipAlpha.update(-delta);
}
/**
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
* Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications.
* @param g the graphics context
*/
public static void draw(Graphics g) {
@@ -134,7 +131,7 @@ public class UI {
}
/**
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
* Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications.
* @param g the graphics context
* @param mouseX the mouse x coordinate
* @param mouseY the mouse y coordinate
@@ -178,18 +175,18 @@ public class UI {
*/
public static void drawTab(float x, float y, String text, boolean selected, boolean isHover) {
Image tabImage = GameImage.MENU_TAB.getImage();
float tabTextX = x - (Utils.FONT_MEDIUM.getWidth(text) / 2);
float tabTextX = x - (Fonts.MEDIUM.getWidth(text) / 2);
float tabTextY = y - (tabImage.getHeight() / 2);
Color filter, textColor;
if (selected) {
filter = Color.white;
textColor = Color.black;
} else {
filter = (isHover) ? Utils.COLOR_RED_HOVER : Color.red;
filter = (isHover) ? Colors.RED_HOVER : Color.red;
textColor = Color.white;
}
tabImage.drawCentered(x, y, filter);
Utils.FONT_MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
Fonts.MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
}
/**
@@ -201,14 +198,14 @@ public class UI {
return;
String fps = String.format("%dFPS", container.getFPS());
Utils.FONT_BOLD.drawString(
container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth(fps),
container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight(fps),
Fonts.BOLD.drawString(
container.getWidth() * 0.997f - Fonts.BOLD.getWidth(fps),
container.getHeight() * 0.997f - Fonts.BOLD.getHeight(fps),
Integer.toString(container.getFPS()), Color.white
);
Utils.FONT_DEFAULT.drawString(
container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth("FPS"),
container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight("FPS"),
Fonts.DEFAULT.drawString(
container.getWidth() * 0.997f - Fonts.BOLD.getWidth("FPS"),
container.getHeight() * 0.997f - Fonts.BOLD.getHeight("FPS"),
"FPS", Color.white
);
}
@@ -263,7 +260,7 @@ public class UI {
*/
public static void changeVolume(int units) {
final float UNIT_OFFSET = 0.05f;
Options.setMasterVolume(container, Utils.getBoundedValue(Options.getMasterVolume(), UNIT_OFFSET * units, 0f, 1f));
Options.setMasterVolume(container, Utils.clamp(Options.getMasterVolume() + (UNIT_OFFSET * units), 0f, 1f));
if (volumeDisplay == -1)
volumeDisplay = 0;
else if (volumeDisplay >= VOLUME_DISPLAY_TIME / 10)
@@ -271,8 +268,9 @@ public class UI {
}
/**
* Draws loading progress (OSZ unpacking, beatmap parsing, sound loading)
* Draws loading progress (OSZ unpacking, beatmap parsing, replay importing, sound loading)
* at the bottom of the screen.
* @param g the graphics context
*/
public static void drawLoadingProgress(Graphics g) {
String text, file;
@@ -298,16 +296,16 @@ public class UI {
// draw loading info
float marginX = container.getWidth() * 0.02f, marginY = container.getHeight() * 0.02f;
float lineY = container.getHeight() - marginY;
int lineOffsetY = Utils.FONT_MEDIUM.getLineHeight();
int lineOffsetY = Fonts.MEDIUM.getLineHeight();
if (Options.isLoadVerbose()) {
// verbose: display percentages and file names
Utils.FONT_MEDIUM.drawString(
Fonts.MEDIUM.drawString(
marginX, lineY - (lineOffsetY * 2),
String.format("%s (%d%%)", text, progress), Color.white);
Utils.FONT_MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
Fonts.MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
} else {
// draw loading bar
Utils.FONT_MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
Fonts.MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
g.setColor(Color.white);
g.fillRoundRect(marginX, lineY - (lineOffsetY / 2f),
(container.getWidth() - (marginX * 2f)) * progress / 100f, lineOffsetY / 4f, 4
@@ -357,12 +355,7 @@ public class UI {
if (s != null) {
tooltip = s;
tooltipNewlines = newlines;
if (tooltipTimer <= 0)
tooltipTimer = delta;
else
tooltipTimer += delta * 2;
if (tooltipTimer > TOOLTIP_FADE_TIME)
tooltipTimer = TOOLTIP_FADE_TIME;
tooltipAlpha.update(delta * 2);
}
}
@@ -372,26 +365,26 @@ public class UI {
* @param g the graphics context
*/
public static void drawTooltip(Graphics g) {
if (tooltipTimer <= 0 || tooltip == null)
if (tooltipAlpha.getTime() == 0 || tooltip == null)
return;
int containerWidth = container.getWidth(), containerHeight = container.getHeight();
int margin = containerWidth / 100, textMarginX = 2;
int offset = GameImage.CURSOR_MIDDLE.getImage().getWidth() / 2;
int lineHeight = Utils.FONT_SMALL.getLineHeight();
int lineHeight = Fonts.SMALL.getLineHeight();
int textWidth = textMarginX * 2, textHeight = lineHeight;
if (tooltipNewlines) {
String[] lines = tooltip.split("\\n");
int maxWidth = Utils.FONT_SMALL.getWidth(lines[0]);
int maxWidth = Fonts.SMALL.getWidth(lines[0]);
for (int i = 1; i < lines.length; i++) {
int w = Utils.FONT_SMALL.getWidth(lines[i]);
int w = Fonts.SMALL.getWidth(lines[i]);
if (w > maxWidth)
maxWidth = w;
}
textWidth += maxWidth;
textHeight += lineHeight * (lines.length - 1);
} else
textWidth += Utils.FONT_SMALL.getWidth(tooltip);
textWidth += Fonts.SMALL.getWidth(tooltip);
// get drawing coordinates
int x = input.getMouseX() + offset, y = input.getMouseY() + offset;
@@ -405,29 +398,29 @@ public class UI {
y = margin;
// draw tooltip text inside a filled rectangle
float alpha = (float) tooltipTimer / TOOLTIP_FADE_TIME;
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a = alpha;
g.setColor(Utils.COLOR_BLACK_ALPHA);
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
float alpha = tooltipAlpha.getValue();
float oldAlpha = Colors.BLACK_ALPHA.a;
Colors.BLACK_ALPHA.a = alpha;
g.setColor(Colors.BLACK_ALPHA);
Colors.BLACK_ALPHA.a = oldAlpha;
g.fillRect(x, y, textWidth, textHeight);
oldAlpha = Utils.COLOR_DARK_GRAY.a;
Utils.COLOR_DARK_GRAY.a = alpha;
g.setColor(Utils.COLOR_DARK_GRAY);
oldAlpha = Colors.DARK_GRAY.a;
Colors.DARK_GRAY.a = alpha;
g.setColor(Colors.DARK_GRAY);
g.setLineWidth(1);
g.drawRect(x, y, textWidth, textHeight);
Utils.COLOR_DARK_GRAY.a = oldAlpha;
oldAlpha = Utils.COLOR_WHITE_ALPHA.a;
Utils.COLOR_WHITE_ALPHA.a = alpha;
Utils.FONT_SMALL.drawString(x + textMarginX, y, tooltip, Utils.COLOR_WHITE_ALPHA);
Utils.COLOR_WHITE_ALPHA.a = oldAlpha;
Colors.DARK_GRAY.a = oldAlpha;
oldAlpha = Colors.WHITE_ALPHA.a;
Colors.WHITE_ALPHA.a = alpha;
Fonts.SMALL.drawString(x + textMarginX, y, tooltip, Colors.WHITE_ALPHA);
Colors.WHITE_ALPHA.a = oldAlpha;
}
/**
* Resets the tooltip.
*/
public static void resetTooltip() {
tooltipTimer = -1;
tooltipAlpha.setTime(0);
tooltip = null;
}
@@ -475,18 +468,18 @@ public class UI {
if (barNotifTimer >= BAR_NOTIFICATION_TIME * 0.9f)
alpha -= 1 - ((BAR_NOTIFICATION_TIME - barNotifTimer) / (BAR_NOTIFICATION_TIME * 0.1f));
int midX = container.getWidth() / 2, midY = container.getHeight() / 2;
float barHeight = Utils.FONT_LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
float oldAlphaB = Utils.COLOR_BLACK_ALPHA.a, oldAlphaW = Utils.COLOR_WHITE_ALPHA.a;
Utils.COLOR_BLACK_ALPHA.a *= alpha;
Utils.COLOR_WHITE_ALPHA.a = alpha;
g.setColor(Utils.COLOR_BLACK_ALPHA);
float barHeight = Fonts.LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
float oldAlphaB = Colors.BLACK_ALPHA.a, oldAlphaW = Colors.WHITE_ALPHA.a;
Colors.BLACK_ALPHA.a *= alpha;
Colors.WHITE_ALPHA.a = alpha;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, midY - barHeight / 2f, container.getWidth(), barHeight);
Utils.FONT_LARGE.drawString(
midX - Utils.FONT_LARGE.getWidth(barNotif) / 2f,
midY - Utils.FONT_LARGE.getLineHeight() / 2.2f,
barNotif, Utils.COLOR_WHITE_ALPHA);
Utils.COLOR_BLACK_ALPHA.a = oldAlphaB;
Utils.COLOR_WHITE_ALPHA.a = oldAlphaW;
Fonts.LARGE.drawString(
midX - Fonts.LARGE.getWidth(barNotif) / 2f,
midY - Fonts.LARGE.getLineHeight() / 2.2f,
barNotif, Colors.WHITE_ALPHA);
Colors.BLACK_ALPHA.a = oldAlphaB;
Colors.WHITE_ALPHA.a = oldAlphaW;
}
/**

View File

@@ -0,0 +1,134 @@
/*
* 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.ui.animations;
import itdelatrisu.opsu.Utils;
/**
* Utility class for updating a value using an animation equation.
*/
public class AnimatedValue {
/** The animation duration, in milliseconds. */
private int duration;
/** The current time, in milliseconds. */
private int time;
/** The base value. */
private float base;
/** The maximum difference from the base value. */
private float diff;
/** The current value. */
private float value;
/** The animation equation to use. */
private AnimationEquation eqn;
/**
* Constructor.
* @param duration the total animation duration, in milliseconds
* @param min the minimum value
* @param max the maximum value
* @param eqn the animation equation to use
*/
public AnimatedValue(int duration, float min, float max, AnimationEquation eqn) {
this.time = 0;
this.duration = duration;
this.value = min;
this.base = min;
this.diff = max - min;
this.eqn = eqn;
}
/**
* Returns the current value.
*/
public float getValue() { return value; }
/**
* Returns the current animation time, in milliseconds.
*/
public int getTime() { return time; }
/**
* Sets the animation time manually.
* @param time the new time, in milliseconds
*/
public void setTime(int time) {
this.time = Utils.clamp(time, 0, duration);
updateValue();
}
/**
* Returns the total animation duration, in milliseconds.
*/
public int getDuration() { return duration; }
/**
* Sets the animation duration.
* @param duration the new duration, in milliseconds
*/
public void setDuration(int duration) {
this.duration = duration;
int newTime = Utils.clamp(time, 0, duration);
if (time != newTime) {
this.time = newTime;
updateValue();
}
}
/**
* Returns the animation equation being used.
*/
public AnimationEquation getEquation() { return eqn; }
/**
* Sets the animation equation to use.
* @param eqn the new equation
*/
public void setEquation(AnimationEquation eqn) {
this.eqn = eqn;
updateValue();
}
/**
* Updates the animation by a delta interval.
* @param delta the delta interval since the last call.
* @return true if an update was applied, false if the animation was not updated
*/
public boolean update(int delta) {
int newTime = Utils.clamp(time + delta, 0, duration);
if (time != newTime) {
this.time = newTime;
updateValue();
return true;
}
return false;
}
/**
* Recalculates the value by applying the animation equation with the current time.
*/
private void updateValue() {
float t = eqn.calc((float) time / duration);
this.value = base + (t * diff);
}
}

View File

@@ -0,0 +1,308 @@
/*
* 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.ui.animations;
/*
* These equations are copyright (c) 2001 Robert Penner, all rights reserved,
* and are open source under the BSD License.
* http://www.opensource.org/licenses/bsd-license.php
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of contributors may be used
* to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* Easing functions for animations.
*
* @author Robert Penner (<a href="http://robertpenner.com/easing/">http://robertpenner.com/easing/</a>)
* @author CharlotteGore (<a href="https://github.com/CharlotteGore/functional-easing">https://github.com/CharlotteGore/functional-easing</a>)
*/
public enum AnimationEquation {
/* Linear */
LINEAR {
@Override
public float calc(float t) { return t; }
},
/* Quadratic */
IN_QUAD {
@Override
public float calc(float t) { return t * t; }
},
OUT_QUAD {
@Override
public float calc(float t) { return -1 * t * (t - 2); }
},
IN_OUT_QUAD {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t;
t = t - 1;
return -0.5f * (t * (t - 2) - 1);
}
},
/* Cubic */
IN_CUBIC {
@Override
public float calc(float t) { return t * t * t; }
},
OUT_CUBIC {
@Override
public float calc(float t) {
t = t - 1;
return t * t * t + 1;
}
},
IN_OUT_CUBIC {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t * t;
t = t - 2;
return 0.5f * (t * t * t + 2);
}
},
/* Quartic */
IN_QUART {
@Override
public float calc(float t) { return t * t * t * t; }
},
OUT_QUART {
@Override
public float calc(float t) {
t = t - 1;
return -1 * (t * t * t * t - 1);
}
},
IN_OUT_QUART {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t * t * t;
t = t - 2;
return -0.5f * (t * t * t * t - 2);
}
},
/* Quintic */
IN_QUINT {
@Override
public float calc(float t) { return t * t * t * t * t; }
},
OUT_QUINT {
@Override
public float calc(float t) {
t = t - 1;
return (t * t * t * t * t + 1);
}
},
IN_OUT_QUINT {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return 0.5f * t * t * t * t * t;
t = t - 2;
return 0.5f * (t * t * t * t * t + 2);
}
},
/* Sine */
IN_SINE {
@Override
public float calc(float t) { return -1 * (float) Math.cos(t * (Math.PI / 2)) + 1; }
},
OUT_SINE {
@Override
public float calc(float t) { return (float) Math.sin(t * (Math.PI / 2)); }
},
IN_OUT_SINE {
@Override
public float calc(float t) { return (float) (Math.cos(Math.PI * t) - 1) / -2; }
},
/* Exponential */
IN_EXPO {
@Override
public float calc(float t) { return (t == 0) ? 0 : (float) Math.pow(2, 10 * (t - 1)); }
},
OUT_EXPO {
@Override
public float calc(float t) { return (t == 1) ? 1 : (float) -Math.pow(2, -10 * t) + 1; }
},
IN_OUT_EXPO {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
t = t * 2;
if (t < 1)
return 0.5f * (float) Math.pow(2, 10 * (t - 1));
t = t - 1;
return 0.5f * ((float) -Math.pow(2, -10 * t) + 2);
}
},
/* Circular */
IN_CIRC {
@Override
public float calc(float t) { return -1 * ((float) Math.sqrt(1 - t * t) - 1); }
},
OUT_CIRC {
@Override
public float calc(float t) {
t = t - 1;
return (float) Math.sqrt(1 - t * t);
}
},
IN_OUT_CIRC {
@Override
public float calc(float t) {
t = t * 2;
if (t < 1)
return -0.5f * ((float) Math.sqrt(1 - t * t) - 1);
t = t - 2;
return 0.5f * ((float) Math.sqrt(1 - t * t) + 1);
}
},
/* Back */
IN_BACK {
@Override
public float calc(float t) { return t * t * ((OVERSHOOT + 1) * t - OVERSHOOT); }
},
OUT_BACK {
@Override
public float calc(float t) {
t = t - 1;
return t * t * ((OVERSHOOT + 1) * t + OVERSHOOT) + 1;
}
},
IN_OUT_BACK {
@Override
public float calc(float t) {
float overshoot = OVERSHOOT * 1.525f;
t = t * 2;
if (t < 1)
return 0.5f * (t * t * ((overshoot + 1) * t - overshoot));
t = t - 2;
return 0.5f * (t * t * ((overshoot + 1) * t + overshoot) + 2);
}
},
/* Bounce */
IN_BOUNCE {
@Override
public float calc(float t) { return 1 - OUT_BOUNCE.calc(1 - t); }
},
OUT_BOUNCE {
@Override
public float calc(float t) {
if (t < 0.36363636f)
return 7.5625f * t * t;
else if (t < 0.72727273f) {
t = t - 0.54545454f;
return 7.5625f * t * t + 0.75f;
} else if (t < 0.90909091f) {
t = t - 0.81818182f;
return 7.5625f * t * t + 0.9375f;
} else {
t = t - 0.95454546f;
return 7.5625f * t * t + 0.984375f;
}
}
},
IN_OUT_BOUNCE {
@Override
public float calc(float t) {
if (t < 0.5f)
return IN_BOUNCE.calc(t * 2) * 0.5f;
return OUT_BOUNCE.calc(t * 2 - 1) * 0.5f + 0.5f;
}
},
/* Elastic */
IN_ELASTIC {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
float period = 0.3f;
t = t - 1;
return -((float) Math.pow(2, 10 * t) * (float) Math.sin(((t - period / 4) * (Math.PI * 2)) / period));
}
},
OUT_ELASTIC {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
float period = 0.3f;
return (float) Math.pow(2, -10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period) + 1;
}
},
IN_OUT_ELASTIC {
@Override
public float calc(float t) {
if (t == 0 || t == 1)
return t;
float period = 0.44999996f;
t = t * 2 - 1;
if (t < 0)
return -0.5f * ((float) Math.pow(2, 10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period));
return (float) Math.pow(2, -10 * t) * (float) Math.sin((t - period / 4) * (Math.PI * 2) / period) * 0.5f + 1;
}
};
/** Overshoot constant for "back" easings. */
private static final float OVERSHOOT = 1.70158f;
/**
* Calculates a new {@code t} value using the animation equation.
* @param t the raw {@code t} value [0,1]
* @return the new {@code t} value [0,1]
*/
public abstract float calc(float t);
}

View File

@@ -38,6 +38,7 @@ import org.newdawn.slick.opengl.TextureImpl;
import org.newdawn.slick.opengl.pbuffer.GraphicsFactory;
import org.newdawn.slick.opengl.renderer.Renderer;
import org.newdawn.slick.opengl.renderer.SGL;
import org.newdawn.slick.util.FastTrig;
import org.newdawn.slick.util.Log;
/**
@@ -104,7 +105,7 @@ public class Image implements Renderable {
/** The colours for each of the corners */
protected Color[] corners;
/** The OpenGL max filter */
private int filter = SGL.GL_LINEAR;
private int filter = FILTER_LINEAR;
/** True if the image should be flipped vertically */
private boolean flipped;
@@ -562,7 +563,7 @@ public class Image implements Renderable {
* @param y The y coordinate to place the image's center at
*/
public void drawCentered(float x, float y) {
draw(x-(getWidth()/2),y-(getHeight()/2));
draw(x - (getWidth() / 2f), y - (getHeight() / 2f));
}
/**
@@ -595,11 +596,22 @@ public class Image implements Renderable {
* @param y The y location to draw the image at
* @param filter The color to filter with when drawing
*/
@Override
public void draw(float x, float y, Color filter) {
init();
draw(x,y,width,height, filter);
}
/**
* Draw this image as part of a collection of images
*
* @param x The x location to draw the image at
* @param y The y location to draw the image at
*/
public void drawEmbedded(float x,float y) {
drawEmbedded(x, y, getWidth(), getHeight());
}
/**
* Draw this image as part of a collection of images
*
@@ -719,6 +731,7 @@ public class Image implements Renderable {
* @param height
* The height to render the image at
*/
@Override
public void draw(float x,float y,float width,float height) {
init();
draw(x,y,width,height,Color.white);
@@ -797,6 +810,7 @@ public class Image implements Renderable {
* @param height The height to render the image at
* @param filter The color to filter with while drawing
*/
@Override
public void draw(float x,float y,float width,float height,Color filter) {
if (alpha != 1) {
if (filter == null) {
@@ -1159,6 +1173,83 @@ public class Image implements Renderable {
newTextureOffsetY);
GL.glVertex3f((x + mywidth),y, 0.0f);
}
/**
* Unlike the other drawEmbedded methods, this allows for the embedded image
* to be rotated. This is done by applying a rotation transform to each
* vertex of the image. This ignores getRotation but depends on the
* center x/y (scaled accordingly to the new width/height).
*
* @param x the x to render the image at
* @param y the y to render the image at
* @param width the new width to render the image
* @param height the new height to render the image
* @param rotation the rotation to render the image, using getCenterOfRotationX/Y
*
* @author davedes
*/
public void drawEmbedded(float x, float y, float width, float height, float rotation) {
if (rotation==0) {
drawEmbedded(x, y, width, height);
return;
}
init();
float scaleX = width/this.width;
float scaleY = height/this.height;
float cx = getCenterOfRotationX()*scaleX;
float cy = getCenterOfRotationY()*scaleY;
float p1x = -cx;
float p1y = -cy;
float p2x = width - cx;
float p2y = -cy;
float p3x = width - cx;
float p3y = height - cy;
float p4x = -cx;
float p4y = height - cy;
double rad = Math.toRadians(rotation);
final float cos = (float) FastTrig.cos(rad);
final float sin = (float) FastTrig.sin(rad);
float tx = getTextureOffsetX();
float ty = getTextureOffsetY();
float tw = getTextureWidth();
float th = getTextureHeight();
float x1 = (cos * p1x - sin * p1y) + cx; // TOP LEFT
float y1 = (sin * p1x + cos * p1y) + cy;
float x2 = (cos * p4x - sin * p4y) + cx; // BOTTOM LEFT
float y2 = (sin * p4x + cos * p4y) + cy;
float x3 = (cos * p3x - sin * p3y) + cx; // BOTTOM RIGHT
float y3 = (sin * p3x + cos * p3y) + cy;
float x4 = (cos * p2x - sin * p2y) + cx; // TOP RIGHT
float y4 = (sin * p2x + cos * p2y) + cy;
if (corners == null) {
GL.glTexCoord2f(tx, ty);
GL.glVertex3f(x+x1, y+y1, 0);
GL.glTexCoord2f(tx, ty + th);
GL.glVertex3f(x+x2, y+y2, 0);
GL.glTexCoord2f(tx + tw, ty + th);
GL.glVertex3f(x+x3, y+y3, 0);
GL.glTexCoord2f(tx + tw, ty);
GL.glVertex3f(x+x4, y+y4, 0);
} else {
corners[TOP_LEFT].bind();
GL.glTexCoord2f(tx, ty);
GL.glVertex3f(x+x1, y+y1, 0);
corners[BOTTOM_LEFT].bind();
GL.glTexCoord2f(tx, ty + th);
GL.glVertex3f(x+x2, y+y2, 0);
corners[BOTTOM_RIGHT].bind();
GL.glTexCoord2f(tx + tw, ty + th);
GL.glVertex3f(x+x3, y+y3, 0);
corners[TOP_RIGHT].bind();
GL.glTexCoord2f(tx + tw, ty);
GL.glVertex3f(x+x4, y+y4, 0);
}
}
/**
* Draw the image in a warper rectangle. The effects this can
@@ -1457,7 +1548,7 @@ public class Image implements Renderable {
if (isDestroyed()) {
return;
}
flushPixelData();
destroyed = true;
texture.release();
GraphicsFactory.releaseGraphicsForImage(this);

View File

@@ -1078,10 +1078,10 @@ public class Input {
throw new SlickException("Unable to create controller - no jinput found - add jinput.jar to your classpath");
}
throw new SlickException("Unable to create controllers");
} catch (NoClassDefFoundError e) {
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
// forget it, no jinput availble
}
}
}
/**
* Notification from an event handle that an event has been consumed
@@ -1233,7 +1233,7 @@ public class Input {
}
while (Mouse.next()) {
if (Mouse.getEventButton() >= 0) {
if (Mouse.getEventButton() >= 0 && Mouse.getEventButton() < mousePressed.length) {
if (Mouse.getEventButtonState()) {
consumed = false;
mousePressed[Mouse.getEventButton()] = true;

View File

@@ -76,8 +76,9 @@ public class Mp3InputStream extends InputStream implements AudioInputStream {
/**
* Create a new stream to decode MP3 data.
* @param input the input stream from which to read the MP3 file
* @throws IOException failure to read the header from the input stream
*/
public Mp3InputStream(InputStream input) {
public Mp3InputStream(InputStream input) throws IOException {
decoder = new Decoder();
bitstream = new Bitstream(input);
try {
@@ -85,6 +86,10 @@ public class Mp3InputStream extends InputStream implements AudioInputStream {
} catch (BitstreamException e) {
Log.error(e);
}
if (header == null) {
close();
throw new IOException("Failed to read header from MP3 input stream.");
}
channels = (header.mode() == Header.SINGLE_CHANNEL) ? 1 : 2;
sampleRate = header.frequency();

Some files were not shown because too many files have changed in this diff Show More