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
6
.gitignore
vendored
@@ -15,11 +15,17 @@
|
|||||||
.metadata
|
.metadata
|
||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
|
.externalToolBuilders/
|
||||||
|
|
||||||
# IntelliJ
|
# IntelliJ
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
*.iws
|
*.iws
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
/target
|
/target
|
||||||
84
README.md
@@ -1,16 +1,18 @@
|
|||||||
# [opsu!](http://itdelatrisu.github.io/opsu/)
|
# [opsu!](http://itdelatrisu.github.io/opsu/)
|
||||||
**opsu!** is an unofficial open-source client for [osu!](https://osu.ppy.sh/),
|
**opsu!** is an unofficial open-source client for the rhythm game
|
||||||
a rhythm game based on popular commercial games such as *Ouendan* and
|
[osu!](https://osu.ppy.sh/). It is written in Java using
|
||||||
*Elite Beat Agents*. It is written in Java using [Slick2D](http://slick.ninjacave.com/)
|
[Slick2D](http://slick.ninjacave.com/) and [LWJGL](http://lwjgl.org/),
|
||||||
and [LWJGL](http://lwjgl.org/), wrappers around the OpenGL and OpenAL libraries.
|
wrappers around the OpenGL and OpenAL libraries.
|
||||||
|
|
||||||
opsu! runs on Windows, OS X, and Linux platforms. A [libGDX port](https://github.com/fluddokt/opsu)
|
opsu! runs on Windows, OS X, and Linux platforms.
|
||||||
additionally supports Android devices.
|
A [libGDX port](https://github.com/fluddokt/opsu) additionally supports Android
|
||||||
|
devices.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
Precompiled binaries for opsu! can be found on the
|
Precompiled binaries for opsu! can be found on the
|
||||||
[releases](https://github.com/itdelatrisu/opsu/releases) page, with the latest
|
[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
|
### Java Setup
|
||||||
The Java Runtime Environment (JRE) must be installed in order to run opsu!.
|
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
|
### Beatmaps
|
||||||
opsu! requires beatmaps to run, which are available for download on the
|
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! 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.
|
Beatmaps can also be downloaded directly through opsu! in the downloads menu.
|
||||||
|
|
||||||
If osu! is already installed, this application will attempt to load songs
|
If osu! is already installed, this application will attempt to load beatmaps
|
||||||
directly from the osu! program folder. Otherwise, place songs in the generated
|
directly from the osu! program folder. Otherwise, place beatmaps in the
|
||||||
`Songs` folder or set the `BeatmapDirectory` value in the generated
|
generated `Songs` folder or set the "BeatmapDirectory" value in the generated
|
||||||
configuration file to the path of the root song directory.
|
configuration file to the path of the root beatmap directory.
|
||||||
|
|
||||||
Note that beatmaps are typically delivered as OSZ files. These can be extracted
|
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.
|
folder if placed in the `SongPacks` directory.
|
||||||
|
|
||||||
### First Run
|
### 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
|
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
|
other game options can be accessed by clicking the "Other Options" button in
|
||||||
the song menu.
|
the song menu.
|
||||||
|
|
||||||
## Building
|
### Directory Structure
|
||||||
opsu! is distributed as a Maven project.
|
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`.
|
## Building
|
||||||
* To create a single executable JAR file, execute the Maven goal
|
opsu! is distributed as both a [Maven](https://maven.apache.org/) and
|
||||||
`install -Djar`. This will link the LWJGL native libraries using a
|
[Gradle](https://gradle.org/) project.
|
||||||
[modified version](https://github.com/itdelatrisu/JarSplicePlus) of
|
|
||||||
[JarSplice](http://ninjacave.com/jarsplice), which is included in the
|
### Maven
|
||||||
`tools` directory in both its original and modified forms. The resulting
|
Maven builds are built to the `target` directory.
|
||||||
file will be located in `target/opsu-${version}-runnable.jar`.
|
* 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
|
## Credits
|
||||||
This software was created by Jeffrey Han
|
This software was created by Jeffrey Han
|
||||||
|
|||||||
109
build.gradle
Normal 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
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
@@ -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
@@ -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
|
||||||
72
pom.xml
@@ -3,10 +3,13 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>itdelatrisu</groupId>
|
<groupId>itdelatrisu</groupId>
|
||||||
<artifactId>opsu</artifactId>
|
<artifactId>opsu</artifactId>
|
||||||
<version>0.9.0</version>
|
<version>0.11.0</version>
|
||||||
<properties>
|
<properties>
|
||||||
|
<version>${project.version}</version>
|
||||||
<timestamp>${maven.build.timestamp}</timestamp>
|
<timestamp>${maven.build.timestamp}</timestamp>
|
||||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||||
|
<mainClassName>itdelatrisu.opsu.Opsu</mainClassName>
|
||||||
|
<XDG>false</XDG>
|
||||||
</properties>
|
</properties>
|
||||||
<build>
|
<build>
|
||||||
<sourceDirectory>src</sourceDirectory>
|
<sourceDirectory>src</sourceDirectory>
|
||||||
@@ -67,30 +70,9 @@
|
|||||||
<skip>${jar}</skip>
|
<skip>${jar}</skip>
|
||||||
<executable>java</executable>
|
<executable>java</executable>
|
||||||
<arguments>
|
<arguments>
|
||||||
<argument>-Djava.library.path=${project.build.directory}/natives</argument>
|
|
||||||
<argument>-cp</argument>
|
<argument>-cp</argument>
|
||||||
<classpath />
|
<classpath />
|
||||||
<argument>itdelatrisu.opsu.Opsu</argument>
|
<argument>${mainClassName}</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>
|
|
||||||
</arguments>
|
</arguments>
|
||||||
</configuration>
|
</configuration>
|
||||||
</execution>
|
</execution>
|
||||||
@@ -98,15 +80,8 @@
|
|||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
<version>2.3</version>
|
<version>2.4.1</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<!--
|
|
||||||
<artifactSet>
|
|
||||||
<excludes>
|
|
||||||
<exclude>*:*:natives*</exclude>
|
|
||||||
</excludes>
|
|
||||||
</artifactSet>
|
|
||||||
-->
|
|
||||||
<filters>
|
<filters>
|
||||||
<filter>
|
<filter>
|
||||||
<!-- Overwritten classes -->
|
<!-- Overwritten classes -->
|
||||||
@@ -124,6 +99,14 @@
|
|||||||
</excludes>
|
</excludes>
|
||||||
</filter>
|
</filter>
|
||||||
</filters>
|
</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>
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
</configuration>
|
</configuration>
|
||||||
<executions>
|
<executions>
|
||||||
@@ -142,12 +125,24 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.lwjgl.lwjgl</groupId>
|
<groupId>org.lwjgl.lwjgl</groupId>
|
||||||
<artifactId>lwjgl</artifactId>
|
<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>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slick2d</groupId>
|
<groupId>org.slick2d</groupId>
|
||||||
<artifactId>slick2d-core</artifactId>
|
<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>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jcraft</groupId>
|
<groupId>org.jcraft</groupId>
|
||||||
@@ -197,17 +192,22 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.maven</groupId>
|
<groupId>org.apache.maven</groupId>
|
||||||
<artifactId>maven-artifact</artifactId>
|
<artifactId>maven-artifact</artifactId>
|
||||||
<version>3.0.3</version>
|
<version>3.3.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-compress</artifactId>
|
<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>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.jponge</groupId>
|
<groupId>com.github.jponge</groupId>
|
||||||
<artifactId>lzma-java</artifactId>
|
<artifactId>lzma-java</artifactId>
|
||||||
<version>1.2</version>
|
<version>1.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
BIN
res/bang.png
|
Before Width: | Height: | Size: 653 B |
BIN
res/chevron-down.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
res/chevron-right.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
res/download.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 114 KiB |
BIN
res/options-background.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
res/star.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
res/star2.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
res/update.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
@@ -1,3 +1,3 @@
|
|||||||
version=${pom.version}
|
version=${version}
|
||||||
file=https://github.com/itdelatrisu/opsu/releases/download/${pom.version}/opsu-${pom.version}.jar
|
file=https://github.com/itdelatrisu/opsu/releases/download/${version}/opsu-${version}.jar
|
||||||
build.date=${timestamp}
|
build.date=${timestamp}
|
||||||
1
settings.gradle
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = 'opsu'
|
||||||
@@ -21,6 +21,7 @@ package itdelatrisu.opsu;
|
|||||||
import itdelatrisu.opsu.audio.MusicController;
|
import itdelatrisu.opsu.audio.MusicController;
|
||||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||||
|
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
|
||||||
import itdelatrisu.opsu.downloads.DownloadList;
|
import itdelatrisu.opsu.downloads.DownloadList;
|
||||||
import itdelatrisu.opsu.downloads.Updater;
|
import itdelatrisu.opsu.downloads.Updater;
|
||||||
import itdelatrisu.opsu.render.CurveRenderState;
|
import itdelatrisu.opsu.render.CurveRenderState;
|
||||||
@@ -125,7 +126,7 @@ public class Container extends AppGameContainer {
|
|||||||
// reset image references
|
// reset image references
|
||||||
GameImage.clearReferences();
|
GameImage.clearReferences();
|
||||||
GameData.Grade.clearReferences();
|
GameData.Grade.clearReferences();
|
||||||
Beatmap.getBackgroundImageCache().clear();
|
Beatmap.clearBackgroundImageCache();
|
||||||
|
|
||||||
// prevent loading tracks from re-initializing OpenAL
|
// prevent loading tracks from re-initializing OpenAL
|
||||||
MusicController.reset();
|
MusicController.reset();
|
||||||
@@ -136,6 +137,11 @@ public class Container extends AppGameContainer {
|
|||||||
|
|
||||||
// delete OpenGL objects involved in the Curve rendering
|
// delete OpenGL objects involved in the Curve rendering
|
||||||
CurveRenderState.shutdown();
|
CurveRenderState.shutdown();
|
||||||
|
|
||||||
|
// destroy watch service
|
||||||
|
if (!Options.isWatchServiceEnabled())
|
||||||
|
BeatmapWatchService.destroy();
|
||||||
|
BeatmapWatchService.removeListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ package itdelatrisu.opsu;
|
|||||||
|
|
||||||
import java.awt.Cursor;
|
import java.awt.Cursor;
|
||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
@@ -44,12 +46,13 @@ public class ErrorHandler {
|
|||||||
/** Error popup description text. */
|
/** Error popup description text. */
|
||||||
private static final String
|
private static final String
|
||||||
desc = "opsu! has encountered an error.",
|
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. */
|
/** Error popup button options. */
|
||||||
private static final String[]
|
private static final String[]
|
||||||
options = {"View Error Log", "Close"},
|
optionsLog = {"View Error Log", "Close"},
|
||||||
optionsR = {"Send Report", "View Error Log", "Close"};
|
optionsReport = {"Send Report", "Close"},
|
||||||
|
optionsLogReport = {"Send Report", "View Error Log", "Close"};
|
||||||
|
|
||||||
/** Text area for Exception. */
|
/** Text area for Exception. */
|
||||||
private static final JTextArea textArea = new JTextArea(7, 30);
|
private static final JTextArea textArea = new JTextArea(7, 30);
|
||||||
@@ -59,6 +62,7 @@ public class ErrorHandler {
|
|||||||
textArea.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
|
textArea.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
|
||||||
textArea.setTabSize(2);
|
textArea.setTabSize(2);
|
||||||
textArea.setLineWrap(true);
|
textArea.setLineWrap(true);
|
||||||
|
textArea.setWrapStyleWord(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Scroll pane holding JTextArea. */
|
/** Scroll pane holding JTextArea. */
|
||||||
@@ -67,7 +71,7 @@ public class ErrorHandler {
|
|||||||
/** Error popup objects. */
|
/** Error popup objects. */
|
||||||
private static final Object[]
|
private static final Object[]
|
||||||
message = { desc, scroll },
|
message = { desc, scroll },
|
||||||
messageR = { descR, scroll };
|
messageReport = { descReport, scroll };
|
||||||
|
|
||||||
// This class should not be instantiated.
|
// This class should not be instantiated.
|
||||||
private ErrorHandler() {}
|
private ErrorHandler() {}
|
||||||
@@ -107,23 +111,72 @@ public class ErrorHandler {
|
|||||||
// display popup
|
// display popup
|
||||||
try {
|
try {
|
||||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
|
Desktop desktop = null;
|
||||||
|
boolean isBrowseSupported = false, isOpenSupported = false;
|
||||||
if (Desktop.isDesktopSupported()) {
|
if (Desktop.isDesktopSupported()) {
|
||||||
// try to open the log file and/or issues webpage
|
desktop = Desktop.getDesktop();
|
||||||
if (report) {
|
isBrowseSupported = desktop.isSupported(Desktop.Action.BROWSE);
|
||||||
// ask to report the error
|
isOpenSupported = desktop.isSupported(Desktop.Action.OPEN);
|
||||||
int n = JOptionPane.showOptionDialog(null, messageR, title,
|
}
|
||||||
|
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,
|
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
|
||||||
null, optionsR, optionsR[2]);
|
null, optionsLogReport, optionsLogReport[2]);
|
||||||
if (n == 0) {
|
if (n == 0)
|
||||||
// auto-fill debug information
|
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, optionsLog, optionsLog[1]);
|
||||||
|
if (n == 0)
|
||||||
|
desktop.open(Options.LOG_FILE);
|
||||||
|
}
|
||||||
|
} else { // display error only
|
||||||
|
JOptionPane.showMessageDialog(null, report ? messageReport : message,
|
||||||
|
title, JOptionPane.ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
} catch (Exception 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();
|
String issueTitle = (error != null) ? error : e.getMessage();
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
try {
|
||||||
|
// read version and build date from version file, if possible
|
||||||
Properties props = new Properties();
|
Properties props = new Properties();
|
||||||
props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE));
|
props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE));
|
||||||
String version = props.getProperty("version");
|
String version = props.getProperty("version");
|
||||||
if (version != null && !version.equals("${pom.version}")) {
|
if (version != null && !version.equals("${pom.version}")) {
|
||||||
sb.append("**Version:** ");
|
sb.append("**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');
|
sb.append('\n');
|
||||||
}
|
}
|
||||||
String timestamp = props.getProperty("build.date");
|
String timestamp = props.getProperty("build.date");
|
||||||
@@ -133,6 +186,9 @@ public class ErrorHandler {
|
|||||||
sb.append(timestamp);
|
sb.append(timestamp);
|
||||||
sb.append('\n');
|
sb.append('\n');
|
||||||
}
|
}
|
||||||
|
} catch (IOException e1) {
|
||||||
|
Log.warn("Could not read version file.", e1);
|
||||||
|
}
|
||||||
sb.append("**OS:** ");
|
sb.append("**OS:** ");
|
||||||
sb.append(System.getProperty("os.name"));
|
sb.append(System.getProperty("os.name"));
|
||||||
sb.append(" (");
|
sb.append(" (");
|
||||||
@@ -152,27 +208,15 @@ public class ErrorHandler {
|
|||||||
sb.append(trace);
|
sb.append(trace);
|
||||||
sb.append("```");
|
sb.append("```");
|
||||||
}
|
}
|
||||||
URI uri = URI.create(String.format(Options.ISSUES_URL,
|
|
||||||
|
// return auto-filled URI
|
||||||
|
try {
|
||||||
|
return URI.create(String.format(Options.ISSUES_URL,
|
||||||
URLEncoder.encode(issueTitle, "UTF-8"),
|
URLEncoder.encode(issueTitle, "UTF-8"),
|
||||||
URLEncoder.encode(sb.toString(), "UTF-8")));
|
URLEncoder.encode(sb.toString(), "UTF-8")));
|
||||||
Desktop.getDesktop().browse(uri);
|
} catch (UnsupportedEncodingException e1) {
|
||||||
} else if (n == 1)
|
Log.warn("URLEncoder failed to encode the auto-filled issue report URL.");
|
||||||
Desktop.getDesktop().open(Options.LOG_FILE);
|
return URI.create(String.format(Options.ISSUES_URL, "", ""));
|
||||||
} else {
|
|
||||||
// don't report the error
|
|
||||||
int n = JOptionPane.showOptionDialog(null, message, title,
|
|
||||||
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
|
|
||||||
null, options, options[1]);
|
|
||||||
if (n == 0)
|
|
||||||
Desktop.getDesktop().open(Options.LOG_FILE);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// display error only
|
|
||||||
JOptionPane.showMessageDialog(null, report ? messageR : message,
|
|
||||||
title, JOptionPane.ERROR_MESSAGE);
|
|
||||||
}
|
|
||||||
} catch (Exception e1) {
|
|
||||||
Log.error("Error opening crash popup.", e1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,12 @@ import itdelatrisu.opsu.beatmap.Beatmap;
|
|||||||
import itdelatrisu.opsu.beatmap.HitObject;
|
import itdelatrisu.opsu.beatmap.HitObject;
|
||||||
import itdelatrisu.opsu.downloads.Updater;
|
import itdelatrisu.opsu.downloads.Updater;
|
||||||
import itdelatrisu.opsu.objects.curves.Curve;
|
import itdelatrisu.opsu.objects.curves.Curve;
|
||||||
|
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||||
import itdelatrisu.opsu.replay.Replay;
|
import itdelatrisu.opsu.replay.Replay;
|
||||||
import itdelatrisu.opsu.replay.ReplayFrame;
|
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.io.File;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
@@ -87,7 +91,7 @@ public class GameData {
|
|||||||
D (GameImage.RANKING_D, GameImage.RANKING_D_SMALL);
|
D (GameImage.RANKING_D, GameImage.RANKING_D_SMALL);
|
||||||
|
|
||||||
/** GameImages associated with this grade (large and small sizes). */
|
/** 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. */
|
/** Large-size image scaled for use in song menu. */
|
||||||
private Image menuImage;
|
private Image menuImage;
|
||||||
@@ -129,7 +133,7 @@ public class GameData {
|
|||||||
return menuImage;
|
return menuImage;
|
||||||
|
|
||||||
Image img = getSmallImage();
|
Image img = getSmallImage();
|
||||||
if (!small.hasSkinImage()) // save default image only
|
if (!small.hasBeatmapSkinImage()) // save default image only
|
||||||
this.menuImage = img;
|
this.menuImage = img;
|
||||||
return img;
|
return img;
|
||||||
}
|
}
|
||||||
@@ -199,14 +203,14 @@ public class GameData {
|
|||||||
*/
|
*/
|
||||||
private class HitErrorInfo {
|
private class HitErrorInfo {
|
||||||
/** The correct hit time. */
|
/** The correct hit time. */
|
||||||
private int time;
|
private final int time;
|
||||||
|
|
||||||
/** The coordinates of the hit. */
|
/** The coordinates of the hit. */
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private int x, y;
|
private final int x, y;
|
||||||
|
|
||||||
/** The difference between the correct and actual hit times. */
|
/** The difference between the correct and actual hit times. */
|
||||||
private int timeDiff;
|
private final int timeDiff;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
@@ -232,29 +236,32 @@ public class GameData {
|
|||||||
/** Hit result helper class. */
|
/** Hit result helper class. */
|
||||||
private class HitObjectResult {
|
private class HitObjectResult {
|
||||||
/** Object start time. */
|
/** Object start time. */
|
||||||
public int time;
|
public final int time;
|
||||||
|
|
||||||
/** Hit result. */
|
/** Hit result. */
|
||||||
public int result;
|
public final int result;
|
||||||
|
|
||||||
/** Object coordinates. */
|
/** Object coordinates. */
|
||||||
public float x, y;
|
public final float x, y;
|
||||||
|
|
||||||
/** Combo color. */
|
/** Combo color. */
|
||||||
public Color color;
|
public final Color color;
|
||||||
|
|
||||||
/** The type of the hit object. */
|
/** 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). */
|
/** Alpha level (for fading out). */
|
||||||
public float alpha = 1f;
|
public float alpha = 1f;
|
||||||
|
|
||||||
/** Slider curve. */
|
|
||||||
public Curve curve;
|
|
||||||
|
|
||||||
/** Whether or not to expand when animating. */
|
|
||||||
public boolean expand;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param time the result's starting track position
|
* @param time the result's starting track position
|
||||||
@@ -262,11 +269,13 @@ public class GameData {
|
|||||||
* @param x the center x coordinate
|
* @param x the center x coordinate
|
||||||
* @param y the center y coordinate
|
* @param y the center y coordinate
|
||||||
* @param color the color of the hit object
|
* @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 curve the slider curve (or null if not applicable)
|
||||||
* @param expand whether or not the hit result animation should expand (if 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,
|
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.time = time;
|
||||||
this.result = result;
|
this.result = result;
|
||||||
this.x = x;
|
this.x = x;
|
||||||
@@ -275,6 +284,7 @@ public class GameData {
|
|||||||
this.hitResultType = hitResultType;
|
this.hitResultType = hitResultType;
|
||||||
this.curve = curve;
|
this.curve = curve;
|
||||||
this.expand = expand;
|
this.expand = expand;
|
||||||
|
this.hideResult = hideResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +326,7 @@ public class GameData {
|
|||||||
private Replay replay;
|
private Replay replay;
|
||||||
|
|
||||||
/** Whether this object is used for gameplay (true) or score viewing (false). */
|
/** Whether this object is used for gameplay (true) or score viewing (false). */
|
||||||
private boolean gameplay;
|
private boolean isGameplay;
|
||||||
|
|
||||||
/** Container dimensions. */
|
/** Container dimensions. */
|
||||||
private int width, height;
|
private int width, height;
|
||||||
@@ -329,7 +339,7 @@ public class GameData {
|
|||||||
public GameData(int width, int height) {
|
public GameData(int width, int height) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.gameplay = true;
|
this.isGameplay = true;
|
||||||
|
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
@@ -345,7 +355,7 @@ public class GameData {
|
|||||||
public GameData(ScoreData s, int width, int height) {
|
public GameData(ScoreData s, int width, int height) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
this.gameplay = false;
|
this.isGameplay = false;
|
||||||
|
|
||||||
this.scoreData = s;
|
this.scoreData = s;
|
||||||
this.score = s.score;
|
this.score = s.score;
|
||||||
@@ -400,8 +410,8 @@ public class GameData {
|
|||||||
// gameplay-specific images
|
// gameplay-specific images
|
||||||
if (isGameplay()) {
|
if (isGameplay()) {
|
||||||
// combo burst images
|
// combo burst images
|
||||||
if (GameImage.COMBO_BURST.hasSkinImages() ||
|
if (GameImage.COMBO_BURST.hasBeatmapSkinImages() ||
|
||||||
(!GameImage.COMBO_BURST.hasSkinImage() && GameImage.COMBO_BURST.getImages() != null))
|
(!GameImage.COMBO_BURST.hasBeatmapSkinImage() && GameImage.COMBO_BURST.getImages() != null))
|
||||||
comboBurstImages = GameImage.COMBO_BURST.getImages();
|
comboBurstImages = GameImage.COMBO_BURST.getImages();
|
||||||
else
|
else
|
||||||
comboBurstImages = new Image[]{ GameImage.COMBO_BURST.getImage() };
|
comboBurstImages = new Image[]{ GameImage.COMBO_BURST.getImage() };
|
||||||
@@ -474,6 +484,7 @@ public class GameData {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the array of hit result offsets.
|
* 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; }
|
public void setHitResultOffset(int[] hitResultOffset) { this.hitResultOffset = hitResultOffset; }
|
||||||
|
|
||||||
@@ -614,7 +625,7 @@ public class GameData {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// lead-in time (yellow)
|
// lead-in time (yellow)
|
||||||
g.setColor(Utils.COLOR_YELLOW_ALPHA);
|
g.setColor(Colors.YELLOW_ALPHA);
|
||||||
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
|
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
|
||||||
-90 + (int) (360f * trackPosition / firstObjectTime), -90
|
-90 + (int) (360f * trackPosition / firstObjectTime), -90
|
||||||
);
|
);
|
||||||
@@ -651,27 +662,27 @@ public class GameData {
|
|||||||
float hitErrorY = height / uiScale - margin - 10;
|
float hitErrorY = height / uiScale - margin - 10;
|
||||||
float barY = (hitErrorY - 3) * uiScale, barHeight = 6 * uiScale;
|
float barY = (hitErrorY - 3) * uiScale, barHeight = 6 * uiScale;
|
||||||
float tickY = (hitErrorY - 10) * uiScale, tickHeight = 20 * uiScale;
|
float tickY = (hitErrorY - 10) * uiScale, tickHeight = 20 * uiScale;
|
||||||
float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a;
|
float oldAlphaBlack = Colors.BLACK_ALPHA.a;
|
||||||
Utils.COLOR_BLACK_ALPHA.a = hitErrorAlpha;
|
Colors.BLACK_ALPHA.a = hitErrorAlpha;
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, tickY,
|
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, tickY,
|
||||||
(hitResultOffset[HIT_50] * 2) * uiScale, tickHeight);
|
(hitResultOffset[HIT_50] * 2) * uiScale, tickHeight);
|
||||||
Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack;
|
Colors.BLACK_ALPHA.a = oldAlphaBlack;
|
||||||
Utils.COLOR_LIGHT_ORANGE.a = hitErrorAlpha;
|
Colors.LIGHT_ORANGE.a = hitErrorAlpha;
|
||||||
g.setColor(Utils.COLOR_LIGHT_ORANGE);
|
g.setColor(Colors.LIGHT_ORANGE);
|
||||||
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, barY,
|
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, barY,
|
||||||
(hitResultOffset[HIT_50] * 2) * uiScale, barHeight);
|
(hitResultOffset[HIT_50] * 2) * uiScale, barHeight);
|
||||||
Utils.COLOR_LIGHT_ORANGE.a = 1f;
|
Colors.LIGHT_ORANGE.a = 1f;
|
||||||
Utils.COLOR_LIGHT_GREEN.a = hitErrorAlpha;
|
Colors.LIGHT_GREEN.a = hitErrorAlpha;
|
||||||
g.setColor(Utils.COLOR_LIGHT_GREEN);
|
g.setColor(Colors.LIGHT_GREEN);
|
||||||
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_100]) * uiScale, barY,
|
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_100]) * uiScale, barY,
|
||||||
(hitResultOffset[HIT_100] * 2) * uiScale, barHeight);
|
(hitResultOffset[HIT_100] * 2) * uiScale, barHeight);
|
||||||
Utils.COLOR_LIGHT_GREEN.a = 1f;
|
Colors.LIGHT_GREEN.a = 1f;
|
||||||
Utils.COLOR_LIGHT_BLUE.a = hitErrorAlpha;
|
Colors.LIGHT_BLUE.a = hitErrorAlpha;
|
||||||
g.setColor(Utils.COLOR_LIGHT_BLUE);
|
g.setColor(Colors.LIGHT_BLUE);
|
||||||
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_300]) * uiScale, barY,
|
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_300]) * uiScale, barY,
|
||||||
(hitResultOffset[HIT_300] * 2) * uiScale, barHeight);
|
(hitResultOffset[HIT_300] * 2) * uiScale, barHeight);
|
||||||
Utils.COLOR_LIGHT_BLUE.a = 1f;
|
Colors.LIGHT_BLUE.a = 1f;
|
||||||
white.a = hitErrorAlpha;
|
white.a = hitErrorAlpha;
|
||||||
g.setColor(white);
|
g.setColor(white);
|
||||||
g.fillRect((hitErrorX - 1.5f) * uiScale, tickY, 3 * uiScale, tickHeight);
|
g.fillRect((hitErrorX - 1.5f) * uiScale, tickY, 3 * uiScale, tickHeight);
|
||||||
@@ -830,16 +841,16 @@ public class GameData {
|
|||||||
|
|
||||||
// header
|
// header
|
||||||
Image rankingTitle = GameImage.RANKING_TITLE.getImage();
|
Image rankingTitle = GameImage.RANKING_TITLE.getImage();
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
g.fillRect(0, 0, width, 100 * uiScale);
|
g.fillRect(0, 0, width, 100 * uiScale);
|
||||||
rankingTitle.draw((width * 0.97f) - rankingTitle.getWidth(), 0);
|
rankingTitle.draw((width * 0.97f) - rankingTitle.getWidth(), 0);
|
||||||
float marginX = width * 0.01f, marginY = height * 0.002f;
|
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);
|
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.format("Beatmap by %s", beatmap.creator), Color.white);
|
||||||
String player = (scoreData.playerName == null) ? "" : String.format(" by %s", scoreData.playerName);
|
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);
|
String.format("Played%s on %s.", player, scoreData.getTimeString()), Color.white);
|
||||||
|
|
||||||
// mod icons
|
// mod icons
|
||||||
@@ -872,7 +883,7 @@ public class GameData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hit lighting
|
// 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) {
|
hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) {
|
||||||
// TODO: add particle system
|
// TODO: add particle system
|
||||||
Image lighting = GameImage.LIGHTING.getImage();
|
Image lighting = GameImage.LIGHTING.getImage();
|
||||||
@@ -885,27 +896,25 @@ public class GameData {
|
|||||||
hitResult.hitResultType == HitObjectType.CIRCLE ||
|
hitResult.hitResultType == HitObjectType.CIRCLE ||
|
||||||
hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
|
hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
|
||||||
hitResult.hitResultType == HitObjectType.SLIDER_LAST)) {
|
hitResult.hitResultType == HitObjectType.SLIDER_LAST)) {
|
||||||
float scale = (!hitResult.expand) ? 1f : Utils.easeOut(
|
float progress = AnimationEquation.OUT_CUBIC.calc(
|
||||||
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME),
|
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
|
||||||
1f, HITCIRCLE_ANIM_SCALE - 1f, HITCIRCLE_FADE_TIME
|
float scale = (!hitResult.expand) ? 1f : 1f + (HITCIRCLE_ANIM_SCALE - 1f) * progress;
|
||||||
);
|
float alpha = 1f - progress;
|
||||||
float alpha = Utils.easeOut(
|
|
||||||
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME),
|
|
||||||
1f, -1f, HITCIRCLE_FADE_TIME
|
|
||||||
);
|
|
||||||
|
|
||||||
// slider curve
|
// slider curve
|
||||||
if (hitResult.curve != null) {
|
if (hitResult.curve != null) {
|
||||||
float oldWhiteAlpha = Utils.COLOR_WHITE_FADE.a;
|
float oldWhiteAlpha = Colors.WHITE_FADE.a;
|
||||||
float oldColorAlpha = hitResult.color.a;
|
float oldColorAlpha = hitResult.color.a;
|
||||||
Utils.COLOR_WHITE_FADE.a = alpha;
|
Colors.WHITE_FADE.a = alpha;
|
||||||
hitResult.color.a = alpha;
|
hitResult.color.a = alpha;
|
||||||
hitResult.curve.draw(hitResult.color);
|
hitResult.curve.draw(hitResult.color);
|
||||||
Utils.COLOR_WHITE_FADE.a = oldWhiteAlpha;
|
Colors.WHITE_FADE.a = oldWhiteAlpha;
|
||||||
hitResult.color.a = oldColorAlpha;
|
hitResult.color.a = oldColorAlpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
// hit circles
|
// hit circles
|
||||||
|
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 scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
|
||||||
Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
|
Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
|
||||||
scaledHitCircle.setAlpha(alpha);
|
scaledHitCircle.setAlpha(alpha);
|
||||||
@@ -913,19 +922,19 @@ public class GameData {
|
|||||||
scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
|
scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
|
||||||
scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
|
scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// hit result
|
// hit result
|
||||||
if (hitResult.hitResultType == HitObjectType.CIRCLE ||
|
if (!hitResult.hideResult && (
|
||||||
|
hitResult.hitResultType == HitObjectType.CIRCLE ||
|
||||||
hitResult.hitResultType == HitObjectType.SPINNER ||
|
hitResult.hitResultType == HitObjectType.SPINNER ||
|
||||||
hitResult.curve != null) {
|
hitResult.curve != null)) {
|
||||||
float scale = Utils.easeBounce(
|
float scaleProgress = AnimationEquation.IN_OUT_BOUNCE.calc(
|
||||||
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME),
|
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME) / HITCIRCLE_TEXT_BOUNCE_TIME);
|
||||||
1f, HITCIRCLE_TEXT_ANIM_SCALE - 1f, HITCIRCLE_TEXT_BOUNCE_TIME
|
float scale = 1f + (HITCIRCLE_TEXT_ANIM_SCALE - 1f) * scaleProgress;
|
||||||
);
|
float fadeProgress = AnimationEquation.OUT_CUBIC.calc(
|
||||||
float alpha = Utils.easeOut(
|
(float) Utils.clamp((trackPosition - hitResult.time) - HITCIRCLE_FADE_TIME, 0, HITCIRCLE_TEXT_FADE_TIME) / HITCIRCLE_TEXT_FADE_TIME);
|
||||||
Utils.clamp((trackPosition - hitResult.time) - HITCIRCLE_FADE_TIME, 0, HITCIRCLE_TEXT_FADE_TIME),
|
float alpha = 1f - fadeProgress;
|
||||||
1f, -1f, HITCIRCLE_TEXT_FADE_TIME
|
|
||||||
);
|
|
||||||
Image scaledHitResult = hitResults[hitResult.result].getScaledCopy(scale);
|
Image scaledHitResult = hitResults[hitResult.result].getScaledCopy(scale);
|
||||||
scaledHitResult.setAlpha(alpha);
|
scaledHitResult.setAlpha(alpha);
|
||||||
scaledHitResult.drawCentered(hitResult.x, hitResult.y);
|
scaledHitResult.drawCentered(hitResult.x, hitResult.y);
|
||||||
@@ -1037,10 +1046,13 @@ public class GameData {
|
|||||||
* or {@code Grade.NULL} if no objects have been processed.
|
* or {@code Grade.NULL} if no objects have been processed.
|
||||||
*/
|
*/
|
||||||
private Grade getGrade() {
|
private Grade getGrade() {
|
||||||
|
boolean silver = (scoreData == null) ?
|
||||||
|
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive()) :
|
||||||
|
(scoreData.mods & (GameMod.HIDDEN.getBit() | GameMod.FLASHLIGHT.getBit())) != 0;
|
||||||
return getGrade(
|
return getGrade(
|
||||||
hitResultCount[HIT_300], hitResultCount[HIT_100],
|
hitResultCount[HIT_300], hitResultCount[HIT_100],
|
||||||
hitResultCount[HIT_50], hitResultCount[HIT_MISS],
|
hitResultCount[HIT_50], hitResultCount[HIT_MISS],
|
||||||
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive())
|
silver
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,7 +1191,7 @@ public class GameData {
|
|||||||
switch (result) {
|
switch (result) {
|
||||||
case HIT_SLIDER30:
|
case HIT_SLIDER30:
|
||||||
hitValue = 30;
|
hitValue = 30;
|
||||||
changeHealth(1f);
|
changeHealth(2f);
|
||||||
SoundController.playHitSound(
|
SoundController.playHitSound(
|
||||||
hitObject.getEdgeHitSoundType(repeat),
|
hitObject.getEdgeHitSoundType(repeat),
|
||||||
hitObject.getSampleSet(repeat),
|
hitObject.getSampleSet(repeat),
|
||||||
@@ -1187,6 +1199,7 @@ public class GameData {
|
|||||||
break;
|
break;
|
||||||
case HIT_SLIDER10:
|
case HIT_SLIDER10:
|
||||||
hitValue = 10;
|
hitValue = 10;
|
||||||
|
changeHealth(1f);
|
||||||
SoundController.playHitSound(HitSound.SLIDERTICK);
|
SoundController.playHitSound(HitSound.SLIDERTICK);
|
||||||
break;
|
break;
|
||||||
case HIT_MISS:
|
case HIT_MISS:
|
||||||
@@ -1204,7 +1217,7 @@ public class GameData {
|
|||||||
if (!Options.isPerfectHitBurstEnabled())
|
if (!Options.isPerfectHitBurstEnabled())
|
||||||
; // hide perfect hit results
|
; // hide perfect hit results
|
||||||
else
|
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++;
|
fullObjectCount++;
|
||||||
}
|
}
|
||||||
@@ -1360,20 +1373,18 @@ public class GameData {
|
|||||||
int hitResult = handleHitResult(time, result, x, y, color, end, hitObject,
|
int hitResult = handleHitResult(time, result, x, y, color, end, hitObject,
|
||||||
hitResultType, repeat, (curve != null && !sliderHeldToEnd));
|
hitResultType, repeat, (curve != null && !sliderHeldToEnd));
|
||||||
|
|
||||||
if ((hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled())
|
if (hitResult == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
|
||||||
; // hide perfect hit results
|
return; // "relax" and "autopilot" mods: hide misses
|
||||||
else if (hitResult == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
|
|
||||||
; // "relax" and "autopilot" mods: hide misses
|
boolean hideResult = (hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled();
|
||||||
else {
|
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand, hideResult));
|
||||||
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand));
|
|
||||||
|
|
||||||
// sliders: add the other curve endpoint for the hit animation
|
// sliders: add the other curve endpoint for the hit animation
|
||||||
if (curve != null) {
|
if (curve != null) {
|
||||||
boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST);
|
boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST);
|
||||||
float[] p = curve.pointAt((isFirst) ? 1f : 0f);
|
Vec2f p = curve.pointAt((isFirst) ? 1f : 0f);
|
||||||
HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST;
|
HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST;
|
||||||
hitResultList.add(new HitObjectResult(time, hitResult, p[0], p[1], color, type, null, expand));
|
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.
|
* Returns whether or not this object is used for gameplay.
|
||||||
* @return true if gameplay, false if score viewing
|
* @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.
|
* Adds the hit into the list of hit error information.
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package itdelatrisu.opsu;
|
package itdelatrisu.opsu;
|
||||||
|
|
||||||
|
import itdelatrisu.opsu.ui.Fonts;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -35,8 +37,8 @@ public enum GameImage {
|
|||||||
CURSOR ("cursor", "png"),
|
CURSOR ("cursor", "png"),
|
||||||
CURSOR_MIDDLE ("cursormiddle", "png"),
|
CURSOR_MIDDLE ("cursormiddle", "png"),
|
||||||
CURSOR_TRAIL ("cursortrail", "png"),
|
CURSOR_TRAIL ("cursortrail", "png"),
|
||||||
CURSOR_OLD ("cursor2", "png", false, false),
|
CURSOR_OLD ("cursor2", "png", false, false), // custom
|
||||||
CURSOR_TRAIL_OLD ("cursortrail2", "png", false, false),
|
CURSOR_TRAIL_OLD ("cursortrail2", "png", false, false), // custom
|
||||||
|
|
||||||
// Game
|
// Game
|
||||||
SECTION_PASS ("section-pass", "png"),
|
SECTION_PASS ("section-pass", "png"),
|
||||||
@@ -251,14 +253,14 @@ public enum GameImage {
|
|||||||
MENU_MUSICNOTE ("music-note", "png", false, false) {
|
MENU_MUSICNOTE ("music-note", "png", false, false) {
|
||||||
@Override
|
@Override
|
||||||
protected Image process_sub(Image img, int w, int h) {
|
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);
|
return img.getScaledCopy(r, r);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MENU_LOADER ("loader", "png", false, false) {
|
MENU_LOADER ("loader", "png", false, false) {
|
||||||
@Override
|
@Override
|
||||||
protected Image process_sub(Image img, int w, int h) {
|
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);
|
return img.getScaledCopy(r / 48f);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -290,12 +292,25 @@ public enum GameImage {
|
|||||||
MENU_BUTTON_MID ("button-middle", "png", false, false),
|
MENU_BUTTON_MID ("button-middle", "png", false, false),
|
||||||
MENU_BUTTON_LEFT ("button-left", "png", false, false),
|
MENU_BUTTON_LEFT ("button-left", "png", false, false),
|
||||||
MENU_BUTTON_RIGHT ("button-right", "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 Player Buttons
|
||||||
MUSIC_PLAY ("music-play", "png", false, false),
|
MUSIC_PLAY ("music-play", "png", false, false),
|
||||||
MUSIC_PAUSE ("music-pause", "png", false, false),
|
MUSIC_PAUSE ("music-pause", "png", false, false),
|
||||||
MUSIC_NEXT ("music-next", "png", false, false),
|
MUSIC_NEXT ("music-next", "png", false, false),
|
||||||
MUSIC_PREVIOUS ("music-previous", "png", false, false),
|
MUSIC_PREVIOUS ("music-previous", "png", false, false),
|
||||||
|
|
||||||
DOWNLOADS ("downloads", "png", false, false) {
|
DOWNLOADS ("downloads", "png", false, false) {
|
||||||
@Override
|
@Override
|
||||||
protected Image process_sub(Image img, int w, int h) {
|
protected Image process_sub(Image img, int w, int h) {
|
||||||
@@ -312,7 +327,7 @@ public enum GameImage {
|
|||||||
DELETE ("delete", "png", false, false) {
|
DELETE ("delete", "png", false, false) {
|
||||||
@Override
|
@Override
|
||||||
protected Image process_sub(Image img, int w, int h) {
|
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);
|
return img.getScaledCopy(lineHeight, lineHeight);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -328,10 +343,16 @@ public enum GameImage {
|
|||||||
return img.getScaledCopy((h / 17f) / img.getHeight());
|
return img.getScaledCopy((h / 17f) / img.getHeight());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
BANG ("bang", "png", false, false) {
|
DOWNLOAD ("download", "png", false, false) {
|
||||||
@Override
|
@Override
|
||||||
protected Image process_sub(Image img, int w, int h) {
|
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) {
|
OPTIONS_BG ("options-background", "png|jpg", false, true) {
|
||||||
@@ -341,6 +362,8 @@ public enum GameImage {
|
|||||||
return img.getScaledCopy(w, h);
|
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?)
|
// TODO: ensure this image hasn't been modified (checksum?)
|
||||||
ALPHA_MAP ("alpha", "png", false, false);
|
ALPHA_MAP ("alpha", "png", false, false);
|
||||||
@@ -351,22 +374,22 @@ public enum GameImage {
|
|||||||
IMG_JPG = 2;
|
IMG_JPG = 2;
|
||||||
|
|
||||||
/** The file name. */
|
/** The file name. */
|
||||||
private String filename;
|
private final String filename;
|
||||||
|
|
||||||
/** The formatted file name string (for loading multiple images). */
|
/** The formatted file name string (for loading multiple images). */
|
||||||
private String filenameFormat;
|
private String filenameFormat;
|
||||||
|
|
||||||
/** Image file type. */
|
/** Image file type. */
|
||||||
private byte type;
|
private final byte type;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the image is skinnable by a beatmap.
|
* Whether or not the image is skinnable by a beatmap.
|
||||||
* These images are typically related to gameplay.
|
* 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. */
|
/** Whether or not to preload the image when the program starts. */
|
||||||
private boolean preload;
|
private final boolean preload;
|
||||||
|
|
||||||
/** The default image. */
|
/** The default image. */
|
||||||
private Image defaultImage;
|
private Image defaultImage;
|
||||||
@@ -374,6 +397,9 @@ public enum GameImage {
|
|||||||
/** The default image array. */
|
/** The default image array. */
|
||||||
private Image[] defaultImages;
|
private Image[] defaultImages;
|
||||||
|
|
||||||
|
/** Whether the image is currently skinned by a game skin. */
|
||||||
|
private boolean isSkinned = false;
|
||||||
|
|
||||||
/** The beatmap skin image (optional, temporary). */
|
/** The beatmap skin image (optional, temporary). */
|
||||||
private Image skinImage;
|
private Image skinImage;
|
||||||
|
|
||||||
@@ -421,6 +447,7 @@ public enum GameImage {
|
|||||||
for (GameImage img : GameImage.values()) {
|
for (GameImage img : GameImage.values()) {
|
||||||
img.defaultImage = img.skinImage = null;
|
img.defaultImage = img.skinImage = null;
|
||||||
img.defaultImages = img.skinImages = 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 filename the image file name
|
||||||
* @param type the file types (separated by '|')
|
* @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 filename the image file name
|
||||||
* @param filenameFormat the formatted file name string (for loading multiple images)
|
* @param filenameFormat the formatted file name string (for loading multiple images)
|
||||||
* @param type the file types (separated by '|')
|
* @param type the file types (separated by '|')
|
||||||
@@ -511,21 +538,21 @@ public enum GameImage {
|
|||||||
* Constructor for general images.
|
* Constructor for general images.
|
||||||
* @param filename the image file name
|
* @param filename the image file name
|
||||||
* @param type the file types (separated by '|')
|
* @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
|
* @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.filename = filename;
|
||||||
this.type = getType(type);
|
this.type = getType(type);
|
||||||
this.skinnable = skinnable;
|
this.beatmapSkinnable = beatmapSkinnable;
|
||||||
this.preload = preload;
|
this.preload = preload;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether or not the image is skinnable.
|
* Returns whether or not the image is beatmap-skinnable.
|
||||||
* @return true if 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.
|
* 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.
|
* 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() {
|
public Image getImage() {
|
||||||
setDefaultImage();
|
setDefaultImage();
|
||||||
@@ -556,7 +583,7 @@ public enum GameImage {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the image array associated with this resource.
|
* 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() {
|
public Image[] getImages() {
|
||||||
setDefaultImage();
|
setDefaultImage();
|
||||||
@@ -565,7 +592,7 @@ public enum GameImage {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the image associated with this resource to another image.
|
* 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
|
* @param img the image to set
|
||||||
*/
|
*/
|
||||||
public void setImage(Image img) {
|
public void setImage(Image img) {
|
||||||
@@ -577,7 +604,7 @@ public enum GameImage {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets an image associated with this resource to another image.
|
* 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 img the image to set
|
||||||
* @param index the index in the image array
|
* @param index the index in the image array
|
||||||
*/
|
*/
|
||||||
@@ -602,16 +629,26 @@ public enum GameImage {
|
|||||||
// try to load multiple images
|
// try to load multiple images
|
||||||
File skinDir = Options.getSkin().getDirectory();
|
File skinDir = Options.getSkin().getDirectory();
|
||||||
if (filenameFormat != null) {
|
if (filenameFormat != null) {
|
||||||
if ((skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) ||
|
if (skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) {
|
||||||
((defaultImages = loadImageArray(null)) != null)) {
|
isSkinned = true;
|
||||||
|
process();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((defaultImages = loadImageArray(null)) != null) {
|
||||||
|
isSkinned = false;
|
||||||
process();
|
process();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to load a single image
|
// try to load a single image
|
||||||
if ((skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) ||
|
if (skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) {
|
||||||
((defaultImage = loadImageSingle(null)) != null)) {
|
isSkinned = true;
|
||||||
|
process();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((defaultImage = loadImageSingle(null)) != null) {
|
||||||
|
isSkinned = false;
|
||||||
process();
|
process();
|
||||||
return;
|
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.
|
* If the path does not contain the image, the default image is used.
|
||||||
* @param dir the image directory to search
|
* @param dir the image directory to search
|
||||||
* @return true if a new skin image is loaded, false otherwise
|
* @return true if a new skin image is loaded, false otherwise
|
||||||
*/
|
*/
|
||||||
public boolean setSkinImage(File dir) {
|
public boolean setBeatmapSkinImage(File dir) {
|
||||||
if (dir == null)
|
if (dir == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// destroy the existing images, if any
|
// destroy the existing images, if any
|
||||||
destroySkinImage();
|
destroyBeatmapSkinImage();
|
||||||
|
|
||||||
// beatmap skins disabled
|
// beatmap skins disabled
|
||||||
if (Options.isBeatmapSkinIgnored())
|
if (Options.isBeatmapSkinIgnored())
|
||||||
@@ -709,21 +746,27 @@ public enum GameImage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether a skin image is currently loaded.
|
* Returns whether the default image loaded is part of a game skin.
|
||||||
* @return true if skin image exists
|
* @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.
|
* Returns whether a beatmap skin image is currently loaded.
|
||||||
* @return true if any skin image exists
|
* @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)
|
if (skinImage == null && skinImages == null)
|
||||||
return;
|
return;
|
||||||
try {
|
try {
|
||||||
@@ -740,7 +783,7 @@ public enum GameImage {
|
|||||||
skinImages = null;
|
skinImages = null;
|
||||||
}
|
}
|
||||||
} catch (SlickException e) {
|
} 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@
|
|||||||
|
|
||||||
package itdelatrisu.opsu;
|
package itdelatrisu.opsu;
|
||||||
|
|
||||||
|
import itdelatrisu.opsu.ui.Fonts;
|
||||||
import itdelatrisu.opsu.ui.MenuButton;
|
import itdelatrisu.opsu.ui.MenuButton;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -47,7 +49,7 @@ public enum GameMod {
|
|||||||
"DoubleTime", "Zoooooooooom."),
|
"DoubleTime", "Zoooooooooom."),
|
||||||
// NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f,
|
// NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f,
|
||||||
// "Nightcore", "uguuuuuuuu"),
|
// "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."),
|
"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 (Category.HARD, 4, GameImage.MOD_FLASHLIGHT, "FL", 1024, Input.KEY_G, 1.12f,
|
||||||
"Flashlight", "Restricted view area."),
|
"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**"),
|
"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,
|
AUTOPILOT (Category.SPECIAL, 1, GameImage.MOD_AUTOPILOT, "AP", 8192, Input.KEY_X, 0f,
|
||||||
"Relax2", "Automatic cursor movement - just follow the rhythm.\n**UNRANKED**"),
|
"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."),
|
"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.");
|
"Autoplay", "Watch a perfect automated play through the song.");
|
||||||
|
|
||||||
/** Mod categories. */
|
/** Mod categories. */
|
||||||
@@ -67,13 +69,13 @@ public enum GameMod {
|
|||||||
SPECIAL (2, "Special", Color.white);
|
SPECIAL (2, "Special", Color.white);
|
||||||
|
|
||||||
/** Drawing index. */
|
/** Drawing index. */
|
||||||
private int index;
|
private final int index;
|
||||||
|
|
||||||
/** Category name. */
|
/** Category name. */
|
||||||
private String name;
|
private final String name;
|
||||||
|
|
||||||
/** Text color. */
|
/** Text color. */
|
||||||
private Color color;
|
private final Color color;
|
||||||
|
|
||||||
/** The coordinates of the category. */
|
/** The coordinates of the category. */
|
||||||
private float x, y;
|
private float x, y;
|
||||||
@@ -96,10 +98,10 @@ public enum GameMod {
|
|||||||
* @param height the container height
|
* @param height the container height
|
||||||
*/
|
*/
|
||||||
public void init(int width, int 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;
|
float offsetY = GameImage.MOD_EASY.getImage().getHeight() * 1.5f;
|
||||||
this.x = width / 30f;
|
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. */
|
/** The category for the mod. */
|
||||||
private Category category;
|
private final Category category;
|
||||||
|
|
||||||
/** The index in the category (for positioning). */
|
/** The index in the category (for positioning). */
|
||||||
private int categoryIndex;
|
private final int categoryIndex;
|
||||||
|
|
||||||
/** The file name of the mod image. */
|
/** The file name of the mod image. */
|
||||||
private GameImage image;
|
private final GameImage image;
|
||||||
|
|
||||||
/** The abbreviation for the mod. */
|
/** The abbreviation for the mod. */
|
||||||
private String abbrev;
|
private final String abbrev;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bit value associated with the mod.
|
* Bit value associated with the mod.
|
||||||
* See the osu! API: https://github.com/peppy/osu-api/wiki#mods
|
* 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. */
|
/** The shortcut key associated with the mod. */
|
||||||
private int key;
|
private final int key;
|
||||||
|
|
||||||
/** The score multiplier. */
|
/** The score multiplier. */
|
||||||
private float multiplier;
|
private final float multiplier;
|
||||||
|
|
||||||
/** Whether or not the mod is implemented. */
|
|
||||||
private boolean implemented;
|
|
||||||
|
|
||||||
/** The name of the mod. */
|
/** The name of the mod. */
|
||||||
private String name;
|
private final String name;
|
||||||
|
|
||||||
/** The description of the mod. */
|
/** The description of the mod. */
|
||||||
private String description;
|
private final String description;
|
||||||
|
|
||||||
/** Whether or not this mod is active. */
|
/** Whether or not this mod is active. */
|
||||||
private boolean active = false;
|
private boolean active = false;
|
||||||
@@ -192,13 +191,15 @@ public enum GameMod {
|
|||||||
c.init(width, height);
|
c.init(width, height);
|
||||||
|
|
||||||
// create buttons
|
// 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;
|
float offsetX = GameImage.MOD_EASY.getImage().getWidth() * 2.1f;
|
||||||
for (GameMod mod : GameMod.values()) {
|
for (GameMod mod : GameMod.values()) {
|
||||||
Image img = mod.image.getImage();
|
Image img = mod.image.getImage();
|
||||||
mod.button = new MenuButton(img,
|
mod.button = new MenuButton(img,
|
||||||
baseX + (offsetX * mod.categoryIndex) + img.getWidth() / 2f,
|
baseX + (offsetX * mod.categoryIndex) + img.getWidth() / 2f,
|
||||||
mod.category.getY());
|
mod.category.getY());
|
||||||
|
mod.button.setHoverAnimationDuration(300);
|
||||||
|
mod.button.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
|
||||||
mod.button.setHoverExpand(1.2f);
|
mod.button.setHoverExpand(1.2f);
|
||||||
mod.button.setHoverRotate(10f);
|
mod.button.setHoverRotate(10f);
|
||||||
|
|
||||||
@@ -309,24 +310,6 @@ public enum GameMod {
|
|||||||
*/
|
*/
|
||||||
GameMod(Category category, int categoryIndex, GameImage image, String abbrev,
|
GameMod(Category category, int categoryIndex, GameImage image, String abbrev,
|
||||||
int bit, int key, float multiplier, String name, String description) {
|
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.category = category;
|
||||||
this.categoryIndex = categoryIndex;
|
this.categoryIndex = categoryIndex;
|
||||||
this.image = image;
|
this.image = image;
|
||||||
@@ -334,7 +317,6 @@ public enum GameMod {
|
|||||||
this.bit = bit;
|
this.bit = bit;
|
||||||
this.key = key;
|
this.key = key;
|
||||||
this.multiplier = multiplier;
|
this.multiplier = multiplier;
|
||||||
this.implemented = implemented;
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
@@ -376,20 +358,11 @@ public enum GameMod {
|
|||||||
*/
|
*/
|
||||||
public String getDescription() { return description; }
|
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.
|
* Toggles the active status of the mod.
|
||||||
* @param checkInverse if true, perform checks for mutual exclusivity
|
* @param checkInverse if true, perform checks for mutual exclusivity
|
||||||
*/
|
*/
|
||||||
public void toggle(boolean checkInverse) {
|
public void toggle(boolean checkInverse) {
|
||||||
if (!implemented)
|
|
||||||
return;
|
|
||||||
|
|
||||||
active = !active;
|
active = !active;
|
||||||
scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
|
scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
|
||||||
|
|
||||||
@@ -446,14 +419,7 @@ public enum GameMod {
|
|||||||
/**
|
/**
|
||||||
* Draws the game mod.
|
* Draws the game mod.
|
||||||
*/
|
*/
|
||||||
public void draw() {
|
public void draw() { button.draw(); }
|
||||||
if (!implemented) {
|
|
||||||
button.getImage().setAlpha(0.2f);
|
|
||||||
button.draw();
|
|
||||||
button.getImage().setAlpha(1f);
|
|
||||||
} else
|
|
||||||
button.draw();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the coordinates are within the image bounds.
|
* Checks if the coordinates are within the image bounds.
|
||||||
|
|||||||
104
src/itdelatrisu/opsu/NativeLoader.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,10 +38,14 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
import org.newdawn.slick.Color;
|
||||||
import org.newdawn.slick.GameContainer;
|
import org.newdawn.slick.GameContainer;
|
||||||
|
import org.newdawn.slick.Input;
|
||||||
import org.newdawn.slick.SlickException;
|
import org.newdawn.slick.SlickException;
|
||||||
import org.newdawn.slick.state.StateBasedGame;
|
import org.newdawn.slick.state.StateBasedGame;
|
||||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||||
@@ -116,16 +120,43 @@ public class Opsu extends StateBasedGame {
|
|||||||
|
|
||||||
// only allow a single instance
|
// only allow a single instance
|
||||||
try {
|
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) {
|
} 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);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set path for lwjgl natives - NOT NEEDED if using JarSplice
|
File nativeDir;
|
||||||
File nativeDir = new File("./target/natives/");
|
if (!Utils.isJarRunning() && (
|
||||||
if (nativeDir.isDirectory())
|
(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("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
|
// set the resource paths
|
||||||
ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));
|
ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));
|
||||||
@@ -142,6 +173,7 @@ public class Opsu extends StateBasedGame {
|
|||||||
Updater.get().setUpdateInfo(args[0], args[1]);
|
Updater.get().setUpdateInfo(args[0], args[1]);
|
||||||
|
|
||||||
// check for updates
|
// check for updates
|
||||||
|
if (!Options.isUpdaterDisabled()) {
|
||||||
new Thread() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -152,6 +184,10 @@ public class Opsu extends StateBasedGame {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable jinput
|
||||||
|
Input.disableControllers();
|
||||||
|
|
||||||
// start the game
|
// start the game
|
||||||
try {
|
try {
|
||||||
@@ -190,20 +226,18 @@ public class Opsu extends StateBasedGame {
|
|||||||
SongMenu songMenu = (SongMenu) this.getState(Opsu.STATE_SONGMENU);
|
SongMenu songMenu = (SongMenu) this.getState(Opsu.STATE_SONGMENU);
|
||||||
if (id == STATE_GAMERANKING) {
|
if (id == STATE_GAMERANKING) {
|
||||||
GameData data = ((GameRanking) this.getState(Opsu.STATE_GAMERANKING)).getGameData();
|
GameData data = ((GameRanking) this.getState(Opsu.STATE_GAMERANKING)).getGameData();
|
||||||
if (data != null && data.isGameplay()) {
|
if (data != null && data.isGameplay())
|
||||||
songMenu.resetGameDataOnLoad();
|
|
||||||
songMenu.resetTrackOnLoad();
|
songMenu.resetTrackOnLoad();
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
songMenu.resetGameDataOnLoad();
|
|
||||||
if (id == STATE_GAME) {
|
if (id == STATE_GAME) {
|
||||||
MusicController.pause();
|
MusicController.pause();
|
||||||
MusicController.resume();
|
MusicController.resume();
|
||||||
} else
|
} else
|
||||||
songMenu.resetTrackOnLoad();
|
songMenu.resetTrackOnLoad();
|
||||||
}
|
}
|
||||||
if (UI.getCursor().isSkinned())
|
if (UI.getCursor().isBeatmapSkinned())
|
||||||
UI.getCursor().reset();
|
UI.getCursor().reset();
|
||||||
|
songMenu.resetGameDataOnLoad();
|
||||||
this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import itdelatrisu.opsu.audio.MusicController;
|
|||||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||||
import itdelatrisu.opsu.skins.Skin;
|
import itdelatrisu.opsu.skins.Skin;
|
||||||
import itdelatrisu.opsu.skins.SkinLoader;
|
import itdelatrisu.opsu.skins.SkinLoader;
|
||||||
|
import itdelatrisu.opsu.ui.Fonts;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
@@ -37,6 +38,9 @@ import java.util.Date;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.TimeUnit;
|
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.lwjgl.input.Keyboard;
|
||||||
import org.newdawn.slick.GameContainer;
|
import org.newdawn.slick.GameContainer;
|
||||||
@@ -51,12 +55,18 @@ import org.newdawn.slick.util.ResourceLoader;
|
|||||||
* Handles all user options.
|
* Handles all user options.
|
||||||
*/
|
*/
|
||||||
public class Options {
|
public class Options {
|
||||||
|
/** Whether to use XDG directories. */
|
||||||
|
public static final boolean USE_XDG = checkXDGFlag();
|
||||||
|
|
||||||
/** The config directory. */
|
/** The config directory. */
|
||||||
private static final File CONFIG_DIR = getXDGBaseDir("XDG_CONFIG_HOME", ".config");
|
private static final File CONFIG_DIR = getXDGBaseDir("XDG_CONFIG_HOME", ".config");
|
||||||
|
|
||||||
/** The data directory. */
|
/** The data directory. */
|
||||||
private static final File DATA_DIR = getXDGBaseDir("XDG_DATA_HOME", ".local/share");
|
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. */
|
/** File for logging errors. */
|
||||||
public static final File LOG_FILE = new File(CONFIG_DIR, ".opsu.log");
|
public static final File LOG_FILE = new File(CONFIG_DIR, ".opsu.log");
|
||||||
|
|
||||||
@@ -83,6 +93,9 @@ public class Options {
|
|||||||
/** Score database name. */
|
/** Score database name. */
|
||||||
public static final File SCORE_DB = new File(DATA_DIR, ".opsu_scores.db");
|
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. */
|
/** Font file name. */
|
||||||
public static final String FONT_NAME = "DroidSansFallback.ttf";
|
public static final String FONT_NAME = "DroidSansFallback.ttf";
|
||||||
|
|
||||||
@@ -119,15 +132,35 @@ public class Options {
|
|||||||
/** Port binding. */
|
/** Port binding. */
|
||||||
private static int port = 49250;
|
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
|
* 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 env the environment variable to check (XDG_*_*)
|
||||||
* @param fallback the fallback directory relative to ~home
|
* @param fallback the fallback directory relative to ~home
|
||||||
* @return the XDG base directory, or the working directory if unavailable
|
* @return the XDG base directory, or the working directory if unavailable
|
||||||
*/
|
*/
|
||||||
private static File getXDGBaseDir(String env, String fallback) {
|
private static File getXDGBaseDir(String env, String fallback) {
|
||||||
if (System.getProperty("XDG") == null)
|
if (!USE_XDG)
|
||||||
return new File("./");
|
return new File("./");
|
||||||
|
|
||||||
String OS = System.getProperty("os.name").toLowerCase();
|
String OS = System.getProperty("os.name").toLowerCase();
|
||||||
@@ -140,8 +173,8 @@ public class Options {
|
|||||||
rootPath = String.format("%s/%s", home, fallback);
|
rootPath = String.format("%s/%s", home, fallback);
|
||||||
}
|
}
|
||||||
File dir = new File(rootPath, "opsu");
|
File dir = new File(rootPath, "opsu");
|
||||||
if (!dir.isDirectory())
|
if (!dir.isDirectory() && !dir.mkdir())
|
||||||
dir.mkdir();
|
ErrorHandler.error(String.format("Failed to create configuration folder at '%s/opsu'.", rootPath), null, false);
|
||||||
return dir;
|
return dir;
|
||||||
} else
|
} else
|
||||||
return new File("./");
|
return new File("./");
|
||||||
@@ -212,7 +245,7 @@ public class Options {
|
|||||||
@Override
|
@Override
|
||||||
public void read(String s) {
|
public void read(String s) {
|
||||||
int i = Integer.parseInt(s);
|
int i = Integer.parseInt(s);
|
||||||
if (i > 0 && i < 65535)
|
if (i > 0 && i <= 65535)
|
||||||
port = i;
|
port = i;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -287,9 +320,9 @@ public class Options {
|
|||||||
super.click(container);
|
super.click(container);
|
||||||
if (bool) {
|
if (bool) {
|
||||||
try {
|
try {
|
||||||
Utils.FONT_LARGE.loadGlyphs();
|
Fonts.LARGE.loadGlyphs();
|
||||||
Utils.FONT_MEDIUM.loadGlyphs();
|
Fonts.MEDIUM.loadGlyphs();
|
||||||
Utils.FONT_DEFAULT.loadGlyphs();
|
Fonts.DEFAULT.loadGlyphs();
|
||||||
} catch (SlickException e) {
|
} catch (SlickException e) {
|
||||||
Log.warn("Failed to load glyphs.", e);
|
Log.warn("Failed to load glyphs.", e);
|
||||||
}
|
}
|
||||||
@@ -357,7 +390,7 @@ public class Options {
|
|||||||
public String getValueString() { return String.format("%dms", val); }
|
public String getValueString() { return String.format("%dms", val); }
|
||||||
},
|
},
|
||||||
DISABLE_SOUNDS ("Disable All Sound Effects", "DisableSound", "May resolve Linux sound driver issues. Requires a restart.",
|
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.") {
|
KEY_LEFT ("Left Game Key", "keyOsuLeft", "Select this option to input a key.") {
|
||||||
@Override
|
@Override
|
||||||
public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); }
|
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_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_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),
|
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),
|
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),
|
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)));
|
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. */
|
/** Option name. */
|
||||||
private String name;
|
private final String name;
|
||||||
|
|
||||||
/** Option name, as displayed in the configuration file. */
|
/** Option name, as displayed in the configuration file. */
|
||||||
private String displayName;
|
private final String displayName;
|
||||||
|
|
||||||
/** Option description. */
|
/** Option description. */
|
||||||
private String description;
|
private final String description;
|
||||||
|
|
||||||
/** The boolean value for the option (if applicable). */
|
/** The boolean value for the option (if applicable). */
|
||||||
protected boolean bool;
|
protected boolean bool;
|
||||||
@@ -484,7 +521,7 @@ public class Options {
|
|||||||
* @param displayName the option name, as displayed in the configuration file
|
* @param displayName the option name, as displayed in the configuration file
|
||||||
*/
|
*/
|
||||||
GameOption(String displayName) {
|
GameOption(String displayName) {
|
||||||
this.displayName = displayName;
|
this(null, displayName, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -604,7 +641,7 @@ public class Options {
|
|||||||
*/
|
*/
|
||||||
public void drag(GameContainer container, int d) {
|
public void drag(GameContainer container, int d) {
|
||||||
if (type == OptionType.NUMERIC)
|
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(); }
|
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.
|
* Sets the track checkpoint time, if within bounds.
|
||||||
* @param time the track position (in ms)
|
* @param time the track position (in ms)
|
||||||
@@ -1005,6 +1060,12 @@ public class Options {
|
|||||||
"Mouse buttons are disabled." : "Mouse buttons are enabled.");
|
"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.
|
* Returns the left game key.
|
||||||
* @return the left key code
|
* @return the left key code
|
||||||
@@ -1080,7 +1141,10 @@ public class Options {
|
|||||||
if (beatmapDir.isDirectory())
|
if (beatmapDir.isDirectory())
|
||||||
return beatmapDir;
|
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;
|
return beatmapDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1094,7 +1158,8 @@ public class Options {
|
|||||||
return oszDir;
|
return oszDir;
|
||||||
|
|
||||||
oszDir = new File(DATA_DIR, "SongPacks/");
|
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;
|
return oszDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1108,7 +1173,8 @@ public class Options {
|
|||||||
return replayImportDir;
|
return replayImportDir;
|
||||||
|
|
||||||
replayImportDir = new File(DATA_DIR, "ReplayImport/");
|
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;
|
return replayImportDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1153,7 +1219,10 @@ public class Options {
|
|||||||
if (skinRootDir.isDirectory())
|
if (skinRootDir.isDirectory())
|
||||||
return skinRootDir;
|
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;
|
return skinRootDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ package itdelatrisu.opsu;
|
|||||||
|
|
||||||
import itdelatrisu.opsu.GameData.Grade;
|
import itdelatrisu.opsu.GameData.Grade;
|
||||||
import itdelatrisu.opsu.states.SongMenu;
|
import itdelatrisu.opsu.states.SongMenu;
|
||||||
|
import itdelatrisu.opsu.ui.Colors;
|
||||||
|
import itdelatrisu.opsu.ui.Fonts;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||||
|
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
@@ -84,11 +87,6 @@ public class ScoreData implements Comparable<ScoreData> {
|
|||||||
/** Drawing values. */
|
/** Drawing values. */
|
||||||
private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset, buttonAreaHeight;
|
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.
|
* Initializes the base coordinates for drawing.
|
||||||
* @param containerWidth the container width
|
* @param containerWidth the container width
|
||||||
@@ -99,7 +97,7 @@ public class ScoreData implements Comparable<ScoreData> {
|
|||||||
baseY = topY;
|
baseY = topY;
|
||||||
buttonWidth = containerWidth * 0.4f;
|
buttonWidth = containerWidth * 0.4f;
|
||||||
float gradeHeight = GameImage.MENU_BUTTON_BG.getImage().getHeight() * 0.45f;
|
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;
|
buttonOffset = buttonHeight + gradeHeight / 10f;
|
||||||
buttonAreaHeight = (SongMenu.MAX_SCORE_BUTTONS - 1) * buttonOffset + buttonHeight;
|
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() {}
|
public ScoreData() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Builds a score data object from a database result set.
|
||||||
* @param rs the ResultSet to read from (at the current cursor position)
|
* @param rs the {@link ResultSet} to read from (at the current cursor position)
|
||||||
* @throws SQLException
|
* @throws SQLException if a database access error occurs or the result set is closed
|
||||||
*/
|
*/
|
||||||
public ScoreData(ResultSet rs) throws SQLException {
|
public ScoreData(ResultSet rs) throws SQLException {
|
||||||
this.timestamp = rs.getLong(1);
|
this.timestamp = rs.getLong(1);
|
||||||
@@ -244,46 +242,52 @@ public class ScoreData implements Comparable<ScoreData> {
|
|||||||
* @param rank the score rank
|
* @param rank the score rank
|
||||||
* @param prevScore the previous (lower) score, or -1 if none
|
* @param prevScore the previous (lower) score, or -1 if none
|
||||||
* @param focus whether the button is focused
|
* @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) {
|
public void draw(Graphics g, float position, int rank, long prevScore, boolean focus, float t) {
|
||||||
Image img = getGrade().getMenuImage();
|
float x = baseX - buttonWidth * (1 - AnimationEquation.OUT_BACK.calc(t)) / 2.5f;
|
||||||
float textX = baseX + buttonWidth * 0.24f;
|
float textX = x + buttonWidth * 0.24f;
|
||||||
float edgeX = baseX + buttonWidth * 0.98f;
|
float edgeX = x + buttonWidth * 0.98f;
|
||||||
float y = baseY + position;
|
float y = baseY + position;
|
||||||
float midY = y + buttonHeight / 2f;
|
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
|
// rectangle outline
|
||||||
g.setColor((focus) ? BG_FOCUS : BG_NORMAL);
|
Color rectColor = (focus) ? Colors.BLACK_BG_FOCUS : Colors.BLACK_BG_NORMAL;
|
||||||
g.fillRect(baseX, y, buttonWidth, buttonHeight);
|
float oldRectAlpha = rectColor.a;
|
||||||
|
rectColor.a *= AnimationEquation.IN_QUAD.calc(alpha);
|
||||||
|
g.setColor(rectColor);
|
||||||
|
g.fillRect(x, y, buttonWidth, buttonHeight);
|
||||||
|
rectColor.a = oldRectAlpha;
|
||||||
|
|
||||||
// rank
|
// rank
|
||||||
if (focus) {
|
if (focus) {
|
||||||
Utils.FONT_LARGE.drawString(
|
Fonts.LARGE.drawString(
|
||||||
baseX + buttonWidth * 0.04f,
|
x + buttonWidth * 0.04f,
|
||||||
y + (buttonHeight - Utils.FONT_LARGE.getLineHeight()) / 2f,
|
y + (buttonHeight - Fonts.LARGE.getLineHeight()) / 2f,
|
||||||
Integer.toString(rank + 1), Color.white
|
Integer.toString(rank + 1), c
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// grade image
|
// 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
|
// score
|
||||||
float textOffset = (buttonHeight - Utils.FONT_MEDIUM.getLineHeight() - Utils.FONT_SMALL.getLineHeight()) / 2f;
|
float textOffset = (buttonHeight - Fonts.MEDIUM.getLineHeight() - Fonts.SMALL.getLineHeight()) / 2f;
|
||||||
Utils.FONT_MEDIUM.drawString(
|
Fonts.MEDIUM.drawString(textX, y + textOffset,
|
||||||
textX, y + textOffset,
|
String.format("Score: %s (%dx)", NumberFormat.getNumberInstance().format(score), combo), c);
|
||||||
String.format("Score: %s (%dx)", NumberFormat.getNumberInstance().format(score), combo),
|
|
||||||
Color.white
|
|
||||||
);
|
|
||||||
|
|
||||||
// hit counts (custom: osu! shows user instead, above score)
|
// hit counts (custom: osu! shows user instead, above score)
|
||||||
String player = (playerName == null) ? "" : String.format(" (%s)", playerName);
|
String player = (playerName == null) ? "" : String.format(" (%s)", playerName);
|
||||||
Utils.FONT_SMALL.drawString(
|
Fonts.SMALL.drawString(textX, y + textOffset + Fonts.MEDIUM.getLineHeight(),
|
||||||
textX, y + textOffset + Utils.FONT_MEDIUM.getLineHeight(),
|
String.format("300:%d 100:%d 50:%d Miss:%d%s", hit300, hit100, hit50, miss, player), c);
|
||||||
String.format("300:%d 100:%d 50:%d Miss:%d%s", hit300, hit100, hit50, miss, player),
|
|
||||||
Color.white
|
|
||||||
);
|
|
||||||
|
|
||||||
// mods
|
// mods
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
@@ -296,39 +300,30 @@ public class ScoreData implements Comparable<ScoreData> {
|
|||||||
if (sb.length() > 0) {
|
if (sb.length() > 0) {
|
||||||
sb.setLength(sb.length() - 1);
|
sb.setLength(sb.length() - 1);
|
||||||
String modString = sb.toString();
|
String modString = sb.toString();
|
||||||
Utils.FONT_DEFAULT.drawString(
|
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(modString), y + marginY, modString, c);
|
||||||
edgeX - Utils.FONT_DEFAULT.getWidth(modString),
|
|
||||||
y + marginY, modString, Color.white
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// accuracy
|
// accuracy
|
||||||
String accuracy = String.format("%.2f%%", getScorePercent());
|
String accuracy = String.format("%.2f%%", getScorePercent());
|
||||||
Utils.FONT_DEFAULT.drawString(
|
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(accuracy), y + marginY + Fonts.DEFAULT.getLineHeight(), accuracy, c);
|
||||||
edgeX - Utils.FONT_DEFAULT.getWidth(accuracy),
|
|
||||||
y + marginY + Utils.FONT_DEFAULT.getLineHeight(),
|
|
||||||
accuracy, Color.white
|
|
||||||
);
|
|
||||||
|
|
||||||
// score difference
|
// score difference
|
||||||
String diff = (prevScore < 0 || score < prevScore) ?
|
String diff = (prevScore < 0 || score < prevScore) ?
|
||||||
"-" : String.format("+%s", NumberFormat.getNumberInstance().format(score - prevScore));
|
"-" : String.format("+%s", NumberFormat.getNumberInstance().format(score - prevScore));
|
||||||
Utils.FONT_DEFAULT.drawString(
|
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(diff), y + marginY + Fonts.DEFAULT.getLineHeight() * 2, diff, c);
|
||||||
edgeX - Utils.FONT_DEFAULT.getWidth(diff),
|
|
||||||
y + marginY + Utils.FONT_DEFAULT.getLineHeight() * 2,
|
|
||||||
diff, Color.white
|
|
||||||
);
|
|
||||||
|
|
||||||
// time since
|
// time since
|
||||||
if (getTimeSince() != null) {
|
if (getTimeSince() != null) {
|
||||||
Image clock = GameImage.HISTORY.getImage();
|
Image clock = GameImage.HISTORY.getImage();
|
||||||
clock.drawCentered(baseX + buttonWidth * 1.02f + clock.getWidth() / 2f, midY);
|
clock.drawCentered(x + buttonWidth * 1.02f + clock.getWidth() / 2f, midY);
|
||||||
Utils.FONT_DEFAULT.drawString(
|
Fonts.DEFAULT.drawString(
|
||||||
baseX + buttonWidth * 1.03f + clock.getWidth(),
|
x + buttonWidth * 1.03f + clock.getWidth(),
|
||||||
midY - Utils.FONT_DEFAULT.getLineHeight() / 2f,
|
midY - Fonts.DEFAULT.getLineHeight() / 2f,
|
||||||
getTimeSince(), Color.white
|
getTimeSince(), c
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.a = oldAlpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -24,14 +24,15 @@ import itdelatrisu.opsu.beatmap.HitObject;
|
|||||||
import itdelatrisu.opsu.downloads.Download;
|
import itdelatrisu.opsu.downloads.Download;
|
||||||
import itdelatrisu.opsu.downloads.DownloadNode;
|
import itdelatrisu.opsu.downloads.DownloadNode;
|
||||||
import itdelatrisu.opsu.replay.PlaybackSpeed;
|
import itdelatrisu.opsu.replay.PlaybackSpeed;
|
||||||
|
import itdelatrisu.opsu.ui.Fonts;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
|
||||||
import java.awt.Font;
|
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
@@ -43,13 +44,10 @@ import java.nio.ByteBuffer;
|
|||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
|
||||||
@@ -60,16 +58,10 @@ import org.lwjgl.BufferUtils;
|
|||||||
import org.lwjgl.opengl.Display;
|
import org.lwjgl.opengl.Display;
|
||||||
import org.lwjgl.opengl.GL11;
|
import org.lwjgl.opengl.GL11;
|
||||||
import org.newdawn.slick.Animation;
|
import org.newdawn.slick.Animation;
|
||||||
import org.newdawn.slick.Color;
|
|
||||||
import org.newdawn.slick.GameContainer;
|
import org.newdawn.slick.GameContainer;
|
||||||
import org.newdawn.slick.Input;
|
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.state.StateBasedGame;
|
||||||
import org.newdawn.slick.util.Log;
|
import org.newdawn.slick.util.Log;
|
||||||
import org.newdawn.slick.util.ResourceLoader;
|
|
||||||
|
|
||||||
import com.sun.jna.platform.FileUtils;
|
import com.sun.jna.platform.FileUtils;
|
||||||
|
|
||||||
@@ -77,34 +69,6 @@ import com.sun.jna.platform.FileUtils;
|
|||||||
* Contains miscellaneous utilities.
|
* Contains miscellaneous utilities.
|
||||||
*/
|
*/
|
||||||
public class Utils {
|
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.
|
* List of illegal filename characters.
|
||||||
* @see #cleanFileName(String, char)
|
* @see #cleanFileName(String, char)
|
||||||
@@ -128,10 +92,8 @@ public class Utils {
|
|||||||
* Initializes game settings and class data.
|
* Initializes game settings and class data.
|
||||||
* @param container the game container
|
* @param container the game container
|
||||||
* @param game the game object
|
* @param game the game object
|
||||||
* @throws SlickException
|
|
||||||
*/
|
*/
|
||||||
public static void init(GameContainer container, StateBasedGame game)
|
public static void init(GameContainer container, StateBasedGame game) {
|
||||||
throws SlickException {
|
|
||||||
input = container.getInput();
|
input = container.getInput();
|
||||||
int width = container.getWidth();
|
int width = container.getWidth();
|
||||||
int height = container.getHeight();
|
int height = container.getHeight();
|
||||||
@@ -149,23 +111,8 @@ public class Utils {
|
|||||||
GameImage.init(width, height);
|
GameImage.init(width, height);
|
||||||
|
|
||||||
// create fonts
|
// create fonts
|
||||||
float fontBase = 12f * GameImage.getUIscale();
|
|
||||||
try {
|
try {
|
||||||
Font javaFont = Font.createFont(Font.TRUETYPE_FONT, ResourceLoader.getResourceAsStream(Options.FONT_NAME));
|
Fonts.init();
|
||||||
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);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ErrorHandler.error("Failed to load fonts.", e, true);
|
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.
|
* Clamps a value between a lower and upper bound.
|
||||||
* @param base the initial value
|
* @param val the value to clamp
|
||||||
* @param diff the value change
|
* @param low the lower bound
|
||||||
* @param min the minimum value
|
* @param high the upper bound
|
||||||
* @param max the maximum value
|
* @return the clamped value
|
||||||
* @return the bounded value
|
* @author fluddokt
|
||||||
*/
|
*/
|
||||||
public static int getBoundedValue(int base, int diff, int min, int max) {
|
public static int clamp(int val, int low, int high) {
|
||||||
int val = base + diff;
|
if (val < low)
|
||||||
if (val < min)
|
return low;
|
||||||
val = min;
|
if (val > high)
|
||||||
else if (val > max)
|
return high;
|
||||||
val = max;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a bounded value for a base value and displacement.
|
|
||||||
* @param base the initial value
|
|
||||||
* @param diff the value change
|
|
||||||
* @param min the minimum value
|
|
||||||
* @param max the maximum value
|
|
||||||
* @return the bounded value
|
|
||||||
*/
|
|
||||||
public static float getBoundedValue(float base, float diff, float min, float max) {
|
|
||||||
float val = base + diff;
|
|
||||||
if (val < min)
|
|
||||||
val = min;
|
|
||||||
else if (val > max)
|
|
||||||
val = max;
|
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,6 +198,13 @@ public class Utils {
|
|||||||
return (float) Math.sqrt((v1 * v1) + (v2 * v2));
|
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).
|
* Returns true if a game input key is pressed (mouse/keyboard left/right).
|
||||||
* @return true if pressed
|
* @return true if pressed
|
||||||
@@ -289,12 +225,10 @@ public class Utils {
|
|||||||
public static void takeScreenShot() {
|
public static void takeScreenShot() {
|
||||||
// create the screenshot directory
|
// create the screenshot directory
|
||||||
File dir = Options.getScreenshotDir();
|
File dir = Options.getScreenshotDir();
|
||||||
if (!dir.isDirectory()) {
|
if (!dir.isDirectory() && !dir.mkdir()) {
|
||||||
if (!dir.mkdir()) {
|
ErrorHandler.error(String.format("Failed to create screenshot directory at '%s'.", dir.getAbsolutePath()), null, false);
|
||||||
ErrorHandler.error("Failed to create screenshot directory.", null, false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// create file name
|
// create file name
|
||||||
SimpleDateFormat date = new SimpleDateFormat("yyyyMMdd_HHmmss");
|
SimpleDateFormat date = new SimpleDateFormat("yyyyMMdd_HHmmss");
|
||||||
@@ -333,54 +267,6 @@ public class Utils {
|
|||||||
}.start();
|
}.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.
|
* Returns a human-readable representation of a given number of bytes.
|
||||||
* @param bytes the number of bytes
|
* @param bytes the number of bytes
|
||||||
@@ -474,50 +360,12 @@ public class Utils {
|
|||||||
dir.delete();
|
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.
|
* Returns a the contents of a URL as a string.
|
||||||
* @param url the remote URL
|
* @param url the remote URL
|
||||||
* @return the contents as a string, or null if any error occurred
|
* @return the contents as a string, or null if any error occurred
|
||||||
* @author Roland Illig (http://stackoverflow.com/a/4308662)
|
* @author Roland Illig (http://stackoverflow.com/a/4308662)
|
||||||
|
* @throws IOException if an I/O exception occurs
|
||||||
*/
|
*/
|
||||||
public static String readDataFromUrl(URL url) throws IOException {
|
public static String readDataFromUrl(URL url) throws IOException {
|
||||||
// open connection
|
// open connection
|
||||||
@@ -553,6 +401,7 @@ public class Utils {
|
|||||||
* Returns a JSON object from a URL.
|
* Returns a JSON object from a URL.
|
||||||
* @param url the remote URL
|
* @param url the remote URL
|
||||||
* @return the JSON object, or null if an error occurred
|
* @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 {
|
public static JSONObject readJsonObjectFromUrl(URL url) throws IOException {
|
||||||
String s = Utils.readDataFromUrl(url);
|
String s = Utils.readDataFromUrl(url);
|
||||||
@@ -571,6 +420,7 @@ public class Utils {
|
|||||||
* Returns a JSON array from a URL.
|
* Returns a JSON array from a URL.
|
||||||
* @param url the remote URL
|
* @param url the remote URL
|
||||||
* @return the JSON array, or null if an error occurred
|
* @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 {
|
public static JSONArray readJsonArrayFromUrl(URL url) throws IOException {
|
||||||
String s = Utils.readDataFromUrl(url);
|
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);
|
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.
|
* Returns whether or not the application is running within a JAR.
|
||||||
* @return true if JAR, false if file
|
* @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:");
|
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.
|
* Returns the directory where the application is being run.
|
||||||
|
* @return the directory, or null if it could not be determined
|
||||||
*/
|
*/
|
||||||
public static File getRunningDirectory() {
|
public static File getRunningDirectory() {
|
||||||
try {
|
try {
|
||||||
@@ -695,4 +536,29 @@ public class Utils {
|
|||||||
public static boolean parseBoolean(String s) {
|
public static boolean parseBoolean(String s) {
|
||||||
return (Integer.parseInt(s) == 1);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ public enum HitSound implements SoundController.SoundComponent {
|
|||||||
// TAIKO ("taiko", 4);
|
// TAIKO ("taiko", 4);
|
||||||
|
|
||||||
/** The sample set name. */
|
/** The sample set name. */
|
||||||
private String name;
|
private final String name;
|
||||||
|
|
||||||
/** The sample set index. */
|
/** The sample set index. */
|
||||||
private int index;
|
private final int index;
|
||||||
|
|
||||||
/** Total number of sample sets. */
|
/** Total number of sample sets. */
|
||||||
public static final int SIZE = values().length;
|
public static final int SIZE = values().length;
|
||||||
@@ -77,7 +77,7 @@ public enum HitSound implements SoundController.SoundComponent {
|
|||||||
private static SampleSet currentDefaultSampleSet = SampleSet.NORMAL;
|
private static SampleSet currentDefaultSampleSet = SampleSet.NORMAL;
|
||||||
|
|
||||||
/** The file name. */
|
/** The file name. */
|
||||||
private String filename;
|
private final String filename;
|
||||||
|
|
||||||
/** The Clip associated with the hit sound. */
|
/** The Clip associated with the hit sound. */
|
||||||
private HashMap<SampleSet, MultiClip> clips;
|
private HashMap<SampleSet, MultiClip> clips;
|
||||||
|
|||||||
@@ -49,12 +49,15 @@ public class MultiClip {
|
|||||||
private byte[] audioData;
|
private byte[] audioData;
|
||||||
|
|
||||||
/** The name given to this clip. */
|
/** The name given to this clip. */
|
||||||
private String name;
|
private final String name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param name the clip name
|
* @param name the clip name
|
||||||
* @param audioIn the associated AudioInputStream
|
* @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 {
|
public MultiClip(String name, AudioInputStream audioIn) throws IOException, LineUnavailableException {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -105,6 +108,8 @@ public class MultiClip {
|
|||||||
* Plays the clip with the specified volume.
|
* Plays the clip with the specified volume.
|
||||||
* @param volume the volume the play at
|
* @param volume the volume the play at
|
||||||
* @param listener the line listener
|
* @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 {
|
public void start(float volume, LineListener listener) throws LineUnavailableException {
|
||||||
Clip clip = getClip();
|
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.
|
* If no clip is available, then a new one is created if under MAX_CLIPS.
|
||||||
* Otherwise, an existing clip will be returned.
|
* Otherwise, an existing clip will be returned.
|
||||||
* @return the Clip to play
|
* @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 {
|
private Clip getClip() throws LineUnavailableException {
|
||||||
// TODO:
|
// TODO:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import itdelatrisu.opsu.ErrorHandler;
|
|||||||
import itdelatrisu.opsu.Options;
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||||
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -41,6 +42,7 @@ import org.newdawn.slick.MusicListener;
|
|||||||
import org.newdawn.slick.SlickException;
|
import org.newdawn.slick.SlickException;
|
||||||
import org.newdawn.slick.openal.Audio;
|
import org.newdawn.slick.openal.Audio;
|
||||||
import org.newdawn.slick.openal.SoundStore;
|
import org.newdawn.slick.openal.SoundStore;
|
||||||
|
import org.newdawn.slick.util.ResourceLoader;
|
||||||
import org.tritonus.share.sampled.file.TAudioFileFormat;
|
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) {
|
public static void play(final Beatmap beatmap, final boolean loop, final boolean preview) {
|
||||||
// new track: load and play
|
// new track: load and play
|
||||||
if (lastBeatmap == null || !beatmap.audioFilename.equals(lastBeatmap.audioFilename)) {
|
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();
|
reset();
|
||||||
System.gc();
|
System.gc();
|
||||||
|
|
||||||
@@ -96,7 +105,7 @@ public class MusicController {
|
|||||||
trackLoader = new Thread() {
|
trackLoader = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
loadTrack(beatmap.audioFilename, (preview) ? beatmap.previewTime : 0, loop);
|
loadTrack(audioFile, (preview) ? beatmap.previewTime : 0, loop);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
trackLoader.start();
|
trackLoader.start();
|
||||||
@@ -221,6 +230,7 @@ public class MusicController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Fades out the track.
|
* Fades out the track.
|
||||||
|
* @param duration the fade time (in ms)
|
||||||
*/
|
*/
|
||||||
public static void fadeOut(int duration) {
|
public static void fadeOut(int duration) {
|
||||||
if (isPlaying())
|
if (isPlaying())
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ public class SoundController {
|
|||||||
/** Sample volume multiplier, from timing points [0, 1]. */
|
/** Sample volume multiplier, from timing points [0, 1]. */
|
||||||
private static float sampleVolumeMultiplier = 1f;
|
private static float sampleVolumeMultiplier = 1f;
|
||||||
|
|
||||||
|
/** Whether all sounds are muted. */
|
||||||
|
private static boolean isMuted;
|
||||||
|
|
||||||
/** The name of the current sound file being loaded. */
|
/** The name of the current sound file being loaded. */
|
||||||
private static String currentFileName;
|
private static String currentFileName;
|
||||||
|
|
||||||
@@ -261,7 +264,7 @@ public class SoundController {
|
|||||||
if (clip == null) // clip failed to load properly
|
if (clip == null) // clip failed to load properly
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (volume > 0f) {
|
if (volume > 0f && !isMuted) {
|
||||||
try {
|
try {
|
||||||
clip.start(volume, listener);
|
clip.start(volume, listener);
|
||||||
} catch (LineUnavailableException e) {
|
} catch (LineUnavailableException e) {
|
||||||
@@ -317,6 +320,12 @@ public class SoundController {
|
|||||||
playClip(s.getClip(), Options.getHitSoundVolume() * sampleVolumeMultiplier * Options.getMasterVolume(), null);
|
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.
|
* Returns the name of the current file being loaded, or null if none.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public enum SoundEffect implements SoundController.SoundComponent {
|
|||||||
SPINNERSPIN ("spinnerspin");
|
SPINNERSPIN ("spinnerspin");
|
||||||
|
|
||||||
/** The file name. */
|
/** The file name. */
|
||||||
private String filename;
|
private final String filename;
|
||||||
|
|
||||||
/** The Clip associated with the sound effect. */
|
/** The Clip associated with the sound effect. */
|
||||||
private MultiClip clip;
|
private MultiClip clip;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import itdelatrisu.opsu.Options;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
import org.newdawn.slick.Color;
|
||||||
import org.newdawn.slick.Image;
|
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;
|
public static final byte MODE_OSU = 0, MODE_TAIKO = 1, MODE_CTB = 2, MODE_MANIA = 3;
|
||||||
|
|
||||||
/** Background image cache. */
|
/** 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. */
|
/** The OSU File object associated with this beatmap. */
|
||||||
private File file;
|
private File file;
|
||||||
|
|
||||||
|
/** MD5 hash of this file. */
|
||||||
|
public String md5Hash;
|
||||||
|
|
||||||
|
/** The star rating. */
|
||||||
|
public double starRating = -1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [General]
|
* [General]
|
||||||
*/
|
*/
|
||||||
@@ -138,7 +160,7 @@ public class Beatmap implements Comparable<Beatmap> {
|
|||||||
public float HPDrainRate = 5f;
|
public float HPDrainRate = 5f;
|
||||||
|
|
||||||
/** CS: Size of circles and sliders (0:large ~ 10:small). */
|
/** 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). */
|
/** OD: Affects timing window, spinners, and approach speed (0:easy ~ 10:hard). */
|
||||||
public float overallDifficulty = 5f;
|
public float overallDifficulty = 5f;
|
||||||
@@ -147,7 +169,7 @@ public class Beatmap implements Comparable<Beatmap> {
|
|||||||
public float approachRate = -1f;
|
public float approachRate = -1f;
|
||||||
|
|
||||||
/** Slider movement speed multiplier. */
|
/** Slider movement speed multiplier. */
|
||||||
public float sliderMultiplier = 1f;
|
public float sliderMultiplier = 1.4f;
|
||||||
|
|
||||||
/** Rate at which slider ticks are placed (x per beat). */
|
/** Rate at which slider ticks are placed (x per beat). */
|
||||||
public float sliderTickRate = 1f;
|
public float sliderTickRate = 1f;
|
||||||
@@ -185,9 +207,6 @@ public class Beatmap implements Comparable<Beatmap> {
|
|||||||
/** Slider border color. If null, the skin value is used. */
|
/** Slider border color. If null, the skin value is used. */
|
||||||
public Color sliderBorder;
|
public Color sliderBorder;
|
||||||
|
|
||||||
/** MD5 hash of this file. */
|
|
||||||
public String md5Hash;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [HitObjects]
|
* [HitObjects]
|
||||||
*/
|
*/
|
||||||
@@ -255,22 +274,55 @@ 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 width the container width
|
||||||
* @param height the container height
|
* @param height the container height
|
||||||
* @param alpha the alpha value
|
* @param alpha the alpha value
|
||||||
* @param stretch if true, stretch to screen dimensions; otherwise, maintain aspect ratio
|
* @param stretch if true, stretch to screen dimensions; otherwise, maintain aspect ratio
|
||||||
* @return true if successful, false if any errors were produced
|
* @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)
|
if (bg == null)
|
||||||
return false;
|
return false;
|
||||||
try {
|
|
||||||
Image bgImage = bgImageCache.get(this);
|
ImageLoader imageLoader = bgImageCache.get(bg);
|
||||||
if (bgImage == null) {
|
if (imageLoader == null)
|
||||||
bgImage = new Image(bg.getAbsolutePath());
|
return false;
|
||||||
bgImageCache.put(this, bgImage);
|
|
||||||
}
|
Image bgImage = imageLoader.getImage();
|
||||||
|
if (bgImage == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
int swidth = width;
|
int swidth = width;
|
||||||
int sheight = height;
|
int sheight = height;
|
||||||
@@ -288,14 +340,8 @@ public class Beatmap implements Comparable<Beatmap> {
|
|||||||
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
||||||
}
|
}
|
||||||
bgImage = bgImage.getScaledCopy(swidth, sheight);
|
bgImage = bgImage.getScaledCopy(swidth, sheight);
|
||||||
|
|
||||||
bgImage.setAlpha(alpha);
|
bgImage.setAlpha(alpha);
|
||||||
bgImage.drawCentered(width / 2, height / 2);
|
bgImage.drawCentered(width / 2, height / 2);
|
||||||
} catch (Exception e) {
|
|
||||||
Log.warn(String.format("Failed to get background image '%s'.", bg), e);
|
|
||||||
bg = null; // don't try to load the file again until a restart
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
544
src/itdelatrisu/opsu/beatmap/BeatmapDifficultyCalculator.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(); }
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
package itdelatrisu.opsu.beatmap;
|
package itdelatrisu.opsu.beatmap;
|
||||||
|
|
||||||
import itdelatrisu.opsu.ErrorHandler;
|
import itdelatrisu.opsu.ErrorHandler;
|
||||||
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.db.BeatmapDB;
|
import itdelatrisu.opsu.db.BeatmapDB;
|
||||||
import itdelatrisu.opsu.io.MD5InputStreamWrapper;
|
import itdelatrisu.opsu.io.MD5InputStreamWrapper;
|
||||||
@@ -83,6 +84,10 @@ public class BeatmapParser {
|
|||||||
// create a new BeatmapSetList
|
// create a new BeatmapSetList
|
||||||
BeatmapSetList.create();
|
BeatmapSetList.create();
|
||||||
|
|
||||||
|
// create a new watch service
|
||||||
|
if (Options.isWatchServiceEnabled())
|
||||||
|
BeatmapWatchService.create();
|
||||||
|
|
||||||
// parse all directories
|
// parse all directories
|
||||||
parseDirectories(root.listFiles());
|
parseDirectories(root.listFiles());
|
||||||
}
|
}
|
||||||
@@ -110,6 +115,9 @@ public class BeatmapParser {
|
|||||||
List<Beatmap> cachedBeatmaps = new LinkedList<Beatmap>(); // loaded from database
|
List<Beatmap> cachedBeatmaps = new LinkedList<Beatmap>(); // loaded from database
|
||||||
List<Beatmap> parsedBeatmaps = new LinkedList<Beatmap>(); // loaded from parser
|
List<Beatmap> parsedBeatmaps = new LinkedList<Beatmap>(); // loaded from parser
|
||||||
|
|
||||||
|
// watch service
|
||||||
|
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
|
||||||
|
|
||||||
// parse directories
|
// parse directories
|
||||||
BeatmapSetNode lastNode = null;
|
BeatmapSetNode lastNode = null;
|
||||||
for (File dir : dirs) {
|
for (File dir : dirs) {
|
||||||
@@ -134,9 +142,10 @@ public class BeatmapParser {
|
|||||||
|
|
||||||
// check if beatmap is cached
|
// check if beatmap is cached
|
||||||
String path = String.format("%s/%s", dir.getName(), file.getName());
|
String path = String.format("%s/%s", dir.getName(), file.getName());
|
||||||
if (map.containsKey(path)) {
|
if (map != null) {
|
||||||
|
Long lastModified = map.get(path);
|
||||||
|
if (lastModified != null) {
|
||||||
// check last modified times
|
// check last modified times
|
||||||
long lastModified = map.get(path);
|
|
||||||
if (lastModified == file.lastModified()) {
|
if (lastModified == file.lastModified()) {
|
||||||
// add to cached beatmap list
|
// add to cached beatmap list
|
||||||
Beatmap beatmap = new Beatmap(file);
|
Beatmap beatmap = new Beatmap(file);
|
||||||
@@ -146,6 +155,7 @@ public class BeatmapParser {
|
|||||||
} else
|
} else
|
||||||
BeatmapDB.delete(dir.getName(), file.getName());
|
BeatmapDB.delete(dir.getName(), file.getName());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parse hit objects only when needed to save time/memory.
|
// Parse hit objects only when needed to save time/memory.
|
||||||
// Change boolean to 'true' to parse them immediately.
|
// Change boolean to 'true' to parse them immediately.
|
||||||
@@ -162,6 +172,8 @@ public class BeatmapParser {
|
|||||||
if (!beatmaps.isEmpty()) {
|
if (!beatmaps.isEmpty()) {
|
||||||
beatmaps.trimToSize();
|
beatmaps.trimToSize();
|
||||||
allBeatmaps.add(beatmaps);
|
allBeatmaps.add(beatmaps);
|
||||||
|
if (ws != null)
|
||||||
|
ws.registerAll(dir.toPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
// stop parsing files (interrupted)
|
// stop parsing files (interrupted)
|
||||||
@@ -676,10 +688,15 @@ public class BeatmapParser {
|
|||||||
|
|
||||||
beatmap.objects[objectIndex++] = hitObject;
|
beatmap.objects[objectIndex++] = hitObject;
|
||||||
} catch (Exception e) {
|
} 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);
|
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) {
|
} catch (IOException e) {
|
||||||
ErrorHandler.error(String.format("Failed to read file '%s'.", beatmap.getFile().getAbsolutePath()), e, false);
|
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.
|
* Returns the file extension of a file.
|
||||||
|
* @param file the file name
|
||||||
*/
|
*/
|
||||||
public static String getExtension(String file) {
|
public static String getExtension(String file) {
|
||||||
int i = file.lastIndexOf('.');
|
int i = file.lastIndexOf('.');
|
||||||
|
|||||||
@@ -21,14 +21,15 @@ package itdelatrisu.opsu.beatmap;
|
|||||||
import itdelatrisu.opsu.GameMod;
|
import itdelatrisu.opsu.GameMod;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data type containing all beatmaps in a beatmap set.
|
* Data type containing all beatmaps in a beatmap set.
|
||||||
*/
|
*/
|
||||||
public class BeatmapSet {
|
public class BeatmapSet implements Iterable<Beatmap> {
|
||||||
/** List of associated beatmaps. */
|
/** List of associated beatmaps. */
|
||||||
private ArrayList<Beatmap> beatmaps;
|
private final ArrayList<Beatmap> beatmaps;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
@@ -46,7 +47,7 @@ public class BeatmapSet {
|
|||||||
/**
|
/**
|
||||||
* Returns the beatmap at the given index.
|
* Returns the beatmap at the given index.
|
||||||
* @param index the beatmap 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); }
|
public Beatmap get(int index) { return beatmaps.get(index); }
|
||||||
|
|
||||||
@@ -54,10 +55,13 @@ public class BeatmapSet {
|
|||||||
* Removes the beatmap at the given index.
|
* Removes the beatmap at the given index.
|
||||||
* @param index the beatmap index
|
* @param index the beatmap index
|
||||||
* @return the removed beatmap
|
* @return the removed beatmap
|
||||||
* @throws IndexOutOfBoundsException
|
* @throws IndexOutOfBoundsException if the index is out of range
|
||||||
*/
|
*/
|
||||||
public Beatmap remove(int index) { return beatmaps.remove(index); }
|
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.
|
* Returns an array of strings containing beatmap information.
|
||||||
* <ul>
|
* <ul>
|
||||||
@@ -65,10 +69,10 @@ public class BeatmapSet {
|
|||||||
* <li>1: Mapped by {Creator}
|
* <li>1: Mapped by {Creator}
|
||||||
* <li>2: Length: {} BPM: {} Objects: {}
|
* <li>2: Length: {} BPM: {} Objects: {}
|
||||||
* <li>3: Circles: {} Sliders: {} Spinners: {}
|
* <li>3: Circles: {} Sliders: {} Spinners: {}
|
||||||
* <li>4: CS:{} HP:{} AR:{} OD:{}
|
* <li>4: CS:{} HP:{} AR:{} OD:{} Stars:{}
|
||||||
* </ul>
|
* </ul>
|
||||||
* @param index the beatmap index
|
* @param index the beatmap index
|
||||||
* @throws IndexOutOfBoundsException
|
* @throws IndexOutOfBoundsException if the index is out of range
|
||||||
*/
|
*/
|
||||||
public String[] getInfo(int index) {
|
public String[] getInfo(int index) {
|
||||||
Beatmap beatmap = beatmaps.get(index);
|
Beatmap beatmap = beatmaps.get(index);
|
||||||
@@ -88,11 +92,12 @@ public class BeatmapSet {
|
|||||||
(beatmap.hitObjectCircle + beatmap.hitObjectSlider + beatmap.hitObjectSpinner));
|
(beatmap.hitObjectCircle + beatmap.hitObjectSlider + beatmap.hitObjectSpinner));
|
||||||
info[3] = String.format("Circles: %d Sliders: %d Spinners: %d",
|
info[3] = String.format("Circles: %d Sliders: %d Spinners: %d",
|
||||||
beatmap.hitObjectCircle, beatmap.hitObjectSlider, beatmap.hitObjectSpinner);
|
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.circleSize * multiplier, 10f),
|
||||||
Math.min(beatmap.HPDrainRate * multiplier, 10f),
|
Math.min(beatmap.HPDrainRate * multiplier, 10f),
|
||||||
Math.min(beatmap.approachRate * 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;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +131,7 @@ public class BeatmapSet {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
// search: version, tags (remaining beatmaps)
|
// 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);
|
beatmap = beatmaps.get(i);
|
||||||
if (beatmap.version.toLowerCase().contains(query) ||
|
if (beatmap.version.toLowerCase().contains(query) ||
|
||||||
beatmap.tags.contains(query))
|
beatmap.tags.contains(query))
|
||||||
@@ -138,7 +143,7 @@ public class BeatmapSet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether the beatmap set matches a given condition.
|
* 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 operator the operator {@literal (=/==, >, >=, <, <=)}
|
||||||
* @param value the value
|
* @param value the value
|
||||||
* @return true if the condition is met
|
* @return true if the condition is met
|
||||||
@@ -154,6 +159,8 @@ public class BeatmapSet {
|
|||||||
case "hp": v = beatmap.HPDrainRate; break;
|
case "hp": v = beatmap.HPDrainRate; break;
|
||||||
case "bpm": v = beatmap.bpmMax; break;
|
case "bpm": v = beatmap.bpmMax; break;
|
||||||
case "length": v = beatmap.endTime / 1000; break;
|
case "length": v = beatmap.endTime / 1000; break;
|
||||||
|
case "star":
|
||||||
|
case "stars": v = Math.round(beatmap.starRating * 100) / 100f; break;
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package itdelatrisu.opsu.beatmap;
|
package itdelatrisu.opsu.beatmap;
|
||||||
|
|
||||||
import itdelatrisu.opsu.ErrorHandler;
|
import itdelatrisu.opsu.ErrorHandler;
|
||||||
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.audio.MusicController;
|
import itdelatrisu.opsu.audio.MusicController;
|
||||||
import itdelatrisu.opsu.db.BeatmapDB;
|
import itdelatrisu.opsu.db.BeatmapDB;
|
||||||
@@ -44,7 +45,7 @@ public class BeatmapSetList {
|
|||||||
|
|
||||||
/** Search pattern for conditional expressions. */
|
/** Search pattern for conditional expressions. */
|
||||||
private static final Pattern SEARCH_CONDITION_PATTERN = Pattern.compile(
|
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. */
|
/** List containing all parsed nodes. */
|
||||||
@@ -163,12 +164,17 @@ public class BeatmapSetList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// remove all node references
|
// remove all node references
|
||||||
Beatmap beatmap = node.getBeatmapSet().get(0);
|
BeatmapSet beatmapSet = node.getBeatmapSet();
|
||||||
|
Beatmap beatmap = beatmapSet.get(0);
|
||||||
nodes.remove(index);
|
nodes.remove(index);
|
||||||
parsedNodes.remove(eCur);
|
parsedNodes.remove(eCur);
|
||||||
mapCount -= node.getBeatmapSet().size();
|
mapCount -= beatmapSet.size();
|
||||||
if (beatmap.beatmapSetID > 0)
|
if (beatmap.beatmapSetID > 0)
|
||||||
MSIDdb.remove(beatmap.beatmapSetID);
|
MSIDdb.remove(beatmap.beatmapSetID);
|
||||||
|
for (Beatmap bm : beatmapSet) {
|
||||||
|
if (bm.md5Hash != null)
|
||||||
|
this.beatmapHashDB.remove(bm.md5Hash);
|
||||||
|
}
|
||||||
|
|
||||||
// reset indices
|
// reset indices
|
||||||
for (int i = index, size = size(); i < size; i++)
|
for (int i = index, size = size(); i < size; i++)
|
||||||
@@ -199,11 +205,16 @@ public class BeatmapSetList {
|
|||||||
BeatmapDB.delete(dir.getName());
|
BeatmapDB.delete(dir.getName());
|
||||||
|
|
||||||
// delete the associated directory
|
// delete the associated directory
|
||||||
|
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
|
||||||
|
if (ws != null)
|
||||||
|
ws.pause();
|
||||||
try {
|
try {
|
||||||
Utils.deleteToTrash(dir);
|
Utils.deleteToTrash(dir);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ErrorHandler.error("Could not delete song group.", e, true);
|
ErrorHandler.error("Could not delete song group.", e, true);
|
||||||
}
|
}
|
||||||
|
if (ws != null)
|
||||||
|
ws.resume();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -235,6 +246,8 @@ public class BeatmapSetList {
|
|||||||
// remove song reference
|
// remove song reference
|
||||||
Beatmap beatmap = node.getBeatmapSet().remove(node.beatmapIndex);
|
Beatmap beatmap = node.getBeatmapSet().remove(node.beatmapIndex);
|
||||||
mapCount--;
|
mapCount--;
|
||||||
|
if (beatmap.md5Hash != null)
|
||||||
|
beatmapHashDB.remove(beatmap.md5Hash);
|
||||||
|
|
||||||
// re-link nodes
|
// re-link nodes
|
||||||
if (node.prev != null)
|
if (node.prev != null)
|
||||||
@@ -247,11 +260,16 @@ public class BeatmapSetList {
|
|||||||
BeatmapDB.delete(file.getParentFile().getName(), file.getName());
|
BeatmapDB.delete(file.getParentFile().getName(), file.getName());
|
||||||
|
|
||||||
// delete the associated file
|
// delete the associated file
|
||||||
|
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
|
||||||
|
if (ws != null)
|
||||||
|
ws.pause();
|
||||||
try {
|
try {
|
||||||
Utils.deleteToTrash(file);
|
Utils.deleteToTrash(file);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ErrorHandler.error("Could not delete song.", e, true);
|
ErrorHandler.error("Could not delete song.", e, true);
|
||||||
}
|
}
|
||||||
|
if (ws != null)
|
||||||
|
ws.resume();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -312,6 +330,7 @@ public class BeatmapSetList {
|
|||||||
/**
|
/**
|
||||||
* Expands the node at an index by inserting a new node for each Beatmap
|
* Expands the node at an index by inserting a new node for each Beatmap
|
||||||
* in that node and hiding the group node.
|
* in that node and hiding the group node.
|
||||||
|
* @param index the node index
|
||||||
* @return the first of the newly-inserted nodes
|
* @return the first of the newly-inserted nodes
|
||||||
*/
|
*/
|
||||||
public BeatmapSetNode expand(int index) {
|
public BeatmapSetNode expand(int index) {
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ package itdelatrisu.opsu.beatmap;
|
|||||||
import itdelatrisu.opsu.GameData.Grade;
|
import itdelatrisu.opsu.GameData.Grade;
|
||||||
import itdelatrisu.opsu.GameImage;
|
import itdelatrisu.opsu.GameImage;
|
||||||
import itdelatrisu.opsu.Options;
|
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.Color;
|
||||||
import org.newdawn.slick.Image;
|
import org.newdawn.slick.Image;
|
||||||
@@ -31,7 +32,7 @@ import org.newdawn.slick.Image;
|
|||||||
*/
|
*/
|
||||||
public class BeatmapSetNode {
|
public class BeatmapSetNode {
|
||||||
/** The associated beatmap set. */
|
/** The associated beatmap set. */
|
||||||
private BeatmapSet beatmapSet;
|
private final BeatmapSet beatmapSet;
|
||||||
|
|
||||||
/** Index of the selected beatmap (-1 if not focused). */
|
/** Index of the selected beatmap (-1 if not focused). */
|
||||||
public int beatmapIndex = -1;
|
public int beatmapIndex = -1;
|
||||||
@@ -56,6 +57,14 @@ public class BeatmapSetNode {
|
|||||||
*/
|
*/
|
||||||
public BeatmapSet getBeatmapSet() { return beatmapSet; }
|
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.
|
* Draws the button.
|
||||||
* @param x the x coordinate
|
* @param x the x coordinate
|
||||||
@@ -78,16 +87,16 @@ public class BeatmapSetNode {
|
|||||||
bgColor = Color.white;
|
bgColor = Color.white;
|
||||||
textColor = Options.getSkin().getSongSelectActiveTextColor();
|
textColor = Options.getSkin().getSongSelectActiveTextColor();
|
||||||
} else
|
} else
|
||||||
bgColor = Utils.COLOR_BLUE_BUTTON;
|
bgColor = Colors.BLUE_BUTTON;
|
||||||
beatmap = beatmapSet.get(beatmapIndex);
|
beatmap = beatmapSet.get(beatmapIndex);
|
||||||
} else {
|
} else {
|
||||||
bgColor = Utils.COLOR_ORANGE_BUTTON;
|
bgColor = Colors.ORANGE_BUTTON;
|
||||||
beatmap = beatmapSet.get(0);
|
beatmap = beatmapSet.get(0);
|
||||||
}
|
}
|
||||||
bg.draw(x, y, bgColor);
|
bg.draw(x, y, bgColor);
|
||||||
|
|
||||||
float cx = x + (bg.getWidth() * 0.043f);
|
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
|
// draw grade
|
||||||
if (grade != Grade.NULL) {
|
if (grade != Grade.NULL) {
|
||||||
@@ -98,15 +107,58 @@ public class BeatmapSetNode {
|
|||||||
|
|
||||||
// draw text
|
// draw text
|
||||||
if (Options.useUnicodeMetadata()) { // load glyphs
|
if (Options.useUnicodeMetadata()) { // load glyphs
|
||||||
Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, null);
|
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.titleUnicode);
|
||||||
Utils.loadGlyphs(Utils.FONT_DEFAULT, null, beatmap.artistUnicode);
|
Fonts.loadGlyphs(Fonts.DEFAULT, beatmap.artistUnicode);
|
||||||
}
|
}
|
||||||
Utils.FONT_MEDIUM.drawString(cx, cy, beatmap.getTitle(), textColor);
|
Fonts.MEDIUM.drawString(cx, cy, beatmap.getTitle(), textColor);
|
||||||
Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 2,
|
Fonts.DEFAULT.drawString(cx, cy + Fonts.MEDIUM.getLineHeight() - 3,
|
||||||
String.format("%s // %s", beatmap.getArtist(), beatmap.creator), textColor);
|
String.format("%s // %s", beatmap.getArtist(), beatmap.creator), textColor);
|
||||||
if (expanded || beatmapSet.size() == 1)
|
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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ public enum BeatmapSortOrder {
|
|||||||
LENGTH (4, "Length", new LengthOrder());
|
LENGTH (4, "Length", new LengthOrder());
|
||||||
|
|
||||||
/** The ID of the sort (used for tab positioning). */
|
/** The ID of the sort (used for tab positioning). */
|
||||||
private int id;
|
private final int id;
|
||||||
|
|
||||||
/** The name of the sort. */
|
/** The name of the sort. */
|
||||||
private String name;
|
private final String name;
|
||||||
|
|
||||||
/** The comparator for the sort. */
|
/** 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). */
|
/** The tab associated with the sort (displayed in Song Menu screen). */
|
||||||
private MenuButton tab;
|
private MenuButton tab;
|
||||||
@@ -123,13 +123,11 @@ public enum BeatmapSortOrder {
|
|||||||
@Override
|
@Override
|
||||||
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
|
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
|
||||||
int vMax = 0, wMax = 0;
|
int vMax = 0, wMax = 0;
|
||||||
for (int i = 0, size = v.getBeatmapSet().size(); i < size; i++) {
|
for (Beatmap beatmap : v.getBeatmapSet()) {
|
||||||
Beatmap beatmap = v.getBeatmapSet().get(i);
|
|
||||||
if (beatmap.endTime > vMax)
|
if (beatmap.endTime > vMax)
|
||||||
vMax = beatmap.endTime;
|
vMax = beatmap.endTime;
|
||||||
}
|
}
|
||||||
for (int i = 0, size = w.getBeatmapSet().size(); i < size; i++) {
|
for (Beatmap beatmap : w.getBeatmapSet()) {
|
||||||
Beatmap beatmap = w.getBeatmapSet().get(i);
|
|
||||||
if (beatmap.endTime > wMax)
|
if (beatmap.endTime > wMax)
|
||||||
wMax = beatmap.endTime;
|
wMax = beatmap.endTime;
|
||||||
}
|
}
|
||||||
|
|||||||
295
src/itdelatrisu/opsu/beatmap/BeatmapWatchService.java
Normal 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; }
|
||||||
|
}
|
||||||
@@ -19,6 +19,11 @@
|
|||||||
package itdelatrisu.opsu.beatmap;
|
package itdelatrisu.opsu.beatmap;
|
||||||
|
|
||||||
import itdelatrisu.opsu.GameMod;
|
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.DecimalFormat;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
@@ -34,13 +39,6 @@ public class HitObject {
|
|||||||
TYPE_NEWCOMBO = 4, // not an object
|
TYPE_NEWCOMBO = 4, // not an object
|
||||||
TYPE_SPINNER = 8;
|
TYPE_SPINNER = 8;
|
||||||
|
|
||||||
/** Hit object type names. */
|
|
||||||
private static final String
|
|
||||||
CIRCLE = "circle",
|
|
||||||
SLIDER = "slider",
|
|
||||||
SPINNER = "spinner",
|
|
||||||
UNKNOWN = "unknown object";
|
|
||||||
|
|
||||||
/** Hit sound types (bits). */
|
/** Hit sound types (bits). */
|
||||||
public static final byte
|
public static final byte
|
||||||
SOUND_NORMAL = 0,
|
SOUND_NORMAL = 0,
|
||||||
@@ -101,9 +99,18 @@ public class HitObject {
|
|||||||
/** Hit sound type (SOUND_* bitmask). */
|
/** Hit sound type (SOUND_* bitmask). */
|
||||||
private byte hitSound;
|
private byte hitSound;
|
||||||
|
|
||||||
/** Hit sound addition (sampleSet, AdditionSampleSet, ?, ...). */
|
/** Hit sound addition (sampleSet, AdditionSampleSet). */
|
||||||
private byte[] addition;
|
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). */
|
/** Slider curve type (SLIDER_* constant). */
|
||||||
private char sliderType;
|
private char sliderType;
|
||||||
|
|
||||||
@@ -250,9 +257,17 @@ public class HitObject {
|
|||||||
// addition
|
// addition
|
||||||
if (tokens.length > additionIndex) {
|
if (tokens.length > additionIndex) {
|
||||||
String[] additionTokens = tokens[additionIndex].split(":");
|
String[] additionTokens = tokens[additionIndex].split(":");
|
||||||
this.addition = new byte[additionTokens.length];
|
if (additionTokens.length > 1) {
|
||||||
for (int j = 0; j < additionTokens.length; j++)
|
this.addition = new byte[2];
|
||||||
this.addition[j] = Byte.parseByte(additionTokens[j]);
|
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() {
|
public String getTypeName() {
|
||||||
if (isCircle())
|
if (isCircle())
|
||||||
return CIRCLE;
|
return "circle";
|
||||||
else if (isSlider())
|
else if (isSlider())
|
||||||
return SLIDER;
|
return "slider";
|
||||||
else if (isSpinner())
|
else if (isSpinner())
|
||||||
return SPINNER;
|
return "spinner";
|
||||||
else
|
else
|
||||||
return UNKNOWN;
|
return "unknown object type";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -386,6 +401,35 @@ public class HitObject {
|
|||||||
*/
|
*/
|
||||||
public float getPixelLength() { return pixelLength; }
|
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.
|
* Returns the spinner end time.
|
||||||
* @return the end time (in ms)
|
* @return the end time (in ms)
|
||||||
@@ -471,6 +515,21 @@ public class HitObject {
|
|||||||
return 0;
|
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.
|
* Sets the hit object index in the current stack.
|
||||||
* @param stack index in the stack
|
* @param stack index in the stack
|
||||||
@@ -529,9 +588,12 @@ public class HitObject {
|
|||||||
// addition
|
// addition
|
||||||
if (addition != null) {
|
if (addition != null) {
|
||||||
for (int i = 0; i < addition.length; i++) {
|
for (int i = 0; i < addition.length; i++) {
|
||||||
sb.append(addition[i]);
|
sb.append(addition[i]); sb.append(':');
|
||||||
sb.append(':');
|
|
||||||
}
|
}
|
||||||
|
sb.append(additionCustomSampleIndex); sb.append(':');
|
||||||
|
sb.append(additionHitSoundVolume); sb.append(':');
|
||||||
|
if (additionHitSound != null)
|
||||||
|
sb.append(additionHitSound);
|
||||||
} else
|
} else
|
||||||
sb.setLength(sb.length() - 1);
|
sb.setLength(sb.length() - 1);
|
||||||
|
|
||||||
|
|||||||
179
src/itdelatrisu/opsu/beatmap/ImageLoader.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/itdelatrisu/opsu/beatmap/LRUCache.java
Normal 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) {}
|
||||||
|
}
|
||||||
@@ -16,7 +16,10 @@
|
|||||||
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
|
* 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.File;
|
||||||
import java.io.FilenameFilter;
|
import java.io.FilenameFilter;
|
||||||
@@ -62,6 +65,9 @@ public class OszUnpacker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// unpack OSZs
|
// unpack OSZs
|
||||||
|
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
|
||||||
|
if (ws != null)
|
||||||
|
ws.pause();
|
||||||
for (File file : files) {
|
for (File file : files) {
|
||||||
fileIndex++;
|
fileIndex++;
|
||||||
String dirName = file.getName().substring(0, file.getName().lastIndexOf('.'));
|
String dirName = file.getName().substring(0, file.getName().lastIndexOf('.'));
|
||||||
@@ -73,6 +79,8 @@ public class OszUnpacker {
|
|||||||
dirs.add(songDir);
|
dirs.add(songDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (ws != null)
|
||||||
|
ws.resume();
|
||||||
|
|
||||||
fileIndex = -1;
|
fileIndex = -1;
|
||||||
files = null;
|
files = null;
|
||||||
@@ -43,7 +43,7 @@ public class BeatmapDB {
|
|||||||
* Current database version.
|
* Current database version.
|
||||||
* This value should be changed whenever the database format changes.
|
* 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. */
|
/** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
|
||||||
private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
|
private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
|
||||||
@@ -58,7 +58,7 @@ public class BeatmapDB {
|
|||||||
private static Connection connection;
|
private static Connection connection;
|
||||||
|
|
||||||
/** Query statements. */
|
/** 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. */
|
/** Current size of beatmap cache table. */
|
||||||
private static int cacheSize = -1;
|
private static int cacheSize = -1;
|
||||||
@@ -95,12 +95,13 @@ public class BeatmapDB {
|
|||||||
try {
|
try {
|
||||||
insertStmt = connection.prepareStatement(
|
insertStmt = connection.prepareStatement(
|
||||||
"INSERT INTO beatmaps VALUES (" +
|
"INSERT INTO beatmaps VALUES (" +
|
||||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " +
|
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
|
||||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||||
);
|
);
|
||||||
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
|
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
|
||||||
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
|
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
|
||||||
deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
|
deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
|
||||||
|
setStarsStmt = connection.prepareStatement("UPDATE beatmaps SET stars = ? WHERE dir = ? AND file = ?");
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
|
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, " +
|
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
|
||||||
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
|
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
|
||||||
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT, " +
|
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT, " +
|
||||||
"md5hash TEXT" +
|
"md5hash TEXT, stars REAL" +
|
||||||
"); " +
|
"); " +
|
||||||
"CREATE TABLE IF NOT EXISTS info (" +
|
"CREATE TABLE IF NOT EXISTS info (" +
|
||||||
"key TEXT NOT NULL UNIQUE, value TEXT" +
|
"key TEXT NOT NULL UNIQUE, value TEXT" +
|
||||||
@@ -342,6 +343,7 @@ public class BeatmapDB {
|
|||||||
stmt.setString(39, beatmap.breaksToString());
|
stmt.setString(39, beatmap.breaksToString());
|
||||||
stmt.setString(40, beatmap.comboToString());
|
stmt.setString(40, beatmap.comboToString());
|
||||||
stmt.setString(41, beatmap.md5Hash);
|
stmt.setString(41, beatmap.md5Hash);
|
||||||
|
stmt.setDouble(42, beatmap.starRating);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -484,6 +486,7 @@ public class BeatmapDB {
|
|||||||
beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
|
beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
|
||||||
beatmap.sliderBorderFromString(rs.getString(37));
|
beatmap.sliderBorderFromString(rs.getString(37));
|
||||||
beatmap.md5Hash = rs.getString(41);
|
beatmap.md5Hash = rs.getString(41);
|
||||||
|
beatmap.starRating = rs.getDouble(42);
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Exception 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.
|
* Closes the connection to the database.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ public class ScoreDB {
|
|||||||
"timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " +
|
"timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " +
|
||||||
"creator = ? AND version = ? AND hit300 = ? AND hit100 = ? AND hit50 = ? 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 " +
|
"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) {
|
} catch (SQLException e) {
|
||||||
ErrorHandler.error("Failed to prepare score statements.", e, true);
|
ErrorHandler.error("Failed to prepare score statements.", e, true);
|
||||||
@@ -222,6 +224,7 @@ public class ScoreDB {
|
|||||||
try {
|
try {
|
||||||
setStatementFields(insertStmt, data);
|
setStatementFields(insertStmt, data);
|
||||||
insertStmt.setString(18, data.replayString);
|
insertStmt.setString(18, data.replayString);
|
||||||
|
insertStmt.setString(19, data.playerName);
|
||||||
insertStmt.executeUpdate();
|
insertStmt.executeUpdate();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
ErrorHandler.error("Failed to save score to database.", e, true);
|
ErrorHandler.error("Failed to save score to database.", e, true);
|
||||||
@@ -238,6 +241,10 @@ public class ScoreDB {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setStatementFields(deleteScoreStmt, data);
|
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();
|
deleteScoreStmt.executeUpdate();
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
ErrorHandler.error("Failed to delete score from database.", e, true);
|
ErrorHandler.error("Failed to delete score from database.", e, true);
|
||||||
@@ -289,8 +296,6 @@ public class ScoreDB {
|
|||||||
stmt.setInt(15, data.combo);
|
stmt.setInt(15, data.combo);
|
||||||
stmt.setBoolean(16, data.perfect);
|
stmt.setBoolean(16, data.perfect);
|
||||||
stmt.setInt(17, data.mods);
|
stmt.setInt(17, data.mods);
|
||||||
stmt.setString(18, data.replayString);
|
|
||||||
stmt.setString(19, data.playerName);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ public class Download {
|
|||||||
/** Read timeout, in ms. */
|
/** Read timeout, in ms. */
|
||||||
public static final int READ_TIMEOUT = 10000;
|
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. */
|
/** Time between download speed and ETA updates, in ms. */
|
||||||
private static final int UPDATE_INTERVAL = 1000;
|
private static final int UPDATE_INTERVAL = 1000;
|
||||||
|
|
||||||
@@ -58,7 +61,7 @@ public class Download {
|
|||||||
ERROR ("Error");
|
ERROR ("Error");
|
||||||
|
|
||||||
/** The status name. */
|
/** The status name. */
|
||||||
private String name;
|
private final String name;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
@@ -172,13 +175,57 @@ public class Download {
|
|||||||
new Thread() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// open connection, get content length
|
// open connection
|
||||||
HttpURLConnection conn = null;
|
HttpURLConnection conn = null;
|
||||||
try {
|
try {
|
||||||
conn = (HttpURLConnection) url.openConnection();
|
URL downloadURL = url;
|
||||||
|
int redirectCount = 0;
|
||||||
|
boolean isRedirect = false;
|
||||||
|
do {
|
||||||
|
isRedirect = false;
|
||||||
|
|
||||||
|
conn = (HttpURLConnection) downloadURL.openConnection();
|
||||||
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
||||||
conn.setReadTimeout(READ_TIMEOUT);
|
conn.setReadTimeout(READ_TIMEOUT);
|
||||||
conn.setUseCaches(false);
|
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();
|
contentLength = conn.getContentLength();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
status = Status.ERROR;
|
status = Status.ERROR;
|
||||||
@@ -198,9 +245,18 @@ public class Download {
|
|||||||
fos = fileOutputStream;
|
fos = fileOutputStream;
|
||||||
status = Status.DOWNLOADING;
|
status = Status.DOWNLOADING;
|
||||||
updateReadSoFar();
|
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
|
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;
|
status = Status.COMPLETE;
|
||||||
rbc.close();
|
rbc.close();
|
||||||
fos.close();
|
fos.close();
|
||||||
@@ -273,7 +329,7 @@ public class Download {
|
|||||||
public long readSoFar() {
|
public long readSoFar() {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case COMPLETE:
|
case COMPLETE:
|
||||||
return contentLength;
|
return (rbc != null) ? rbc.getReadSoFar() : contentLength;
|
||||||
case DOWNLOADING:
|
case DOWNLOADING:
|
||||||
if (rbc != null)
|
if (rbc != null)
|
||||||
return rbc.getReadSoFar();
|
return rbc.getReadSoFar();
|
||||||
|
|||||||
@@ -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(); }
|
public int size() { return nodes.size(); }
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
|||||||
import itdelatrisu.opsu.downloads.Download.DownloadListener;
|
import itdelatrisu.opsu.downloads.Download.DownloadListener;
|
||||||
import itdelatrisu.opsu.downloads.Download.Status;
|
import itdelatrisu.opsu.downloads.Download.Status;
|
||||||
import itdelatrisu.opsu.downloads.servers.DownloadServer;
|
import itdelatrisu.opsu.downloads.servers.DownloadServer;
|
||||||
|
import itdelatrisu.opsu.ui.Colors;
|
||||||
|
import itdelatrisu.opsu.ui.Fonts;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@@ -42,19 +44,19 @@ public class DownloadNode {
|
|||||||
private Download download;
|
private Download download;
|
||||||
|
|
||||||
/** Beatmap set ID. */
|
/** Beatmap set ID. */
|
||||||
private int beatmapSetID;
|
private final int beatmapSetID;
|
||||||
|
|
||||||
/** Last updated date string. */
|
/** Last updated date string. */
|
||||||
private String date;
|
private final String date;
|
||||||
|
|
||||||
/** Song title. */
|
/** Song title. */
|
||||||
private String title, titleUnicode;
|
private final String title, titleUnicode;
|
||||||
|
|
||||||
/** Song artist. */
|
/** Song artist. */
|
||||||
private String artist, artistUnicode;
|
private final String artist, artistUnicode;
|
||||||
|
|
||||||
/** Beatmap creator. */
|
/** Beatmap creator. */
|
||||||
private String creator;
|
private final String creator;
|
||||||
|
|
||||||
/** Button drawing values. */
|
/** Button drawing values. */
|
||||||
private static float buttonBaseX, buttonBaseY, buttonWidth, buttonHeight, buttonOffset;
|
private static float buttonBaseX, buttonBaseY, buttonWidth, buttonHeight, buttonOffset;
|
||||||
@@ -68,12 +70,6 @@ public class DownloadNode {
|
|||||||
/** Container width. */
|
/** Container width. */
|
||||||
private static int containerWidth;
|
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.
|
* Initializes the base coordinates for drawing.
|
||||||
* @param width the container width
|
* @param width the container width
|
||||||
@@ -86,16 +82,16 @@ public class DownloadNode {
|
|||||||
buttonBaseX = width * 0.024f;
|
buttonBaseX = width * 0.024f;
|
||||||
buttonBaseY = height * 0.2f;
|
buttonBaseY = height * 0.2f;
|
||||||
buttonWidth = width * 0.7f;
|
buttonWidth = width * 0.7f;
|
||||||
buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2.1f;
|
buttonHeight = Fonts.MEDIUM.getLineHeight() * 2.1f;
|
||||||
buttonOffset = buttonHeight * 1.1f;
|
buttonOffset = buttonHeight * 1.1f;
|
||||||
|
|
||||||
// download info
|
// download info
|
||||||
infoBaseX = width * 0.75f;
|
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;
|
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;
|
float buttonHeight = height * 0.038f;
|
||||||
maxResultsShown = (int) ((height - buttonBaseY - searchY) / buttonOffset);
|
maxResultsShown = (int) ((height - buttonBaseY - searchY) / buttonOffset);
|
||||||
maxDownloadsShown = (int) ((height - infoBaseY - searchY - buttonHeight) / infoHeight);
|
maxDownloadsShown = (int) ((height - infoBaseY - searchY - buttonHeight) / infoHeight);
|
||||||
@@ -228,10 +224,9 @@ public class DownloadNode {
|
|||||||
* @param total the total number of buttons
|
* @param total the total number of buttons
|
||||||
*/
|
*/
|
||||||
public static void drawResultScrollbar(Graphics g, float position, float total) {
|
public static void drawResultScrollbar(Graphics g, float position, float total) {
|
||||||
UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset,
|
UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset, buttonBaseX, buttonBaseY,
|
||||||
buttonBaseX, buttonBaseY,
|
|
||||||
buttonWidth * 1.01f, (maxResultsShown-1) * buttonOffset + buttonHeight,
|
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) {
|
public static void drawDownloadScrollbar(Graphics g, float index, float total) {
|
||||||
UI.drawScrollbar(g, index, total, maxDownloadsShown * infoHeight, infoBaseX, infoBaseY,
|
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.
|
* 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,
|
public DownloadNode(int beatmapSetID, String date, String title,
|
||||||
String titleUnicode, String artist, String artistUnicode, String creator) {
|
String titleUnicode, String artist, String artistUnicode, String creator) {
|
||||||
@@ -273,7 +275,7 @@ public class DownloadNode {
|
|||||||
return;
|
return;
|
||||||
String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID);
|
String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID);
|
||||||
String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title);
|
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() {
|
download.setListener(new DownloadListener() {
|
||||||
@Override
|
@Override
|
||||||
public void completed() {
|
public void completed() {
|
||||||
@@ -285,8 +287,9 @@ public class DownloadNode {
|
|||||||
UI.sendBarNotification("Download failed due to a connection error.");
|
UI.sendBarNotification("Download failed due to a connection error.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.download = download;
|
||||||
if (Options.useUnicodeMetadata()) // load glyphs
|
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);
|
Download dl = DownloadList.get().getDownload(beatmapSetID);
|
||||||
|
|
||||||
// rectangle outline
|
// 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);
|
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
|
||||||
|
|
||||||
// map is already loaded
|
// map is already loaded
|
||||||
if (BeatmapSetList.get().containsBeatmapSetID(beatmapSetID)) {
|
if (BeatmapSetList.get().containsBeatmapSetID(beatmapSetID)) {
|
||||||
g.setColor(Utils.COLOR_BLUE_BUTTON);
|
g.setColor(Colors.BLUE_BUTTON);
|
||||||
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
|
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +364,7 @@ public class DownloadNode {
|
|||||||
if (dl != null) {
|
if (dl != null) {
|
||||||
float progress = dl.getProgress();
|
float progress = dl.getProgress();
|
||||||
if (progress > 0f) {
|
if (progress > 0f) {
|
||||||
g.setColor(Utils.COLOR_GREEN);
|
g.setColor(Colors.GREEN);
|
||||||
g.fillRect(buttonBaseX, y, buttonWidth * progress / 100f, buttonHeight);
|
g.fillRect(buttonBaseX, y, buttonWidth * progress / 100f, buttonHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,21 +376,22 @@ public class DownloadNode {
|
|||||||
|
|
||||||
// text
|
// text
|
||||||
// TODO: if the title/artist line is too long, shorten it (e.g. add "...") instead of just clipping
|
// TODO: if the title/artist line is too long, shorten it (e.g. add "...") instead of just clipping
|
||||||
if (Options.useUnicodeMetadata()) // load glyphs
|
if (Options.useUnicodeMetadata()) { // load glyphs
|
||||||
Utils.loadGlyphs(Utils.FONT_BOLD, getTitle(), getArtist());
|
Fonts.loadGlyphs(Fonts.BOLD, getTitle());
|
||||||
|
Fonts.loadGlyphs(Fonts.BOLD, getArtist());
|
||||||
|
}
|
||||||
// TODO can't set clip again or else old clip will be cleared
|
// 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());
|
//g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Fonts.DEFAULT.getWidth(creator)), Fonts.BOLD.getLineHeight());
|
||||||
Utils.FONT_BOLD.drawString(
|
Fonts.BOLD.drawString(
|
||||||
textX, y + marginY,
|
textX, y + marginY,
|
||||||
String.format("%s - %s%s", getArtist(), getTitle(),
|
String.format("%s - %s%s", getArtist(), getTitle(),
|
||||||
(dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white);
|
(dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white);
|
||||||
//g.clearClip();
|
//g.clearClip();
|
||||||
Utils.FONT_DEFAULT.drawString(
|
Fonts.DEFAULT.drawString(
|
||||||
textX, y + marginY + Utils.FONT_BOLD.getLineHeight(),
|
textX, y + marginY + Fonts.BOLD.getLineHeight(),
|
||||||
String.format("Last updated: %s", date), Color.white);
|
String.format("Last updated: %s", date), Color.white);
|
||||||
Utils.FONT_DEFAULT.drawString(
|
Fonts.DEFAULT.drawString(
|
||||||
edgeX - Utils.FONT_DEFAULT.getWidth(creator), y + marginY,
|
edgeX - Fonts.DEFAULT.getWidth(creator), y + marginY,
|
||||||
creator, Color.white);
|
creator, Color.white);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +403,7 @@ public class DownloadNode {
|
|||||||
* @param hover true if the mouse is hovering over this button
|
* @param hover true if the mouse is hovering over this button
|
||||||
*/
|
*/
|
||||||
public void drawDownload(Graphics g, float position, int id, boolean hover) {
|
public void drawDownload(Graphics g, float position, int id, boolean hover) {
|
||||||
|
Download download = this.download; // in case clearDownload() is called asynchronously
|
||||||
if (download == null) {
|
if (download == null) {
|
||||||
ErrorHandler.error("Trying to draw download information for button without Download object.", null, false);
|
ErrorHandler.error("Trying to draw download information for button without Download object.", null, false);
|
||||||
return;
|
return;
|
||||||
@@ -410,7 +415,7 @@ public class DownloadNode {
|
|||||||
float marginY = infoHeight * 0.04f;
|
float marginY = infoHeight * 0.04f;
|
||||||
|
|
||||||
// rectangle outline
|
// 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);
|
g.fillRect(infoBaseX, y, infoWidth, infoHeight);
|
||||||
|
|
||||||
// text
|
// text
|
||||||
@@ -428,8 +433,8 @@ public class DownloadNode {
|
|||||||
info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress,
|
info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress,
|
||||||
Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength()));
|
Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength()));
|
||||||
}
|
}
|
||||||
Utils.FONT_BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
|
Fonts.BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
|
||||||
Utils.FONT_DEFAULT.drawString(textX, y + marginY + Utils.FONT_BOLD.getLineHeight(), info, Color.white);
|
Fonts.DEFAULT.drawString(textX, y + marginY + Fonts.BOLD.getLineHeight(), info, Color.white);
|
||||||
|
|
||||||
// 'x' button
|
// 'x' button
|
||||||
if (hover) {
|
if (hover) {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.nio.channels.ReadableByteChannel;
|
|||||||
*/
|
*/
|
||||||
public class ReadableByteChannelWrapper implements ReadableByteChannel {
|
public class ReadableByteChannelWrapper implements ReadableByteChannel {
|
||||||
/** The wrapped ReadableByteChannel. */
|
/** The wrapped ReadableByteChannel. */
|
||||||
private ReadableByteChannel rbc;
|
private final ReadableByteChannel rbc;
|
||||||
|
|
||||||
/** The number of bytes read. */
|
/** The number of bytes read. */
|
||||||
private long bytesRead;
|
private long bytesRead;
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ public class Updater {
|
|||||||
UPDATE_FINAL ("Update queued.");
|
UPDATE_FINAL ("Update queued.");
|
||||||
|
|
||||||
/** The status description. */
|
/** The status description. */
|
||||||
private String description;
|
private final String description;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
@@ -194,9 +194,10 @@ public class Updater {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the program version against the version file on the update server.
|
* 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 {
|
public void checkForUpdates() throws IOException {
|
||||||
if (status != Status.INITIAL || System.getProperty("XDG") != null)
|
if (status != Status.INITIAL || Options.USE_XDG)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
status = Status.CHECKING;
|
status = Status.CHECKING;
|
||||||
|
|||||||
@@ -75,4 +75,7 @@ public abstract class DownloadServer {
|
|||||||
public String getPreviewURL(int beatmapSetID) {
|
public String getPreviewURL(int beatmapSetID) {
|
||||||
return String.format(PREVIEW_URL, beatmapSetID);
|
return String.format(PREVIEW_URL, beatmapSetID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() { return getName(); }
|
||||||
}
|
}
|
||||||
|
|||||||
202
src/itdelatrisu/opsu/downloads/servers/MengSkyServer.java
Normal 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; }
|
||||||
|
}
|
||||||
133
src/itdelatrisu/opsu/downloads/servers/MnetworkServer.java
Normal 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; }
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@ import org.json.JSONObject;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Download server: http://loli.al/
|
* Download server: http://loli.al/
|
||||||
|
* <p>
|
||||||
|
* <i>This server went offline in August 2015.</i>
|
||||||
*/
|
*/
|
||||||
public class OsuMirrorServer extends DownloadServer {
|
public class OsuMirrorServer extends DownloadServer {
|
||||||
/** Server name. */
|
/** Server name. */
|
||||||
|
|||||||
204
src/itdelatrisu/opsu/downloads/servers/YaSOnlineServer.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,7 @@ public class OsuReader {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the input stream.
|
* Closes the input stream.
|
||||||
|
* @throws IOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
public void close() throws IOException { reader.close(); }
|
public void close() throws IOException { reader.close(); }
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ public class OsuWriter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the output stream.
|
* Closes the output stream.
|
||||||
* @throws IOException
|
* @throws IOException if an I/O error occurs
|
||||||
*/
|
*/
|
||||||
public void close() throws IOException { writer.close(); }
|
public void close() throws IOException { writer.close(); }
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ import itdelatrisu.opsu.GameMod;
|
|||||||
import itdelatrisu.opsu.Options;
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.beatmap.HitObject;
|
import itdelatrisu.opsu.beatmap.HitObject;
|
||||||
|
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||||
import itdelatrisu.opsu.states.Game;
|
import itdelatrisu.opsu.states.Game;
|
||||||
|
import itdelatrisu.opsu.ui.Colors;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
import org.newdawn.slick.Color;
|
||||||
import org.newdawn.slick.GameContainer;
|
import org.newdawn.slick.GameContainer;
|
||||||
@@ -35,9 +37,6 @@ import org.newdawn.slick.Graphics;
|
|||||||
* Data type representing a circle object.
|
* Data type representing a circle object.
|
||||||
*/
|
*/
|
||||||
public class Circle implements GameObject {
|
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. */
|
/** The diameter of hit circles. */
|
||||||
private static float diameter;
|
private static float diameter;
|
||||||
|
|
||||||
@@ -62,11 +61,10 @@ public class Circle implements GameObject {
|
|||||||
/**
|
/**
|
||||||
* Initializes the Circle data type with map modifiers, images, and dimensions.
|
* Initializes the Circle data type with map modifiers, images, and dimensions.
|
||||||
* @param container the game container
|
* @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) {
|
public static void init(GameContainer container, float circleDiameter) {
|
||||||
diameter = (104 - (circleSize * 8));
|
diameter = circleDiameter * HitObject.getXMultiplier(); // convert from Osupixels (640x480)
|
||||||
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
|
||||||
int diameterInt = (int) diameter;
|
int diameterInt = (int) diameter;
|
||||||
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
|
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
|
||||||
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.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
|
@Override
|
||||||
public void draw(Graphics g, int trackPosition) {
|
public void draw(Graphics g, int trackPosition) {
|
||||||
int timeDiff = hitObject.getTime() - trackPosition;
|
int timeDiff = hitObject.getTime() - trackPosition;
|
||||||
float scale = timeDiff / (float) game.getApproachTime();
|
final int approachTime = game.getApproachTime();
|
||||||
float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
|
final int fadeInTime = game.getFadeInTime();
|
||||||
|
float scale = timeDiff / (float) approachTime;
|
||||||
float approachScale = 1 + scale * 3;
|
float approachScale = 1 + scale * 3;
|
||||||
|
float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime;
|
||||||
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
|
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
|
||||||
|
|
||||||
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
|
if (GameMod.HIDDEN.isActive()) {
|
||||||
Utils.COLOR_WHITE_FADE.a = color.a = alpha;
|
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.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
|
||||||
GameImage.HITCIRCLE.getImage().drawCentered(x, y, color);
|
GameImage.HITCIRCLE.getImage().drawCentered(x, y, color);
|
||||||
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
|
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
|
||||||
if (!overlayAboveNumber)
|
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,
|
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
|
||||||
GameImage.HITCIRCLE.getImage().getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
|
GameImage.HITCIRCLE.getImage().getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
|
||||||
if (overlayAboveNumber)
|
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
|
@Override
|
||||||
public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
|
public Vec2f getPointAt(int trackPosition) { return new Vec2f(x, y); }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getEndTime() { return hitObject.getTime(); }
|
public int getEndTime() { return hitObject.getTime(); }
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
package itdelatrisu.opsu.objects;
|
package itdelatrisu.opsu.objects;
|
||||||
|
|
||||||
import itdelatrisu.opsu.beatmap.HitObject;
|
import itdelatrisu.opsu.beatmap.HitObject;
|
||||||
|
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||||
|
|
||||||
import org.newdawn.slick.Graphics;
|
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; }
|
public boolean mousePressed(int x, int y, int trackPosition) { return false; }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
|
public Vec2f getPointAt(int trackPosition) { return new Vec2f(x, y); }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getEndTime() { return hitObject.getTime(); }
|
public int getEndTime() { return hitObject.getTime(); }
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
package itdelatrisu.opsu.objects;
|
package itdelatrisu.opsu.objects;
|
||||||
|
|
||||||
|
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||||
|
|
||||||
import org.newdawn.slick.Graphics;
|
import org.newdawn.slick.Graphics;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,9 +57,9 @@ public interface GameObject {
|
|||||||
/**
|
/**
|
||||||
* Returns the coordinates of the hit object at a given track position.
|
* Returns the coordinates of the hit object at a given track position.
|
||||||
* @param trackPosition the 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.
|
* Returns the end time of the hit object.
|
||||||
|
|||||||
@@ -26,11 +26,10 @@ import itdelatrisu.opsu.Options;
|
|||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||||
import itdelatrisu.opsu.beatmap.HitObject;
|
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.Curve;
|
||||||
import itdelatrisu.opsu.objects.curves.LinearBezier;
|
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||||
import itdelatrisu.opsu.states.Game;
|
import itdelatrisu.opsu.states.Game;
|
||||||
|
import itdelatrisu.opsu.ui.Colors;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
import org.newdawn.slick.Color;
|
||||||
import org.newdawn.slick.GameContainer;
|
import org.newdawn.slick.GameContainer;
|
||||||
@@ -56,9 +55,6 @@ public class Slider implements GameObject {
|
|||||||
/** The diameter of hit circles. */
|
/** The diameter of hit circles. */
|
||||||
private static float diameter;
|
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. */
|
/** The associated HitObject. */
|
||||||
private HitObject hitObject;
|
private HitObject hitObject;
|
||||||
|
|
||||||
@@ -113,22 +109,21 @@ public class Slider implements GameObject {
|
|||||||
/**
|
/**
|
||||||
* Initializes the Slider data type with images and dimensions.
|
* Initializes the Slider data type with images and dimensions.
|
||||||
* @param container the game container
|
* @param container the game container
|
||||||
* @param circleSize the map's circleSize value
|
* @param circleDiameter the circle diameter
|
||||||
* @param beatmap the associated beatmap
|
* @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();
|
containerWidth = container.getWidth();
|
||||||
containerHeight = container.getHeight();
|
containerHeight = container.getHeight();
|
||||||
|
|
||||||
diameter = (104 - (circleSize * 8));
|
diameter = circleDiameter * HitObject.getXMultiplier(); // convert from Osupixels (640x480)
|
||||||
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
|
||||||
int diameterInt = (int) diameter;
|
int diameterInt = (int) diameter;
|
||||||
|
|
||||||
followRadius = diameter / 2 * 3f;
|
followRadius = diameter / 2 * 3f;
|
||||||
|
|
||||||
// slider ball
|
// slider ball
|
||||||
if (GameImage.SLIDER_BALL.hasSkinImages() ||
|
if (GameImage.SLIDER_BALL.hasBeatmapSkinImages() ||
|
||||||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
|
(!GameImage.SLIDER_BALL.hasBeatmapSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
|
||||||
sliderBallImages = GameImage.SLIDER_BALL.getImages();
|
sliderBallImages = GameImage.SLIDER_BALL.getImages();
|
||||||
else
|
else
|
||||||
sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
|
sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
|
||||||
@@ -160,7 +155,7 @@ public class Slider implements GameObject {
|
|||||||
updatePosition();
|
updatePosition();
|
||||||
|
|
||||||
// slider time calculations
|
// slider time calculations
|
||||||
this.sliderTime = game.getBeatLength() * (hitObject.getPixelLength() / sliderMultiplier) / 100f;
|
this.sliderTime = hitObject.getSliderTime(sliderMultiplier, game.getBeatLength());
|
||||||
this.sliderTimeTotal = sliderTime * hitObject.getRepeatCount();
|
this.sliderTimeTotal = sliderTime * hitObject.getRepeatCount();
|
||||||
|
|
||||||
// ticks
|
// ticks
|
||||||
@@ -178,36 +173,46 @@ public class Slider implements GameObject {
|
|||||||
@Override
|
@Override
|
||||||
public void draw(Graphics g, int trackPosition) {
|
public void draw(Graphics g, int trackPosition) {
|
||||||
int timeDiff = hitObject.getTime() - trackPosition;
|
int timeDiff = hitObject.getTime() - trackPosition;
|
||||||
float scale = timeDiff / (float) game.getApproachTime();
|
final int approachTime = game.getApproachTime();
|
||||||
float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
|
final int fadeInTime = game.getFadeInTime();
|
||||||
|
float scale = timeDiff / (float) approachTime;
|
||||||
float approachScale = 1 + scale * 3;
|
float approachScale = 1 + scale * 3;
|
||||||
|
float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime;
|
||||||
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
|
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
|
||||||
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
|
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
|
||||||
|
|
||||||
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
|
float oldAlpha = Colors.WHITE_FADE.a;
|
||||||
Utils.COLOR_WHITE_FADE.a = color.a = alpha;
|
Colors.WHITE_FADE.a = color.a = alpha;
|
||||||
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
|
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
|
||||||
Image hitCircle = GameImage.HITCIRCLE.getImage();
|
Image hitCircle = GameImage.HITCIRCLE.getImage();
|
||||||
float[] endPos = curve.pointAt(1);
|
Vec2f endPos = curve.pointAt(1);
|
||||||
|
|
||||||
curve.draw(color);
|
curve.draw(color);
|
||||||
color.a = alpha;
|
color.a = alpha;
|
||||||
|
|
||||||
// end circle
|
// end circle
|
||||||
hitCircle.drawCentered(endPos[0], endPos[1], color);
|
hitCircle.drawCentered(endPos.x, endPos.y, color);
|
||||||
hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE);
|
hitCircleOverlay.drawCentered(endPos.x, endPos.y, Colors.WHITE_FADE);
|
||||||
|
|
||||||
// start circle
|
// start circle
|
||||||
hitCircle.drawCentered(x, y, color);
|
hitCircle.drawCentered(x, y, color);
|
||||||
if (!overlayAboveNumber)
|
if (!overlayAboveNumber)
|
||||||
hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
|
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
|
||||||
|
|
||||||
// ticks
|
// ticks
|
||||||
if (ticksT != null) {
|
if (ticksT != null) {
|
||||||
Image tick = GameImage.SLIDER_TICK.getImage();
|
Image tick = GameImage.SLIDER_TICK.getImage();
|
||||||
for (int i = 0; i < ticksT.length; i++) {
|
for (int i = 0; i < ticksT.length; i++) {
|
||||||
float[] c = curve.pointAt(ticksT[i]);
|
Vec2f c = curve.pointAt(ticksT[i]);
|
||||||
tick.drawCentered(c[0], c[1], Utils.COLOR_WHITE_FADE);
|
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)
|
if (sliderClickedInitial)
|
||||||
@@ -216,7 +221,7 @@ public class Slider implements GameObject {
|
|||||||
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
|
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
|
||||||
hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
|
hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
|
||||||
if (overlayAboveNumber)
|
if (overlayAboveNumber)
|
||||||
hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
|
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
|
||||||
|
|
||||||
// repeats
|
// repeats
|
||||||
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) {
|
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) {
|
||||||
@@ -232,7 +237,7 @@ public class Slider implements GameObject {
|
|||||||
if (tcurRepeat % 2 == 0) {
|
if (tcurRepeat % 2 == 0) {
|
||||||
// last circle
|
// last circle
|
||||||
arrow.setRotation(curve.getEndAngle());
|
arrow.setRotation(curve.getEndAngle());
|
||||||
arrow.drawCentered(endPos[0], endPos[1]);
|
arrow.drawCentered(endPos.x, endPos.y);
|
||||||
} else {
|
} else {
|
||||||
// first circle
|
// first circle
|
||||||
arrow.setRotation(curve.getStartAngle());
|
arrow.setRotation(curve.getStartAngle());
|
||||||
@@ -243,6 +248,7 @@ public class Slider implements GameObject {
|
|||||||
|
|
||||||
if (timeDiff >= 0) {
|
if (timeDiff >= 0) {
|
||||||
// approach circle
|
// approach circle
|
||||||
|
if (!GameMod.HIDDEN.isActive())
|
||||||
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
|
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
|
||||||
} else {
|
} else {
|
||||||
// Since update() might not have run before drawing during a replay, the
|
// Since update() might not have run before drawing during a replay, the
|
||||||
@@ -250,33 +256,33 @@ public class Slider implements GameObject {
|
|||||||
if (sliderTime == 0)
|
if (sliderTime == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
float[] c = curve.pointAt(getT(trackPosition, false));
|
Vec2f c = curve.pointAt(getT(trackPosition, false));
|
||||||
float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
|
Vec2f c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
|
||||||
|
|
||||||
float t = getT(trackPosition, false);
|
float t = getT(trackPosition, false);
|
||||||
// float dis = hitObject.getPixelLength() * HitObject.getXMultiplier() * (t - (int) t);
|
// float dis = hitObject.getPixelLength() * HitObject.getXMultiplier() * (t - (int) t);
|
||||||
// Image sliderBallFrame = sliderBallImages[(int) (dis / (diameter * Math.PI) * 30) % sliderBallImages.length];
|
// Image sliderBallFrame = sliderBallImages[(int) (dis / (diameter * Math.PI) * 30) % sliderBallImages.length];
|
||||||
Image sliderBallFrame = sliderBallImages[(int) (t * sliderTime * 60 / 1000) % 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.setRotation(angle);
|
||||||
sliderBallFrame.drawCentered(c[0], c[1]);
|
sliderBallFrame.drawCentered(c.x, c.y);
|
||||||
|
|
||||||
// follow circle
|
// follow circle
|
||||||
if (followCircleActive) {
|
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
|
// "flashlight" mod: dim the screen
|
||||||
if (GameMod.FLASHLIGHT.isActive()) {
|
if (GameMod.FLASHLIGHT.isActive()) {
|
||||||
float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a;
|
float oldAlphaBlack = Colors.BLACK_ALPHA.a;
|
||||||
Utils.COLOR_BLACK_ALPHA.a = 0.75f;
|
Colors.BLACK_ALPHA.a = 0.75f;
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
g.fillRect(0, 0, containerWidth, containerHeight);
|
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;
|
float cx, cy;
|
||||||
HitObjectType type;
|
HitObjectType type;
|
||||||
if (currentRepeats % 2 == 0) { // last circle
|
if (currentRepeats % 2 == 0) { // last circle
|
||||||
float[] lastPos = curve.pointAt(1);
|
Vec2f lastPos = curve.pointAt(1);
|
||||||
cx = lastPos[0];
|
cx = lastPos.x;
|
||||||
cy = lastPos[1];
|
cy = lastPos.y;
|
||||||
type = HitObjectType.SLIDER_LAST;
|
type = HitObjectType.SLIDER_LAST;
|
||||||
} else { // first circle
|
} else { // first circle
|
||||||
cx = x;
|
cx = x;
|
||||||
@@ -429,8 +435,8 @@ public class Slider implements GameObject {
|
|||||||
|
|
||||||
// check if cursor pressed and within end circle
|
// check if cursor pressed and within end circle
|
||||||
if (keyPressed || GameMod.RELAX.isActive()) {
|
if (keyPressed || GameMod.RELAX.isActive()) {
|
||||||
float[] c = curve.pointAt(getT(trackPosition, false));
|
Vec2f c = curve.pointAt(getT(trackPosition, false));
|
||||||
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
double distance = Math.hypot(c.x - mouseX, c.y - mouseY);
|
||||||
if (distance < followRadius)
|
if (distance < followRadius)
|
||||||
sliderHeldToEnd = true;
|
sliderHeldToEnd = true;
|
||||||
}
|
}
|
||||||
@@ -473,12 +479,11 @@ public class Slider implements GameObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// holding slider...
|
// holding slider...
|
||||||
float[] c = curve.pointAt(getT(trackPosition, false));
|
Vec2f c = curve.pointAt(getT(trackPosition, false));
|
||||||
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
double distance = Math.hypot(c.x - mouseX, c.y - mouseY);
|
||||||
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followRadius) || isAutoMod) {
|
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followRadius) || isAutoMod) {
|
||||||
// mouse pressed and within follow circle
|
// mouse pressed and within follow circle
|
||||||
followCircleActive = true;
|
followCircleActive = true;
|
||||||
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
|
|
||||||
|
|
||||||
// held during new repeat
|
// held during new repeat
|
||||||
if (isNewRepeat) {
|
if (isNewRepeat) {
|
||||||
@@ -489,14 +494,14 @@ public class Slider implements GameObject {
|
|||||||
curve.getX(lastIndex), curve.getY(lastIndex), hitObject, currentRepeats);
|
curve.getX(lastIndex), curve.getY(lastIndex), hitObject, currentRepeats);
|
||||||
} else // first circle
|
} else // first circle
|
||||||
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
|
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
|
||||||
c[0], c[1], hitObject, currentRepeats);
|
c.x, c.y, hitObject, currentRepeats);
|
||||||
}
|
}
|
||||||
|
|
||||||
// held during new tick
|
// held during new tick
|
||||||
if (isNewTick) {
|
if (isNewTick) {
|
||||||
ticksHit++;
|
ticksHit++;
|
||||||
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER10,
|
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER10,
|
||||||
c[0], c[1], hitObject, currentRepeats);
|
c.x, c.y, hitObject, currentRepeats);
|
||||||
}
|
}
|
||||||
|
|
||||||
// held near end of slider
|
// held near end of slider
|
||||||
@@ -518,22 +523,16 @@ public class Slider implements GameObject {
|
|||||||
public void updatePosition() {
|
public void updatePosition() {
|
||||||
this.x = hitObject.getScaledX();
|
this.x = hitObject.getScaledX();
|
||||||
this.y = hitObject.getScaledY();
|
this.y = hitObject.getScaledY();
|
||||||
|
this.curve = hitObject.getSliderCurve(true);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float[] getPointAt(int trackPosition) {
|
public Vec2f getPointAt(int trackPosition) {
|
||||||
if (trackPosition <= hitObject.getTime())
|
if (trackPosition <= hitObject.getTime())
|
||||||
return new float[] { x, y };
|
return new Vec2f(x, y);
|
||||||
else if (trackPosition >= hitObject.getTime() + sliderTimeTotal) {
|
else if (trackPosition >= hitObject.getTime() + sliderTimeTotal) {
|
||||||
if (hitObject.getRepeatCount() % 2 == 0)
|
if (hitObject.getRepeatCount() % 2 == 0)
|
||||||
return new float[] { x, y };
|
return new Vec2f(x, y);
|
||||||
else
|
else
|
||||||
return curve.pointAt(1);
|
return curve.pointAt(1);
|
||||||
} else
|
} else
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ import itdelatrisu.opsu.Utils;
|
|||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.audio.SoundEffect;
|
import itdelatrisu.opsu.audio.SoundEffect;
|
||||||
import itdelatrisu.opsu.beatmap.HitObject;
|
import itdelatrisu.opsu.beatmap.HitObject;
|
||||||
|
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||||
import itdelatrisu.opsu.states.Game;
|
import itdelatrisu.opsu.states.Game;
|
||||||
|
import itdelatrisu.opsu.ui.Colors;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
import org.newdawn.slick.Color;
|
||||||
import org.newdawn.slick.GameContainer;
|
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. */
|
/** The amount of time, in milliseconds, before another velocity is stored. */
|
||||||
private static final float DELTA_UPDATE_TIME = 1000 / 60f;
|
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) */
|
/** Angle mod multipliers: "auto" (477rpm), "spun out" (287rpm) */
|
||||||
private static final float
|
private static final float
|
||||||
AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI;
|
AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI;
|
||||||
@@ -69,6 +68,9 @@ public class Spinner implements GameObject {
|
|||||||
/** The associated HitObject. */
|
/** The associated HitObject. */
|
||||||
private HitObject hitObject;
|
private HitObject hitObject;
|
||||||
|
|
||||||
|
/** The associated Game object. */
|
||||||
|
private Game game;
|
||||||
|
|
||||||
/** The associated GameData object. */
|
/** The associated GameData object. */
|
||||||
private GameData data;
|
private GameData data;
|
||||||
|
|
||||||
@@ -124,6 +126,7 @@ public class Spinner implements GameObject {
|
|||||||
*/
|
*/
|
||||||
public Spinner(HitObject hitObject, Game game, GameData data) {
|
public Spinner(HitObject hitObject, Game game, GameData data) {
|
||||||
this.hitObject = hitObject;
|
this.hitObject = hitObject;
|
||||||
|
this.game = game;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -162,7 +165,7 @@ public class Spinner implements GameObject {
|
|||||||
final int maxVel = 48;
|
final int maxVel = 48;
|
||||||
final int minTime = 2000;
|
final int minTime = 2000;
|
||||||
final int maxTime = 5000;
|
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);
|
* (maxVel - minVel) / (maxTime - minTime) + minVel, minVel, maxVel);
|
||||||
storedDeltaAngle = new float[maxStoredDeltaAngles];
|
storedDeltaAngle = new float[maxStoredDeltaAngles];
|
||||||
|
|
||||||
@@ -175,20 +178,21 @@ public class Spinner implements GameObject {
|
|||||||
public void draw(Graphics g, int trackPosition) {
|
public void draw(Graphics g, int trackPosition) {
|
||||||
// only draw spinners shortly before start time
|
// only draw spinners shortly before start time
|
||||||
int timeDiff = hitObject.getTime() - trackPosition;
|
int timeDiff = hitObject.getTime() - trackPosition;
|
||||||
if (timeDiff - FADE_IN_TIME > 0)
|
final int fadeInTime = game.getFadeInTime();
|
||||||
|
if (timeDiff - fadeInTime > 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
boolean spinnerComplete = (rotations >= rotationsNeeded);
|
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
|
// darken screen
|
||||||
if (Options.getSkin().isSpinnerFadePlayfield()) {
|
if (Options.getSkin().isSpinnerFadePlayfield()) {
|
||||||
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
|
float oldAlpha = Colors.BLACK_ALPHA.a;
|
||||||
if (timeDiff > 0)
|
if (timeDiff > 0)
|
||||||
Utils.COLOR_BLACK_ALPHA.a *= alpha;
|
Colors.BLACK_ALPHA.a *= alpha;
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
g.fillRect(0, 0, width, height);
|
g.fillRect(0, 0, width, height);
|
||||||
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
|
Colors.BLACK_ALPHA.a = oldAlpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
// rpm
|
// rpm
|
||||||
@@ -210,13 +214,15 @@ public class Spinner implements GameObject {
|
|||||||
spinnerMetreSub.draw(0, height - spinnerMetreSub.getHeight());
|
spinnerMetreSub.draw(0, height - spinnerMetreSub.getHeight());
|
||||||
|
|
||||||
// main spinner elements
|
// 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().setAlpha(alpha);
|
||||||
GameImage.SPINNER_CIRCLE.getImage().setRotation(drawRotation * 360f);
|
GameImage.SPINNER_CIRCLE.getImage().setRotation(drawRotation * 360f);
|
||||||
GameImage.SPINNER_CIRCLE.getImage().drawCentered(width / 2, height / 2);
|
GameImage.SPINNER_CIRCLE.getImage().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);
|
Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale);
|
||||||
approachCircleScaled.setAlpha(alpha);
|
approachCircleScaled.setAlpha(alpha);
|
||||||
approachCircleScaled.drawCentered(width / 2, height / 2);
|
approachCircleScaled.drawCentered(width / 2, height / 2);
|
||||||
|
}
|
||||||
GameImage.SPINNER_SPIN.getImage().setAlpha(alpha);
|
GameImage.SPINNER_SPIN.getImage().setAlpha(alpha);
|
||||||
GameImage.SPINNER_SPIN.getImage().drawCentered(width / 2, height * 3 / 4);
|
GameImage.SPINNER_SPIN.getImage().drawCentered(width / 2, height * 3 / 4);
|
||||||
|
|
||||||
@@ -342,7 +348,7 @@ public class Spinner implements GameObject {
|
|||||||
public void updatePosition() {}
|
public void updatePosition() {}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float[] getPointAt(int trackPosition) {
|
public Vec2f getPointAt(int trackPosition) {
|
||||||
// get spinner time
|
// get spinner time
|
||||||
int timeDiff;
|
int timeDiff;
|
||||||
float x = hitObject.getScaledX(), y = hitObject.getScaledY();
|
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 multiplier = (GameMod.AUTO.isActive()) ? AUTO_MULTIPLIER : SPUN_OUT_MULTIPLIER;
|
||||||
float angle = (timeDiff * multiplier) - HALF_PI;
|
float angle = (timeDiff * multiplier) - HALF_PI;
|
||||||
final float r = height / 10f;
|
final float r = height / 10f;
|
||||||
return new float[] {
|
return new Vec2f((float) (x + r * Math.cos(angle)), (float) (y + r * Math.sin(angle)));
|
||||||
(float) (x + r * Math.cos(angle)),
|
|
||||||
(float) (y + r * Math.sin(angle))
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -18,14 +18,10 @@
|
|||||||
|
|
||||||
package itdelatrisu.opsu.objects.curves;
|
package itdelatrisu.opsu.objects.curves;
|
||||||
|
|
||||||
import itdelatrisu.opsu.ErrorHandler;
|
|
||||||
import itdelatrisu.opsu.beatmap.HitObject;
|
import itdelatrisu.opsu.beatmap.HitObject;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
|
||||||
import org.newdawn.slick.SlickException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of Catmull Curve with equidistant points.
|
* Representation of Catmull Curve with equidistant points.
|
||||||
*
|
*
|
||||||
@@ -35,10 +31,18 @@ public class CatmullCurve extends EqualDistanceMultiCurve {
|
|||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param hitObject the associated HitObject
|
* @param hitObject the associated HitObject
|
||||||
* @param color the color of this curve
|
|
||||||
*/
|
*/
|
||||||
public CatmullCurve(HitObject hitObject, Color color) {
|
public CatmullCurve(HitObject hitObject) {
|
||||||
super(hitObject, color);
|
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>();
|
LinkedList<CurveType> catmulls = new LinkedList<CurveType>();
|
||||||
int ncontrolPoints = hitObject.getSliderX().length + 1;
|
int ncontrolPoints = hitObject.getSliderX().length + 1;
|
||||||
LinkedList<Vec2f> points = new LinkedList<Vec2f>(); // temporary list of points to separate different curves
|
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++) {
|
for (int i = 0; i < ncontrolPoints; i++) {
|
||||||
points.addLast(new Vec2f(getX(i), getY(i)));
|
points.addLast(new Vec2f(getX(i), getY(i)));
|
||||||
if (points.size() >= 4) {
|
if (points.size() >= 4) {
|
||||||
try {
|
|
||||||
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
|
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
|
||||||
} catch (SlickException e) {
|
|
||||||
ErrorHandler.error(null, e, true);
|
|
||||||
}
|
|
||||||
points.removeFirst();
|
points.removeFirst();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (getX(ncontrolPoints - 1) != getX(ncontrolPoints - 2)
|
if (getX(ncontrolPoints - 1) != getX(ncontrolPoints - 2) ||
|
||||||
||getY(ncontrolPoints - 1) != getY(ncontrolPoints - 2))
|
getY(ncontrolPoints - 1) != getY(ncontrolPoints - 2))
|
||||||
points.addLast(new Vec2f(getX(ncontrolPoints - 1), getY(ncontrolPoints - 1)));
|
points.addLast(new Vec2f(getX(ncontrolPoints - 1), getY(ncontrolPoints - 1)));
|
||||||
if (points.size() >= 4) {
|
if (points.size() >= 4)
|
||||||
try {
|
|
||||||
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
|
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
|
||||||
} catch (SlickException e) {
|
|
||||||
ErrorHandler.error(null, e, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(catmulls);
|
init(catmulls);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
|
|
||||||
package itdelatrisu.opsu.objects.curves;
|
package itdelatrisu.opsu.objects.curves;
|
||||||
|
|
||||||
import org.newdawn.slick.SlickException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of a Centripetal Catmull–Rom spline.
|
* Representation of a Centripetal Catmull–Rom spline.
|
||||||
* (Currently not technically Centripetal Catmull–Rom.)
|
* (Currently not technically Centripetal Catmull–Rom.)
|
||||||
@@ -37,11 +35,10 @@ public class CentripetalCatmullRom extends CurveType {
|
|||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param points the control points of the curve
|
* @param points the control points of the curve
|
||||||
* @throws SlickException
|
|
||||||
*/
|
*/
|
||||||
protected CentripetalCatmullRom(Vec2f[] points) throws SlickException {
|
protected CentripetalCatmullRom(Vec2f[] points) {
|
||||||
if (points.length != 4)
|
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;
|
this.points = points;
|
||||||
time = new float[4];
|
time = new float[4];
|
||||||
|
|||||||
@@ -18,11 +18,9 @@
|
|||||||
|
|
||||||
package itdelatrisu.opsu.objects.curves;
|
package itdelatrisu.opsu.objects.curves;
|
||||||
|
|
||||||
import itdelatrisu.opsu.ErrorHandler;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.beatmap.HitObject;
|
import itdelatrisu.opsu.beatmap.HitObject;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of a curve along a Circumscribed Circle of three points.
|
* Representation of a curve along a Circumscribed Circle of three points.
|
||||||
* http://en.wikipedia.org/wiki/Circumscribed_circle
|
* http://en.wikipedia.org/wiki/Circumscribed_circle
|
||||||
@@ -53,10 +51,18 @@ public class CircumscribedCircle extends Curve {
|
|||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param hitObject the associated HitObject
|
* @param hitObject the associated HitObject
|
||||||
* @param color the color of this curve
|
|
||||||
*/
|
*/
|
||||||
public CircumscribedCircle(HitObject hitObject, Color color) {
|
public CircumscribedCircle(HitObject hitObject) {
|
||||||
super(hitObject, color);
|
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
|
// construct the three points
|
||||||
this.start = new Vec2f(getX(0), getY(0));
|
this.start = new Vec2f(getX(0), getY(0));
|
||||||
@@ -70,8 +76,6 @@ public class CircumscribedCircle extends Curve {
|
|||||||
Vec2f norb = mid.cpy().sub(end).nor();
|
Vec2f norb = mid.cpy().sub(end).nor();
|
||||||
|
|
||||||
this.circleCenter = intersect(mida, nora, midb, norb);
|
this.circleCenter = intersect(mida, nora, midb, norb);
|
||||||
if (circleCenter == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// find the angles relative to the circle center
|
// find the angles relative to the circle center
|
||||||
Vec2f startAngPoint = start.cpy().sub(circleCenter);
|
Vec2f startAngPoint = start.cpy().sub(circleCenter);
|
||||||
@@ -92,13 +96,8 @@ public class CircumscribedCircle extends Curve {
|
|||||||
startAng -= TWO_PI;
|
startAng -= TWO_PI;
|
||||||
else if (Math.abs(startAng - (endAng - TWO_PI)) < TWO_PI && isIn(startAng, midAng, endAng - (TWO_PI)))
|
else if (Math.abs(startAng - (endAng - TWO_PI)) < TWO_PI && isIn(startAng, midAng, endAng - (TWO_PI)))
|
||||||
endAng -= TWO_PI;
|
endAng -= TWO_PI;
|
||||||
else {
|
else
|
||||||
ErrorHandler.error(
|
throw new RuntimeException(String.format("Cannot find angles between midAng (%.3f %.3f %.3f).", startAng, midAng, endAng));
|
||||||
String.format("Cannot find angles between midAng (%.3f %.3f %.3f).",
|
|
||||||
startAng, midAng, endAng), null, true
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// find an angle with an arc length of pixelLength along this circle
|
// find an angle with an arc length of pixelLength along this circle
|
||||||
@@ -116,10 +115,8 @@ public class CircumscribedCircle extends Curve {
|
|||||||
// calculate points
|
// calculate points
|
||||||
float step = hitObject.getPixelLength() / CURVE_POINTS_SEPERATION;
|
float step = hitObject.getPixelLength() / CURVE_POINTS_SEPERATION;
|
||||||
curve = new Vec2f[(int) step + 1];
|
curve = new Vec2f[(int) step + 1];
|
||||||
for (int i = 0; i < curve.length; i++) {
|
for (int i = 0; i < curve.length; i++)
|
||||||
float[] xy = pointAt(i / step);
|
curve[i] = pointAt(i / step);
|
||||||
curve[i] = new Vec2f(xy[0], xy[1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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);
|
//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;
|
float des = tb.x * ta.y - tb.y * ta.x;
|
||||||
if (Math.abs(des) < 0.00001f) {
|
if (Math.abs(des) < 0.00001f)
|
||||||
ErrorHandler.error("Vectors are parallel.", null, true);
|
throw new RuntimeException("Vectors are parallel.");
|
||||||
return null;
|
|
||||||
}
|
|
||||||
float u = ((b.y - a.y) * ta.x + (a.x - b.x) * ta.y) / des;
|
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);
|
return b.cpy().add(tb.x * u, tb.y * u);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float[] pointAt(float t) {
|
public Vec2f pointAt(float t) {
|
||||||
float ang = lerp(startAng, endAng, t);
|
float ang = Utils.lerp(startAng, endAng, t);
|
||||||
return new float[] {
|
return new Vec2f(
|
||||||
(float) (Math.cos(ang) * radius + circleCenter.x),
|
(float) (Math.cos(ang) * radius + circleCenter.x),
|
||||||
(float) (Math.sin(ang) * radius + circleCenter.y)
|
(float) (Math.sin(ang) * radius + circleCenter.y)
|
||||||
};
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ package itdelatrisu.opsu.objects.curves;
|
|||||||
|
|
||||||
import itdelatrisu.opsu.GameImage;
|
import itdelatrisu.opsu.GameImage;
|
||||||
import itdelatrisu.opsu.Options;
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.Utils;
|
|
||||||
import itdelatrisu.opsu.beatmap.HitObject;
|
import itdelatrisu.opsu.beatmap.HitObject;
|
||||||
import itdelatrisu.opsu.render.CurveRenderState;
|
import itdelatrisu.opsu.render.CurveRenderState;
|
||||||
import itdelatrisu.opsu.skins.Skin;
|
import itdelatrisu.opsu.skins.Skin;
|
||||||
|
import itdelatrisu.opsu.ui.Colors;
|
||||||
|
|
||||||
import org.lwjgl.opengl.ContextCapabilities;
|
import org.lwjgl.opengl.ContextCapabilities;
|
||||||
import org.lwjgl.opengl.GLContext;
|
import org.lwjgl.opengl.GLContext;
|
||||||
@@ -64,14 +64,21 @@ public abstract class Curve {
|
|||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param hitObject the associated HitObject
|
* @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.hitObject = hitObject;
|
||||||
|
if (scaled) {
|
||||||
this.x = hitObject.getScaledX();
|
this.x = hitObject.getScaledX();
|
||||||
this.y = hitObject.getScaledY();
|
this.y = hitObject.getScaledY();
|
||||||
this.sliderX = hitObject.getScaledSliderX();
|
this.sliderX = hitObject.getScaledSliderX();
|
||||||
this.sliderY = hitObject.getScaledSliderY();
|
this.sliderY = hitObject.getScaledSliderY();
|
||||||
|
} else {
|
||||||
|
this.x = hitObject.getX();
|
||||||
|
this.y = hitObject.getY();
|
||||||
|
this.sliderX = hitObject.getSliderX();
|
||||||
|
this.sliderY = hitObject.getSliderY();
|
||||||
|
}
|
||||||
this.renderState = null;
|
this.renderState = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,28 +87,28 @@ public abstract class Curve {
|
|||||||
* Should be called before any curves are drawn.
|
* Should be called before any curves are drawn.
|
||||||
* @param width the container width
|
* @param width the container width
|
||||||
* @param height the container height
|
* @param height the container height
|
||||||
* @param circleSize the circle size
|
* @param circleDiameter the circle diameter
|
||||||
* @param borderColor the curve border color
|
* @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;
|
Curve.borderColor = borderColor;
|
||||||
|
|
||||||
ContextCapabilities capabilities = GLContext.getCapabilities();
|
ContextCapabilities capabilities = GLContext.getCapabilities();
|
||||||
mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL32;
|
mmsliderSupported = capabilities.GL_EXT_framebuffer_object;
|
||||||
if (mmsliderSupported)
|
if (mmsliderSupported)
|
||||||
CurveRenderState.init(width, height, circleSize);
|
CurveRenderState.init(width, height, circleDiameter);
|
||||||
else {
|
else {
|
||||||
if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER)
|
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.
|
* Returns the point on the curve at a value t.
|
||||||
* @param t the t value [0, 1]
|
* @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.
|
* Draws the full curve to the graphics context.
|
||||||
@@ -116,7 +123,7 @@ public abstract class Curve {
|
|||||||
Image hitCircle = GameImage.HITCIRCLE.getImage();
|
Image hitCircle = GameImage.HITCIRCLE.getImage();
|
||||||
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
|
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
|
||||||
for (int i = 0; i < curve.length; i++)
|
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++)
|
for (int i = 0; i < curve.length; i++)
|
||||||
hitCircle.drawCentered(curve[i].x, curve[i].y, color);
|
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]; }
|
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).
|
* Discards the slider cache (only used for mmsliders).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -18,13 +18,12 @@
|
|||||||
|
|
||||||
package itdelatrisu.opsu.objects.curves;
|
package itdelatrisu.opsu.objects.curves;
|
||||||
|
|
||||||
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.beatmap.HitObject;
|
import itdelatrisu.opsu.beatmap.HitObject;
|
||||||
|
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of multiple curve with equidistant points.
|
* Representation of multiple curve with equidistant points.
|
||||||
* http://pomax.github.io/bezierinfo/#tracing
|
* http://pomax.github.io/bezierinfo/#tracing
|
||||||
@@ -41,10 +40,18 @@ public abstract class EqualDistanceMultiCurve extends Curve {
|
|||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param hitObject the associated HitObject
|
* @param hitObject the associated HitObject
|
||||||
* @param color the color of this curve
|
|
||||||
*/
|
*/
|
||||||
public EqualDistanceMultiCurve(HitObject hitObject, Color color) {
|
public EqualDistanceMultiCurve(HitObject hitObject) {
|
||||||
super(hitObject, color);
|
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
|
// interpolate the point between the two closest distances
|
||||||
if (distanceAt - lastDistanceAt > 1) {
|
if (distanceAt - lastDistanceAt > 1) {
|
||||||
float t = (prefDistance - lastDistanceAt) / (distanceAt - lastDistanceAt);
|
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
|
} else
|
||||||
curve[i] = thisCurve;
|
curve[i] = thisCurve;
|
||||||
}
|
}
|
||||||
@@ -117,20 +124,19 @@ public abstract class EqualDistanceMultiCurve extends Curve {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public float[] pointAt(float t) {
|
public Vec2f pointAt(float t) {
|
||||||
float indexF = t * ncurve;
|
float indexF = t * ncurve;
|
||||||
int index = (int) indexF;
|
int index = (int) indexF;
|
||||||
if (index >= ncurve) {
|
if (index >= ncurve)
|
||||||
Vec2f poi = curve[ncurve];
|
return curve[ncurve].cpy();
|
||||||
return new float[] { poi.x, poi.y };
|
else {
|
||||||
} else {
|
|
||||||
Vec2f poi = curve[index];
|
Vec2f poi = curve[index];
|
||||||
Vec2f poi2 = curve[index + 1];
|
Vec2f poi2 = curve[index + 1];
|
||||||
float t2 = indexF - index;
|
float t2 = indexF - index;
|
||||||
return new float[] {
|
return new Vec2f(
|
||||||
lerp(poi.x, poi2.x, t2),
|
Utils.lerp(poi.x, poi2.x, t2),
|
||||||
lerp(poi.y, poi2.y, t2)
|
Utils.lerp(poi.y, poi2.y, t2)
|
||||||
};
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,6 @@ import itdelatrisu.opsu.beatmap.HitObject;
|
|||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Representation of Bezier curve with equidistant points.
|
* Representation of Bezier curve with equidistant points.
|
||||||
* http://pomax.github.io/bezierinfo/#tracing
|
* http://pomax.github.io/bezierinfo/#tracing
|
||||||
@@ -34,11 +32,20 @@ public class LinearBezier extends EqualDistanceMultiCurve {
|
|||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* @param hitObject the associated HitObject
|
* @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
|
* @param line whether a new curve should be generated for each sequential pair
|
||||||
*/
|
*/
|
||||||
public LinearBezier(HitObject hitObject, Color color, boolean line) {
|
public LinearBezier(HitObject hitObject, boolean line) {
|
||||||
super(hitObject, color);
|
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>();
|
LinkedList<CurveType> beziers = new LinkedList<CurveType>();
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ public class Vec2f {
|
|||||||
*/
|
*/
|
||||||
public 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.
|
* Finds the midpoint between this vector and another vector.
|
||||||
* @param o the other vector
|
* @param o the other vector
|
||||||
@@ -93,6 +103,17 @@ public class Vec2f {
|
|||||||
return this;
|
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.
|
* Returns a copy of this vector.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -18,20 +18,21 @@
|
|||||||
package itdelatrisu.opsu.render;
|
package itdelatrisu.opsu.render;
|
||||||
|
|
||||||
import itdelatrisu.opsu.GameImage;
|
import itdelatrisu.opsu.GameImage;
|
||||||
import itdelatrisu.opsu.Utils;
|
|
||||||
import itdelatrisu.opsu.beatmap.HitObject;
|
import itdelatrisu.opsu.beatmap.HitObject;
|
||||||
import itdelatrisu.opsu.objects.curves.Vec2f;
|
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||||
|
import itdelatrisu.opsu.ui.Colors;
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.FloatBuffer;
|
import java.nio.FloatBuffer;
|
||||||
|
import java.nio.IntBuffer;
|
||||||
|
|
||||||
import org.lwjgl.BufferUtils;
|
import org.lwjgl.BufferUtils;
|
||||||
|
import org.lwjgl.opengl.EXTFramebufferObject;
|
||||||
import org.lwjgl.opengl.GL11;
|
import org.lwjgl.opengl.GL11;
|
||||||
import org.lwjgl.opengl.GL13;
|
import org.lwjgl.opengl.GL13;
|
||||||
import org.lwjgl.opengl.GL14;
|
import org.lwjgl.opengl.GL14;
|
||||||
import org.lwjgl.opengl.GL15;
|
import org.lwjgl.opengl.GL15;
|
||||||
import org.lwjgl.opengl.GL20;
|
import org.lwjgl.opengl.GL20;
|
||||||
import org.lwjgl.opengl.GL30;
|
|
||||||
import org.newdawn.slick.Color;
|
import org.newdawn.slick.Color;
|
||||||
import org.newdawn.slick.Image;
|
import org.newdawn.slick.Image;
|
||||||
import org.newdawn.slick.util.Log;
|
import org.newdawn.slick.util.Log;
|
||||||
@@ -63,15 +64,14 @@ public class CurveRenderState {
|
|||||||
* Should be called before any curves are drawn.
|
* Should be called before any curves are drawn.
|
||||||
* @param width the container width
|
* @param width the container width
|
||||||
* @param height the container height
|
* @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;
|
containerWidth = width;
|
||||||
containerHeight = height;
|
containerHeight = height;
|
||||||
|
|
||||||
// equivalent to what happens in Slider.init()
|
// equivalent to what happens in Slider.init()
|
||||||
scale = (int) (104 - (circleSize * 8));
|
scale = (int) (circleDiameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||||
scale = (int) (scale * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
|
||||||
//scale = scale * 118 / 128; //for curves exactly as big as the sliderball
|
//scale = scale * 118 / 128; //for curves exactly as big as the sliderball
|
||||||
FrameBufferCache.init(width, height);
|
FrameBufferCache.init(width, height);
|
||||||
}
|
}
|
||||||
@@ -114,20 +114,25 @@ public class CurveRenderState {
|
|||||||
mapping = cache.insert(hitObject);
|
mapping = cache.insert(hitObject);
|
||||||
fbo = mapping;
|
fbo = mapping;
|
||||||
|
|
||||||
int old_fb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING);
|
int oldFb = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT);
|
||||||
int old_tex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
|
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.glViewport(0, 0, fbo.width, fbo.height);
|
||||||
GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
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);
|
this.draw_curve(color, borderColor, curve);
|
||||||
color.a = 1f;
|
color.a = 1f;
|
||||||
|
|
||||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_tex);
|
GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex);
|
||||||
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_fb);
|
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFb);
|
||||||
Utils.COLOR_WHITE_FADE.a = alpha;
|
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
|
// draw a fullscreen quad with the texture that contains the curve
|
||||||
@@ -389,7 +394,7 @@ public class CurveRenderState {
|
|||||||
buff.flip();
|
buff.flip();
|
||||||
GL11.glBindTexture(GL11.GL_TEXTURE_1D, gradientTexture);
|
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);
|
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();
|
program = GL20.glCreateProgram();
|
||||||
int vtxShdr = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
|
int vtxShdr = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
|
||||||
int frgShdr = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
|
int frgShdr = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
|
||||||
GL20.glShaderSource(vtxShdr, "#version 330\n"
|
GL20.glShaderSource(vtxShdr, "#version 110\n"
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ "layout(location = 0) in vec4 in_position;\n"
|
+ "attribute vec4 in_position;\n"
|
||||||
+ "layout(location = 1) in vec2 in_tex_coord;\n"
|
+ "attribute vec2 in_tex_coord;\n"
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ "out vec2 tex_coord;\n"
|
+ "varying vec2 tex_coord;\n"
|
||||||
+ "void main()\n"
|
+ "void main()\n"
|
||||||
+ "{\n"
|
+ "{\n"
|
||||||
+ " gl_Position = in_position;\n"
|
+ " gl_Position = in_position;\n"
|
||||||
@@ -419,22 +424,21 @@ public class CurveRenderState {
|
|||||||
String error = GL20.glGetShaderInfoLog(vtxShdr, 1024);
|
String error = GL20.glGetShaderInfoLog(vtxShdr, 1024);
|
||||||
Log.error("Vertex Shader compilation failed.", new Exception(error));
|
Log.error("Vertex Shader compilation failed.", new Exception(error));
|
||||||
}
|
}
|
||||||
GL20.glShaderSource(frgShdr, "#version 330\n"
|
GL20.glShaderSource(frgShdr, "#version 110\n"
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ "uniform sampler1D tex;\n"
|
+ "uniform sampler1D tex;\n"
|
||||||
+ "uniform vec2 tex_size;\n"
|
+ "uniform vec2 tex_size;\n"
|
||||||
+ "uniform vec3 col_tint;\n"
|
+ "uniform vec3 col_tint;\n"
|
||||||
+ "uniform vec4 col_border;\n"
|
+ "uniform vec4 col_border;\n"
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ "in vec2 tex_coord;\n"
|
+ "varying vec2 tex_coord;\n"
|
||||||
+ "layout(location = 0) out vec4 out_colour;\n"
|
|
||||||
+ "\n"
|
+ "\n"
|
||||||
+ "void main()\n"
|
+ "void main()\n"
|
||||||
+ "{\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"
|
+ " 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"
|
+ " 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);
|
GL20.glCompileShader(frgShdr);
|
||||||
res = GL20.glGetShaderi(frgShdr, GL20.GL_COMPILE_STATUS);
|
res = GL20.glGetShaderi(frgShdr, GL20.GL_COMPILE_STATUS);
|
||||||
|
|||||||
@@ -19,10 +19,8 @@ package itdelatrisu.opsu.render;
|
|||||||
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import org.lwjgl.opengl.EXTFramebufferObject;
|
||||||
import org.lwjgl.opengl.GL11;
|
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.
|
* 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) {
|
private Rendertarget(int width, int height) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
fboID = GL30.glGenFramebuffers();
|
fboID = EXTFramebufferObject.glGenFramebuffersEXT();
|
||||||
textureID = GL11.glGenTextures();
|
textureID = GL11.glGenTextures();
|
||||||
depthBufferID = GL30.glGenRenderbuffers();
|
depthBufferID = EXTFramebufferObject.glGenRenderbuffersEXT();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bind this rendertarget as the primary framebuffer.
|
* Bind this rendertarget as the primary framebuffer.
|
||||||
*/
|
*/
|
||||||
public void bind() {
|
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.
|
* Bind the default framebuffer.
|
||||||
*/
|
*/
|
||||||
public static void unbind() {
|
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
|
* @param height the height
|
||||||
*/
|
*/
|
||||||
public static Rendertarget createRTTFramebuffer(int width, int 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);
|
int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
|
||||||
Rendertarget buffer = new Rendertarget(width,height);
|
Rendertarget buffer = new Rendertarget(width,height);
|
||||||
buffer.bind();
|
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_MIN_FILTER, GL11.GL_NEAREST);
|
||||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_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);
|
EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID);
|
||||||
GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL11.GL_DEPTH_COMPONENT, width, height);
|
EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, GL11.GL_DEPTH_COMPONENT, width, height);
|
||||||
GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, buffer.depthBufferID);
|
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);
|
EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, fboTexture, 0);
|
||||||
GL20.glDrawBuffers(GL30.GL_COLOR_ATTACHMENT0);
|
|
||||||
|
|
||||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture);
|
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;
|
return buffer;
|
||||||
}
|
}
|
||||||
@@ -122,8 +119,8 @@ public class Rendertarget {
|
|||||||
* to use this rendertarget with OpenGL after calling this method.
|
* to use this rendertarget with OpenGL after calling this method.
|
||||||
*/
|
*/
|
||||||
public void destroyRTT() {
|
public void destroyRTT() {
|
||||||
GL30.glDeleteFramebuffers(fboID);
|
EXTFramebufferObject.glDeleteFramebuffersEXT(fboID);
|
||||||
GL30.glDeleteRenderbuffers(depthBufferID);
|
EXTFramebufferObject.glDeleteRenderbuffersEXT(depthBufferID);
|
||||||
GL11.glDeleteTextures(textureID);
|
GL11.glDeleteTextures(textureID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ package itdelatrisu.opsu.replay;
|
|||||||
*/
|
*/
|
||||||
public class LifeFrame {
|
public class LifeFrame {
|
||||||
/** Time. */
|
/** Time. */
|
||||||
private int time;
|
private final int time;
|
||||||
|
|
||||||
/** Percentage. */
|
/** Percentage. */
|
||||||
private float percentage;
|
private final float percentage;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ public enum PlaybackSpeed {
|
|||||||
HALF (GameImage.REPLAY_PLAYBACK_HALF, 0.5f);
|
HALF (GameImage.REPLAY_PLAYBACK_HALF, 0.5f);
|
||||||
|
|
||||||
/** The button image. */
|
/** The button image. */
|
||||||
private GameImage gameImage;
|
private final GameImage gameImage;
|
||||||
|
|
||||||
|
/** The playback speed modifier. */
|
||||||
|
private final float modifier;
|
||||||
|
|
||||||
/** The button. */
|
/** The button. */
|
||||||
private MenuButton button;
|
private MenuButton button;
|
||||||
|
|
||||||
/** The playback speed modifier. */
|
|
||||||
private float modifier;
|
|
||||||
|
|
||||||
/** Enum values. */
|
/** Enum values. */
|
||||||
private static PlaybackSpeed[] values = PlaybackSpeed.values();
|
private static PlaybackSpeed[] values = PlaybackSpeed.values();
|
||||||
|
|
||||||
|
|||||||
@@ -41,11 +41,11 @@ import java.util.ArrayList;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import lzma.streams.LzmaOutputStream;
|
|
||||||
|
|
||||||
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
|
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
|
||||||
import org.newdawn.slick.util.Log;
|
import org.newdawn.slick.util.Log;
|
||||||
|
|
||||||
|
import lzma.streams.LzmaOutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Captures osu! replay data.
|
* Captures osu! replay data.
|
||||||
* https://osu.ppy.sh/wiki/Osr_%28file_format%29
|
* https://osu.ppy.sh/wiki/Osr_%28file_format%29
|
||||||
|
|||||||
@@ -38,13 +38,13 @@ public class ReplayFrame {
|
|||||||
private int timeDiff;
|
private int timeDiff;
|
||||||
|
|
||||||
/** Time, in milliseconds. */
|
/** Time, in milliseconds. */
|
||||||
private int time;
|
private final int time;
|
||||||
|
|
||||||
/** Cursor coordinates (in OsuPixels). */
|
/** Cursor coordinates (in OsuPixels). */
|
||||||
private float x, y;
|
private final float x, y;
|
||||||
|
|
||||||
/** Keys pressed (bitmask). */
|
/** Keys pressed (bitmask). */
|
||||||
private int keys;
|
private final int keys;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the start frame.
|
* Returns the start frame.
|
||||||
@@ -81,7 +81,8 @@ public class ReplayFrame {
|
|||||||
public int getTimeDiff() { return timeDiff; }
|
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; }
|
public void setTimeDiff(int diff) { this.timeDiff = diff; }
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,11 @@ import itdelatrisu.opsu.audio.SoundController;
|
|||||||
import itdelatrisu.opsu.audio.SoundEffect;
|
import itdelatrisu.opsu.audio.SoundEffect;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||||
|
import itdelatrisu.opsu.ui.Fonts;
|
||||||
import itdelatrisu.opsu.ui.MenuButton;
|
import itdelatrisu.opsu.ui.MenuButton;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -187,15 +190,15 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
float mult = GameMod.getScoreMultiplier();
|
float mult = GameMod.getScoreMultiplier();
|
||||||
String multString = String.format("Score Multiplier: %.2fx", mult);
|
String multString = String.format("Score Multiplier: %.2fx", mult);
|
||||||
Color multColor = (mult == 1f) ? Color.white : (mult > 1f) ? Color.green : Color.red;
|
Color multColor = (mult == 1f) ? Color.white : (mult > 1f) ? Color.green : Color.red;
|
||||||
float multY = Utils.FONT_LARGE.getLineHeight() * 2 + height * 0.06f;
|
float multY = Fonts.LARGE.getLineHeight() * 2 + height * 0.06f;
|
||||||
Utils.FONT_LARGE.drawString(
|
Fonts.LARGE.drawString(
|
||||||
(width - Utils.FONT_LARGE.getWidth(multString)) / 2f,
|
(width - Fonts.LARGE.getWidth(multString)) / 2f,
|
||||||
multY, multString, multColor);
|
multY, multString, multColor);
|
||||||
|
|
||||||
// category text
|
// category text
|
||||||
for (GameMod.Category category : GameMod.Category.values()) {
|
for (GameMod.Category category : GameMod.Category.values()) {
|
||||||
Utils.FONT_LARGE.drawString(category.getX(),
|
Fonts.LARGE.drawString(category.getX(),
|
||||||
category.getY() - Utils.FONT_LARGE.getLineHeight() / 2f,
|
category.getY() - Fonts.LARGE.getLineHeight() / 2f,
|
||||||
category.getName(), category.getColor());
|
category.getName(), category.getColor());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +220,7 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// tooltips
|
// tooltips
|
||||||
if (hoverMod != null && hoverMod.isImplemented())
|
if (hoverMod != null)
|
||||||
UI.updateTooltip(delta, hoverMod.getDescription(), true);
|
UI.updateTooltip(delta, hoverMod.getDescription(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +256,7 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** The buttons in the state. */
|
/** The buttons in the state. */
|
||||||
private Button[] buttons;
|
private final Button[] buttons;
|
||||||
|
|
||||||
/** The associated MenuButton objects. */
|
/** The associated MenuButton objects. */
|
||||||
private MenuButton[] menuButtons;
|
private MenuButton[] menuButtons;
|
||||||
@@ -261,8 +264,11 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
/** The actual title string list, generated upon entering the state. */
|
/** The actual title string list, generated upon entering the state. */
|
||||||
private List<String> actualTitle;
|
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) */
|
/** 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.
|
* Constructor.
|
||||||
@@ -288,7 +294,7 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
menuButtons = new MenuButton[buttons.length];
|
menuButtons = new MenuButton[buttons.length];
|
||||||
for (int i = 0; i < buttons.length; i++) {
|
for (int i = 0; i < buttons.length; i++) {
|
||||||
MenuButton b = new MenuButton(button, buttonL, buttonR, center, baseY + (i * offsetY));
|
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();
|
b.setHoverFade();
|
||||||
menuButtons[i] = b;
|
menuButtons[i] = b;
|
||||||
}
|
}
|
||||||
@@ -301,7 +307,7 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
*/
|
*/
|
||||||
protected float getBaseY(GameContainer container, StateBasedGame game) {
|
protected float getBaseY(GameContainer container, StateBasedGame game) {
|
||||||
float baseY = container.getHeight() * 0.2f;
|
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;
|
return baseY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,9 +321,9 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
// draw title
|
// draw title
|
||||||
if (actualTitle != null) {
|
if (actualTitle != null) {
|
||||||
float marginX = container.getWidth() * 0.015f, marginY = container.getHeight() * 0.01f;
|
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++)
|
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
|
// draw buttons
|
||||||
@@ -336,18 +342,14 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
*/
|
*/
|
||||||
public void update(GameContainer container, int delta, int mouseX, int mouseY) {
|
public void update(GameContainer container, int delta, int mouseX, int mouseY) {
|
||||||
float center = container.getWidth() / 2f;
|
float center = container.getWidth() / 2f;
|
||||||
|
boolean centerOffsetUpdated = centerOffset.update(delta);
|
||||||
|
float centerOffsetX = centerOffset.getValue();
|
||||||
for (int i = 0; i < buttons.length; i++) {
|
for (int i = 0; i < buttons.length; i++) {
|
||||||
menuButtons[i].hoverUpdate(delta, mouseX, mouseY);
|
menuButtons[i].hoverUpdate(delta, mouseX, mouseY);
|
||||||
|
|
||||||
// move button to center
|
// move button to center
|
||||||
float x = menuButtons[i].getX();
|
if (centerOffsetUpdated)
|
||||||
if (i % 2 == 0) {
|
menuButtons[i].setX((i % 2 == 0) ? center + centerOffsetX : center - centerOffsetX);
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,9 +406,10 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
*/
|
*/
|
||||||
public void enter(GameContainer container, StateBasedGame game) {
|
public void enter(GameContainer container, StateBasedGame game) {
|
||||||
float center = container.getWidth() / 2f;
|
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++) {
|
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();
|
menuButtons[i].resetHover();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,8 +419,8 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
int maxLineWidth = (int) (container.getWidth() * 0.96f);
|
int maxLineWidth = (int) (container.getWidth() * 0.96f);
|
||||||
for (int i = 0; i < title.length; i++) {
|
for (int i = 0; i < title.length; i++) {
|
||||||
// wrap text if too long
|
// wrap text if too long
|
||||||
if (Utils.FONT_LARGE.getWidth(title[i]) > maxLineWidth) {
|
if (Fonts.LARGE.getWidth(title[i]) > maxLineWidth) {
|
||||||
List<String> list = Utils.wrap(title[i], Utils.FONT_LARGE, maxLineWidth);
|
List<String> list = Fonts.wrap(Fonts.LARGE, title[i], maxLineWidth);
|
||||||
actualTitle.addAll(list);
|
actualTitle.addAll(list);
|
||||||
} else
|
} else
|
||||||
actualTitle.add(title[i]);
|
actualTitle.add(title[i]);
|
||||||
@@ -545,10 +548,10 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** The text to show on the button. */
|
/** The text to show on the button. */
|
||||||
private String text;
|
private final String text;
|
||||||
|
|
||||||
/** The button color. */
|
/** The button color. */
|
||||||
private Color color;
|
private final Color color;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
@@ -591,7 +594,7 @@ public class ButtonMenu extends BasicGameState {
|
|||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
private Input input;
|
private Input input;
|
||||||
private int state;
|
private final int state;
|
||||||
|
|
||||||
public ButtonMenu(int state) {
|
public ButtonMenu(int state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ package itdelatrisu.opsu.states;
|
|||||||
import itdelatrisu.opsu.GameImage;
|
import itdelatrisu.opsu.GameImage;
|
||||||
import itdelatrisu.opsu.Opsu;
|
import itdelatrisu.opsu.Opsu;
|
||||||
import itdelatrisu.opsu.Options;
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.OszUnpacker;
|
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.audio.MusicController;
|
import itdelatrisu.opsu.audio.MusicController;
|
||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
@@ -29,13 +28,19 @@ import itdelatrisu.opsu.audio.SoundEffect;
|
|||||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||||
|
import itdelatrisu.opsu.beatmap.OszUnpacker;
|
||||||
import itdelatrisu.opsu.downloads.Download;
|
import itdelatrisu.opsu.downloads.Download;
|
||||||
import itdelatrisu.opsu.downloads.DownloadList;
|
import itdelatrisu.opsu.downloads.DownloadList;
|
||||||
import itdelatrisu.opsu.downloads.DownloadNode;
|
import itdelatrisu.opsu.downloads.DownloadNode;
|
||||||
import itdelatrisu.opsu.downloads.servers.BloodcatServer;
|
import itdelatrisu.opsu.downloads.servers.BloodcatServer;
|
||||||
import itdelatrisu.opsu.downloads.servers.DownloadServer;
|
import itdelatrisu.opsu.downloads.servers.DownloadServer;
|
||||||
import itdelatrisu.opsu.downloads.servers.HexideServer;
|
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.KinecticScrolling;
|
||||||
import itdelatrisu.opsu.ui.MenuButton;
|
import itdelatrisu.opsu.ui.MenuButton;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
@@ -75,10 +80,10 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
private static final int MIN_REQUEST_INTERVAL = 300;
|
private static final int MIN_REQUEST_INTERVAL = 300;
|
||||||
|
|
||||||
/** Available beatmap download servers. */
|
/** Available beatmap download servers. */
|
||||||
private static final DownloadServer[] SERVERS = { new BloodcatServer(), new OsuMirrorServer(), new HexideServer() };
|
private static final DownloadServer[] SERVERS = {
|
||||||
|
new BloodcatServer(), new HexideServer(), new YaSOnlineServer(),
|
||||||
/** The beatmap download server index. */
|
new MnetworkServer(), new MengSkyServer()
|
||||||
private int serverIndex = 0;
|
};
|
||||||
|
|
||||||
/** The current list of search results. */
|
/** The current list of search results. */
|
||||||
private DownloadNode[] resultList;
|
private DownloadNode[] resultList;
|
||||||
@@ -137,14 +142,14 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
/** Page direction for last query. */
|
/** Page direction for last query. */
|
||||||
private Page lastQueryDir = Page.RESET;
|
private Page lastQueryDir = Page.RESET;
|
||||||
|
|
||||||
/** Number of active requests. */
|
|
||||||
private int activeRequests = 0;
|
|
||||||
|
|
||||||
/** Previous and next page buttons. */
|
/** Previous and next page buttons. */
|
||||||
private MenuButton prevPage, nextPage;
|
private MenuButton prevPage, nextPage;
|
||||||
|
|
||||||
/** Buttons. */
|
/** Buttons. */
|
||||||
private MenuButton clearButton, importButton, resetButton, rankedButton, serverButton;
|
private MenuButton clearButton, importButton, resetButton, rankedButton;
|
||||||
|
|
||||||
|
/** Dropdown menu. */
|
||||||
|
private DropdownMenu<DownloadServer> serverMenu;
|
||||||
|
|
||||||
/** Beatmap importing thread. */
|
/** Beatmap importing thread. */
|
||||||
private Thread importThread;
|
private Thread importThread;
|
||||||
@@ -155,266 +160,41 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
/** The bar notification to send upon entering the state. */
|
/** The bar notification to send upon entering the state. */
|
||||||
private String barNotificationOnLoad;
|
private String barNotificationOnLoad;
|
||||||
|
|
||||||
// game-related variables
|
/** Search query, executed in {@code queryThread}. */
|
||||||
private GameContainer container;
|
private SearchQuery searchQuery;
|
||||||
private StateBasedGame game;
|
|
||||||
private Input input;
|
|
||||||
private int state;
|
|
||||||
|
|
||||||
public DownloadsMenu(int state) {
|
/** Search query helper class. */
|
||||||
this.state = state;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/** Interrupt the query and prevent the results from being processed, if not already complete. */
|
||||||
public void init(GameContainer container, StateBasedGame game)
|
public void interrupt() { interrupted = true; }
|
||||||
throws SlickException {
|
|
||||||
this.container = container;
|
|
||||||
this.game = game;
|
|
||||||
this.input = container.getInput();
|
|
||||||
|
|
||||||
int width = container.getWidth();
|
/** Returns whether the query has completed execution. */
|
||||||
int height = container.getHeight();
|
public boolean isComplete() { return complete; }
|
||||||
float baseX = width * 0.024f;
|
|
||||||
float searchY = (height * 0.04f) + Utils.FONT_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()
|
|
||||||
);
|
|
||||||
search.setBackgroundColor(DownloadNode.BG_NORMAL);
|
|
||||||
search.setBorderColor(Color.white);
|
|
||||||
search.setTextColor(Color.white);
|
|
||||||
search.setConsumeEvents(false);
|
|
||||||
search.setMaxLength(255);
|
|
||||||
|
|
||||||
// page buttons
|
|
||||||
float pageButtonY = height * 0.2f;
|
|
||||||
float pageButtonWidth = width * 0.7f;
|
|
||||||
Image prevImg = GameImage.MUSIC_PREVIOUS.getImage();
|
|
||||||
Image nextImg = GameImage.MUSIC_NEXT.getImage();
|
|
||||||
prevPage = new MenuButton(prevImg, baseX + prevImg.getWidth() / 2f,
|
|
||||||
pageButtonY - prevImg.getHeight() / 2f);
|
|
||||||
nextPage = new MenuButton(nextImg, baseX + pageButtonWidth - nextImg.getWidth() / 2f,
|
|
||||||
pageButtonY - nextImg.getHeight() / 2f);
|
|
||||||
prevPage.setHoverExpand(1.5f);
|
|
||||||
nextPage.setHoverExpand(1.5f);
|
|
||||||
|
|
||||||
// buttons
|
|
||||||
float buttonMarginX = width * 0.004f;
|
|
||||||
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 lowerButtonY = height * 0.995f - searchY - buttonHeight / 2f;
|
|
||||||
Image button = GameImage.MENU_BUTTON_MID.getImage();
|
|
||||||
Image buttonL = GameImage.MENU_BUTTON_LEFT.getImage();
|
|
||||||
Image buttonR = GameImage.MENU_BUTTON_RIGHT.getImage();
|
|
||||||
buttonL = buttonL.getScaledCopy(buttonHeight / buttonL.getHeight());
|
|
||||||
buttonR = buttonR.getScaledCopy(buttonHeight / buttonR.getHeight());
|
|
||||||
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);
|
|
||||||
importButton = new MenuButton(lowerButtonImage, buttonL, buttonR,
|
|
||||||
width - buttonMarginX - lowerButtonWidth / 2f, lowerButtonY);
|
|
||||||
resetButton = new MenuButton(resetButtonImage, buttonL, buttonR,
|
|
||||||
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.setHoverFade();
|
|
||||||
importButton.setHoverFade();
|
|
||||||
resetButton.setHoverFade();
|
|
||||||
rankedButton.setHoverFade();
|
|
||||||
serverButton.setHoverFade();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
|
||||||
throws SlickException {
|
|
||||||
int width = container.getWidth();
|
|
||||||
int height = container.getHeight();
|
|
||||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
|
||||||
|
|
||||||
// background
|
|
||||||
GameImage.SEARCH_BG.getImage().draw();
|
|
||||||
|
|
||||||
// title
|
|
||||||
Utils.FONT_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(
|
|
||||||
search.getX() + search.getWidth() * 0.01f, search.getY() + search.getHeight() * 1.3f,
|
|
||||||
searchResultString, Color.white
|
|
||||||
);
|
|
||||||
|
|
||||||
// search results
|
|
||||||
DownloadNode[] nodes = resultList;
|
|
||||||
if (nodes != null) {
|
|
||||||
DownloadNode.clipToResultArea(g);
|
|
||||||
int maxResultsShown = DownloadNode.maxResultsShown();
|
|
||||||
int startResult = (int) (startResultPos.getPosition() / DownloadNode.getButtonOffset());
|
|
||||||
int offset = (int) (-startResultPos.getPosition() + startResult * DownloadNode.getButtonOffset());
|
|
||||||
|
|
||||||
for (int i = 0; i < maxResultsShown + 1; i++) {
|
|
||||||
int index = startResult + i;
|
|
||||||
if(index < 0)
|
|
||||||
continue;
|
|
||||||
if (index >= nodes.length)
|
|
||||||
break;
|
|
||||||
nodes[index].drawResult(g, offset + i * DownloadNode.getButtonOffset(),
|
|
||||||
DownloadNode.resultContains(mouseX, mouseY - offset, i),
|
|
||||||
(index == focusResult), (previewID == nodes[index].getID()));
|
|
||||||
}
|
|
||||||
g.clearClip();
|
|
||||||
|
|
||||||
// scroll bar
|
|
||||||
if (nodes.length > maxResultsShown)
|
|
||||||
DownloadNode.drawResultScrollbar(g, startResultPos.getPosition(), nodes.length * DownloadNode.getButtonOffset());
|
|
||||||
|
|
||||||
// pages
|
|
||||||
if (nodes.length > 0) {
|
|
||||||
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,
|
|
||||||
String.format("Page %d", page), Color.white
|
|
||||||
);
|
|
||||||
if (page > 1)
|
|
||||||
prevPage.draw();
|
|
||||||
if (pageResultTotal < totalResults)
|
|
||||||
nextPage.draw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// downloads
|
|
||||||
float downloadsX = width * 0.75f, downloadsY = search.getY();
|
|
||||||
g.setColor(DownloadNode.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);
|
|
||||||
int downloadsSize = DownloadList.get().size();
|
|
||||||
if (downloadsSize > 0) {
|
|
||||||
int maxDownloadsShown = DownloadNode.maxDownloadsShown();
|
|
||||||
int startDownloadIndex = (int) (startDownloadIndexPos.getPosition() / DownloadNode.getInfoHeight());
|
|
||||||
int offset = (int) (-startDownloadIndexPos.getPosition() + startDownloadIndex * DownloadNode.getInfoHeight());
|
|
||||||
|
|
||||||
DownloadNode.clipToDownloadArea(g);
|
|
||||||
for (int i = 0; i < maxDownloadsShown + 1; i++) {
|
|
||||||
int index = startDownloadIndex + i;
|
|
||||||
if (index >= downloadsSize)
|
|
||||||
break;
|
|
||||||
DownloadNode node = DownloadList.get().getNode(index);
|
|
||||||
if (node == null)
|
|
||||||
break;
|
|
||||||
node.drawDownload(g, i * DownloadNode.getInfoHeight() + offset, index,
|
|
||||||
DownloadNode.downloadContains(mouseX, mouseY - offset, i));
|
|
||||||
}
|
|
||||||
g.clearClip();
|
|
||||||
|
|
||||||
|
|
||||||
// scroll bar
|
|
||||||
if (downloadsSize > maxDownloadsShown)
|
|
||||||
DownloadNode.drawDownloadScrollbar(g, startDownloadIndexPos.getPosition(), downloadsSize * DownloadNode.getInfoHeight());
|
|
||||||
}
|
|
||||||
|
|
||||||
// buttons
|
|
||||||
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.draw(Color.magenta);
|
|
||||||
serverButton.setText(SERVERS[serverIndex].getName(), Utils.FONT_MEDIUM, Color.white);
|
|
||||||
serverButton.draw(Color.blue);
|
|
||||||
|
|
||||||
// importing beatmaps
|
|
||||||
if (importThread != null) {
|
|
||||||
// darken the screen
|
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
|
||||||
g.fillRect(0, 0, width, height);
|
|
||||||
|
|
||||||
UI.drawLoadingProgress(g);
|
|
||||||
}
|
|
||||||
|
|
||||||
// back button
|
|
||||||
else
|
|
||||||
UI.getBackButton().draw();
|
|
||||||
|
|
||||||
UI.draw(g);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void update(GameContainer container, StateBasedGame game, int delta)
|
|
||||||
throws SlickException {
|
|
||||||
UI.update(delta);
|
|
||||||
if (importThread == null)
|
|
||||||
MusicController.loopTrackIfEnded(false);
|
|
||||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
|
||||||
UI.getBackButton().hoverUpdate(delta, mouseX, mouseY);
|
|
||||||
prevPage.hoverUpdate(delta, mouseX, mouseY);
|
|
||||||
nextPage.hoverUpdate(delta, mouseX, mouseY);
|
|
||||||
clearButton.hoverUpdate(delta, mouseX, mouseY);
|
|
||||||
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()));
|
|
||||||
startDownloadIndexPos.update(delta);
|
|
||||||
if (resultList != null)
|
|
||||||
startResultPos.setMinMax(0, DownloadNode.getButtonOffset() * (resultList.length - DownloadNode.maxResultsShown()));
|
|
||||||
startResultPos.update(delta);
|
|
||||||
|
|
||||||
// focus timer
|
|
||||||
if (focusResult != -1 && focusTimer < FOCUS_DELAY)
|
|
||||||
focusTimer += delta;
|
|
||||||
|
|
||||||
// search
|
|
||||||
search.setFocus(true);
|
|
||||||
searchTimer += delta;
|
|
||||||
if (searchTimer >= SEARCH_DELAY && importThread == null) {
|
|
||||||
searchTimer = 0;
|
|
||||||
searchTimerReset = false;
|
|
||||||
|
|
||||||
final String query = search.getText().trim().toLowerCase();
|
|
||||||
final DownloadServer server = SERVERS[serverIndex];
|
|
||||||
if ((lastQuery == null || !query.equals(lastQuery)) &&
|
|
||||||
(query.length() == 0 || query.length() >= server.minQueryLength())) {
|
|
||||||
lastQuery = query;
|
|
||||||
lastQueryDir = pageDir;
|
|
||||||
|
|
||||||
if (queryThread != null && queryThread.isAlive())
|
|
||||||
queryThread.interrupt();
|
|
||||||
|
|
||||||
// execute query
|
|
||||||
queryThread = new Thread() {
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
activeRequests++;
|
|
||||||
|
|
||||||
// check page direction
|
// check page direction
|
||||||
Page lastPageDir = pageDir;
|
Page lastPageDir = pageDir;
|
||||||
pageDir = Page.RESET;
|
pageDir = Page.RESET;
|
||||||
@@ -428,7 +208,7 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
newPage--;
|
newPage--;
|
||||||
try {
|
try {
|
||||||
DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
|
DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
|
||||||
if (activeRequests - 1 == 0) {
|
if (!interrupted) {
|
||||||
// update page total
|
// update page total
|
||||||
page = newPage;
|
page = newPage;
|
||||||
if (nodes != null) {
|
if (nodes != null) {
|
||||||
@@ -458,13 +238,304 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
if (!interrupted)
|
||||||
searchResultString = "Could not establish connection to server.";
|
searchResultString = "Could not establish connection to server.";
|
||||||
} finally {
|
} finally {
|
||||||
activeRequests--;
|
complete = true;
|
||||||
queryThread = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// game-related variables
|
||||||
|
private GameContainer container;
|
||||||
|
private StateBasedGame game;
|
||||||
|
private Input input;
|
||||||
|
private final int state;
|
||||||
|
|
||||||
|
public DownloadsMenu(int state) {
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(GameContainer container, StateBasedGame game)
|
||||||
|
throws SlickException {
|
||||||
|
this.container = container;
|
||||||
|
this.game = game;
|
||||||
|
this.input = container.getInput();
|
||||||
|
|
||||||
|
int width = container.getWidth();
|
||||||
|
int height = container.getHeight();
|
||||||
|
float baseX = width * 0.024f;
|
||||||
|
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, Fonts.DEFAULT, (int) baseX, (int) searchY,
|
||||||
|
(int) searchWidth, Fonts.MEDIUM.getLineHeight()
|
||||||
|
);
|
||||||
|
search.setBackgroundColor(Colors.BLACK_BG_NORMAL);
|
||||||
|
search.setBorderColor(Color.white);
|
||||||
|
search.setTextColor(Color.white);
|
||||||
|
search.setConsumeEvents(false);
|
||||||
|
search.setMaxLength(255);
|
||||||
|
|
||||||
|
// page buttons
|
||||||
|
float pageButtonY = height * 0.2f;
|
||||||
|
float pageButtonWidth = width * 0.7f;
|
||||||
|
Image prevImg = GameImage.MUSIC_PREVIOUS.getImage();
|
||||||
|
Image nextImg = GameImage.MUSIC_NEXT.getImage();
|
||||||
|
prevPage = new MenuButton(prevImg, baseX + prevImg.getWidth() / 2f,
|
||||||
|
pageButtonY - prevImg.getHeight() / 2f);
|
||||||
|
nextPage = new MenuButton(nextImg, baseX + pageButtonWidth - nextImg.getWidth() / 2f,
|
||||||
|
pageButtonY - nextImg.getHeight() / 2f);
|
||||||
|
prevPage.setHoverExpand(1.5f);
|
||||||
|
nextPage.setHoverExpand(1.5f);
|
||||||
|
|
||||||
|
// buttons
|
||||||
|
float buttonMarginX = width * 0.004f;
|
||||||
|
float buttonHeight = height * 0.038f;
|
||||||
|
float resetWidth = width * 0.085f;
|
||||||
|
float rankedWidth = width * 0.15f;
|
||||||
|
float lowerWidth = width * 0.12f;
|
||||||
|
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();
|
||||||
|
Image buttonR = GameImage.MENU_BUTTON_RIGHT.getImage();
|
||||||
|
buttonL = buttonL.getScaledCopy(buttonHeight / buttonL.getHeight());
|
||||||
|
buttonR = buttonR.getScaledCopy(buttonHeight / buttonR.getHeight());
|
||||||
|
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 lowerButtonImage = button.getScaledCopy((int) lowerWidth - lrButtonWidth, (int) buttonHeight);
|
||||||
|
float resetButtonWidth = resetButtonImage.getWidth() + lrButtonWidth;
|
||||||
|
float rankedButtonWidth = rankedButtonImage.getWidth() + lrButtonWidth;
|
||||||
|
float lowerButtonWidth = lowerButtonImage.getWidth() + lrButtonWidth;
|
||||||
|
clearButton = new MenuButton(lowerButtonImage, buttonL, buttonR,
|
||||||
|
width * 0.75f + buttonMarginX + lowerButtonWidth / 2f, lowerButtonY);
|
||||||
|
importButton = new MenuButton(lowerButtonImage, buttonL, buttonR,
|
||||||
|
width - buttonMarginX - lowerButtonWidth / 2f, lowerButtonY);
|
||||||
|
resetButton = new MenuButton(resetButtonImage, buttonL, buttonR,
|
||||||
|
baseX + searchWidth + buttonMarginX + resetButtonWidth / 2f, topButtonY);
|
||||||
|
rankedButton = new MenuButton(rankedButtonImage, buttonL, buttonR,
|
||||||
|
baseX + searchWidth + buttonMarginX * 2f + resetButtonWidth + rankedButtonWidth / 2f, topButtonY);
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
||||||
|
throws SlickException {
|
||||||
|
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
|
||||||
|
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);
|
||||||
|
Fonts.BOLD.drawString(
|
||||||
|
search.getX() + search.getWidth() * 0.01f, search.getY() + search.getHeight() * 1.3f,
|
||||||
|
searchResultString, Color.white
|
||||||
|
);
|
||||||
|
|
||||||
|
// search results
|
||||||
|
DownloadNode[] nodes = resultList;
|
||||||
|
if (nodes != null) {
|
||||||
|
DownloadNode.clipToResultArea(g);
|
||||||
|
int maxResultsShown = DownloadNode.maxResultsShown();
|
||||||
|
int startResult = (int) (startResultPos.getPosition() / DownloadNode.getButtonOffset());
|
||||||
|
int offset = (int) (-startResultPos.getPosition() + startResult * DownloadNode.getButtonOffset());
|
||||||
|
|
||||||
|
for (int i = 0; i < maxResultsShown + 1; i++) {
|
||||||
|
int index = startResult + i;
|
||||||
|
if(index < 0)
|
||||||
|
continue;
|
||||||
|
if (index >= nodes.length)
|
||||||
|
break;
|
||||||
|
nodes[index].drawResult(g, offset + i * DownloadNode.getButtonOffset(),
|
||||||
|
DownloadNode.resultContains(mouseX, mouseY - offset, i) && !inDropdownMenu,
|
||||||
|
(index == focusResult), (previewID == nodes[index].getID()));
|
||||||
|
}
|
||||||
|
g.clearClip();
|
||||||
|
|
||||||
|
// scroll bar
|
||||||
|
if (nodes.length > maxResultsShown)
|
||||||
|
DownloadNode.drawResultScrollbar(g, startResultPos.getPosition(), nodes.length * DownloadNode.getButtonOffset());
|
||||||
|
|
||||||
|
// pages
|
||||||
|
if (nodes.length > 0) {
|
||||||
|
float baseX = width * 0.024f;
|
||||||
|
float buttonY = height * 0.2f;
|
||||||
|
float buttonWidth = width * 0.7f;
|
||||||
|
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)
|
||||||
|
prevPage.draw();
|
||||||
|
if (pageResultTotal < totalResults)
|
||||||
|
nextPage.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloads
|
||||||
|
float downloadsX = width * 0.75f, downloadsY = search.getY();
|
||||||
|
g.setColor(Colors.BLACK_BG_NORMAL);
|
||||||
|
g.fillRect(downloadsX, downloadsY,
|
||||||
|
width * 0.25f, height - downloadsY * 2f);
|
||||||
|
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();
|
||||||
|
int startDownloadIndex = (int) (startDownloadIndexPos.getPosition() / DownloadNode.getInfoHeight());
|
||||||
|
int offset = (int) (-startDownloadIndexPos.getPosition() + startDownloadIndex * DownloadNode.getInfoHeight());
|
||||||
|
|
||||||
|
DownloadNode.clipToDownloadArea(g);
|
||||||
|
for (int i = 0; i < maxDownloadsShown + 1; i++) {
|
||||||
|
int index = startDownloadIndex + i;
|
||||||
|
if (index >= downloadsSize)
|
||||||
|
break;
|
||||||
|
DownloadNode node = DownloadList.get().getNode(index);
|
||||||
|
if (node == null)
|
||||||
|
break;
|
||||||
|
node.drawDownload(g, i * DownloadNode.getInfoHeight() + offset, index,
|
||||||
|
DownloadNode.downloadContains(mouseX, mouseY - offset, i));
|
||||||
|
}
|
||||||
|
g.clearClip();
|
||||||
|
|
||||||
|
|
||||||
|
// scroll bar
|
||||||
|
if (downloadsSize > maxDownloadsShown)
|
||||||
|
DownloadNode.drawDownloadScrollbar(g, startDownloadIndexPos.getPosition(), downloadsSize * DownloadNode.getInfoHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
// buttons
|
||||||
|
clearButton.draw(Color.gray);
|
||||||
|
importButton.draw(Color.orange);
|
||||||
|
resetButton.draw(Color.red);
|
||||||
|
rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Fonts.MEDIUM, Color.white);
|
||||||
|
rankedButton.draw(Color.magenta);
|
||||||
|
|
||||||
|
// dropdown menu
|
||||||
|
serverMenu.render(container, g);
|
||||||
|
|
||||||
|
// importing beatmaps
|
||||||
|
if (importThread != null) {
|
||||||
|
// darken the screen
|
||||||
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
|
g.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
|
UI.drawLoadingProgress(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
// back button
|
||||||
|
else
|
||||||
|
UI.getBackButton().draw();
|
||||||
|
|
||||||
|
UI.draw(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(GameContainer container, StateBasedGame game, int delta)
|
||||||
|
throws SlickException {
|
||||||
|
UI.update(delta);
|
||||||
|
if (importThread == null)
|
||||||
|
MusicController.loopTrackIfEnded(false);
|
||||||
|
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
||||||
|
UI.getBackButton().hoverUpdate(delta, mouseX, mouseY);
|
||||||
|
prevPage.hoverUpdate(delta, mouseX, mouseY);
|
||||||
|
nextPage.hoverUpdate(delta, mouseX, mouseY);
|
||||||
|
clearButton.hoverUpdate(delta, mouseX, mouseY);
|
||||||
|
importButton.hoverUpdate(delta, mouseX, mouseY);
|
||||||
|
resetButton.hoverUpdate(delta, mouseX, mouseY);
|
||||||
|
rankedButton.hoverUpdate(delta, mouseX, mouseY);
|
||||||
|
|
||||||
|
if (DownloadList.get() != null)
|
||||||
|
startDownloadIndexPos.setMinMax(0, DownloadNode.getInfoHeight() * (DownloadList.get().size() - DownloadNode.maxDownloadsShown()));
|
||||||
|
startDownloadIndexPos.update(delta);
|
||||||
|
if (resultList != null)
|
||||||
|
startResultPos.setMinMax(0, DownloadNode.getButtonOffset() * (resultList.length - DownloadNode.maxResultsShown()));
|
||||||
|
startResultPos.update(delta);
|
||||||
|
|
||||||
|
// focus timer
|
||||||
|
if (focusResult != -1 && focusTimer < FOCUS_DELAY)
|
||||||
|
focusTimer += delta;
|
||||||
|
|
||||||
|
// search
|
||||||
|
search.setFocus(true);
|
||||||
|
searchTimer += delta;
|
||||||
|
if (searchTimer >= SEARCH_DELAY && importThread == null) {
|
||||||
|
searchTimer = 0;
|
||||||
|
searchTimerReset = false;
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
queryThread.interrupt();
|
||||||
|
if (searchQuery != null)
|
||||||
|
searchQuery.interrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
// execute query
|
||||||
|
searchQuery = new SearchQuery(query, server);
|
||||||
|
queryThread = new Thread(searchQuery);
|
||||||
queryThread.start();
|
queryThread.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,7 +545,7 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
UI.updateTooltip(delta, "Reset the current search.", false);
|
UI.updateTooltip(delta, "Reset the current search.", false);
|
||||||
else if (rankedButton.contains(mouseX, mouseY))
|
else if (rankedButton.contains(mouseX, mouseY))
|
||||||
UI.updateTooltip(delta, "Toggle the display of unranked maps.\nSome download servers may not support this option.", true);
|
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);
|
UI.updateTooltip(delta, "Select a download server.", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,7 +605,7 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
} else {
|
} else {
|
||||||
// play preview
|
// play preview
|
||||||
try {
|
try {
|
||||||
final URL url = new URL(SERVERS[serverIndex].getPreviewURL(node.getID()));
|
final URL url = new URL(serverMenu.getSelectedItem().getPreviewURL(node.getID()));
|
||||||
MusicController.pause();
|
MusicController.pause();
|
||||||
new Thread() {
|
new Thread() {
|
||||||
@Override
|
@Override
|
||||||
@@ -578,7 +649,7 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
} else {
|
} else {
|
||||||
// start download
|
// start download
|
||||||
if (!DownloadList.get().contains(node.getID())) {
|
if (!DownloadList.get().contains(node.getID())) {
|
||||||
node.createDownload(SERVERS[serverIndex]);
|
node.createDownload(serverMenu.getSelectedItem());
|
||||||
if (node.getDownload() == null)
|
if (node.getDownload() == null)
|
||||||
UI.sendBarNotification("The download could not be started.");
|
UI.sendBarNotification("The download could not be started.");
|
||||||
else {
|
else {
|
||||||
@@ -601,23 +672,27 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
// pages
|
// pages
|
||||||
if (nodes.length > 0) {
|
if (nodes.length > 0) {
|
||||||
if (page > 1 && prevPage.contains(x, y)) {
|
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
|
; // don't send consecutive requests
|
||||||
else {
|
else {
|
||||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||||
pageDir = Page.PREVIOUS;
|
pageDir = Page.PREVIOUS;
|
||||||
lastQuery = null;
|
lastQuery = null;
|
||||||
|
if (searchQuery != null)
|
||||||
|
searchQuery.interrupt();
|
||||||
resetSearchTimer();
|
resetSearchTimer();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pageResultTotal < totalResults && nextPage.contains(x, y)) {
|
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
|
; // don't send consecutive requests
|
||||||
else {
|
else {
|
||||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||||
pageDir = Page.NEXT;
|
pageDir = Page.NEXT;
|
||||||
lastQuery = null;
|
lastQuery = null;
|
||||||
|
if (searchQuery != null)
|
||||||
|
searchQuery.interrupt();
|
||||||
resetSearchTimer();
|
resetSearchTimer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -670,6 +745,8 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
search.setText("");
|
search.setText("");
|
||||||
lastQuery = null;
|
lastQuery = null;
|
||||||
pageDir = Page.RESET;
|
pageDir = Page.RESET;
|
||||||
|
if (searchQuery != null)
|
||||||
|
searchQuery.interrupt();
|
||||||
resetSearchTimer();
|
resetSearchTimer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -678,22 +755,8 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
rankedOnly = !rankedOnly;
|
rankedOnly = !rankedOnly;
|
||||||
lastQuery = null;
|
lastQuery = null;
|
||||||
pageDir = Page.RESET;
|
pageDir = Page.RESET;
|
||||||
resetSearchTimer();
|
if (searchQuery != null)
|
||||||
return;
|
searchQuery.interrupt();
|
||||||
}
|
|
||||||
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;
|
|
||||||
resetSearchTimer();
|
resetSearchTimer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -806,6 +869,8 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||||
lastQuery = null;
|
lastQuery = null;
|
||||||
pageDir = Page.CURRENT;
|
pageDir = Page.CURRENT;
|
||||||
|
if (searchQuery != null)
|
||||||
|
searchQuery.interrupt();
|
||||||
resetSearchTimer();
|
resetSearchTimer();
|
||||||
break;
|
break;
|
||||||
case Input.KEY_F7:
|
case Input.KEY_F7:
|
||||||
@@ -837,7 +902,8 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
importButton.resetHover();
|
importButton.resetHover();
|
||||||
resetButton.resetHover();
|
resetButton.resetHover();
|
||||||
rankedButton.resetHover();
|
rankedButton.resetHover();
|
||||||
serverButton.resetHover();
|
serverMenu.activate();
|
||||||
|
serverMenu.reset();
|
||||||
focusResult = -1;
|
focusResult = -1;
|
||||||
startResultPos.setPosition(0);
|
startResultPos.setPosition(0);
|
||||||
startDownloadIndexPos.setPosition(0);
|
startDownloadIndexPos.setPosition(0);
|
||||||
@@ -853,6 +919,7 @@ public class DownloadsMenu extends BasicGameState {
|
|||||||
public void leave(GameContainer container, StateBasedGame game)
|
public void leave(GameContainer container, StateBasedGame game)
|
||||||
throws SlickException {
|
throws SlickException {
|
||||||
search.setFocus(false);
|
search.setFocus(false);
|
||||||
|
serverMenu.deactivate();
|
||||||
SoundController.stopTrack();
|
SoundController.stopTrack();
|
||||||
MusicController.resume();
|
MusicController.resume();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,12 +42,16 @@ import itdelatrisu.opsu.objects.GameObject;
|
|||||||
import itdelatrisu.opsu.objects.Slider;
|
import itdelatrisu.opsu.objects.Slider;
|
||||||
import itdelatrisu.opsu.objects.Spinner;
|
import itdelatrisu.opsu.objects.Spinner;
|
||||||
import itdelatrisu.opsu.objects.curves.Curve;
|
import itdelatrisu.opsu.objects.curves.Curve;
|
||||||
|
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||||
import itdelatrisu.opsu.render.FrameBufferCache;
|
import itdelatrisu.opsu.render.FrameBufferCache;
|
||||||
import itdelatrisu.opsu.replay.PlaybackSpeed;
|
import itdelatrisu.opsu.replay.PlaybackSpeed;
|
||||||
import itdelatrisu.opsu.replay.Replay;
|
import itdelatrisu.opsu.replay.Replay;
|
||||||
import itdelatrisu.opsu.replay.ReplayFrame;
|
import itdelatrisu.opsu.replay.ReplayFrame;
|
||||||
|
import itdelatrisu.opsu.ui.Colors;
|
||||||
|
import itdelatrisu.opsu.ui.Fonts;
|
||||||
import itdelatrisu.opsu.ui.MenuButton;
|
import itdelatrisu.opsu.ui.MenuButton;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
@@ -116,6 +120,15 @@ public class Game extends BasicGameState {
|
|||||||
/** Hit object approach time, in milliseconds. */
|
/** Hit object approach time, in milliseconds. */
|
||||||
private int approachTime;
|
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). */
|
/** Time offsets for obtaining each hit result (indexed by HIT_* constants). */
|
||||||
private int[] hitResultOffset;
|
private int[] hitResultOffset;
|
||||||
|
|
||||||
@@ -146,7 +159,7 @@ public class Game extends BasicGameState {
|
|||||||
countdown2Sound, countdownGoSound;
|
countdown2Sound, countdownGoSound;
|
||||||
|
|
||||||
/** Mouse coordinates before game paused. */
|
/** Mouse coordinates before game paused. */
|
||||||
private int pausedMouseX = -1, pausedMouseY = -1;
|
private Vec2f pausedMousePosition;
|
||||||
|
|
||||||
/** Track position when game paused. */
|
/** Track position when game paused. */
|
||||||
private int pauseTime = -1;
|
private int pauseTime = -1;
|
||||||
@@ -209,7 +222,7 @@ public class Game extends BasicGameState {
|
|||||||
private int flashlightRadius;
|
private int flashlightRadius;
|
||||||
|
|
||||||
/** The cursor coordinates using the "auto" or "relax" mods. */
|
/** 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. */
|
/** Whether or not the cursor should be pressed using the "auto" mod. */
|
||||||
private boolean autoMousePressed;
|
private boolean autoMousePressed;
|
||||||
@@ -220,11 +233,20 @@ public class Game extends BasicGameState {
|
|||||||
/** Whether the game is currently seeking to a replay position. */
|
/** Whether the game is currently seeking to a replay position. */
|
||||||
private boolean isSeeking;
|
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
|
// game-related variables
|
||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
private Input input;
|
private Input input;
|
||||||
private int state;
|
private final int state;
|
||||||
|
|
||||||
public Game(int state) {
|
public Game(int state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
@@ -245,6 +267,12 @@ public class Game extends BasicGameState {
|
|||||||
gOffscreen = offscreen.getGraphics();
|
gOffscreen = offscreen.getGraphics();
|
||||||
gOffscreen.setBackground(Color.black);
|
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
|
// create the associated GameData object
|
||||||
data = new GameData(width, height);
|
data = new GameData(width, height);
|
||||||
}
|
}
|
||||||
@@ -278,7 +306,7 @@ public class Game extends BasicGameState {
|
|||||||
else
|
else
|
||||||
dimLevel = 1f;
|
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();
|
Image playfield = GameImage.PLAYFIELD.getImage();
|
||||||
playfield.setAlpha(dimLevel);
|
playfield.setAlpha(dimLevel);
|
||||||
playfield.draw();
|
playfield.draw();
|
||||||
@@ -290,32 +318,31 @@ public class Game extends BasicGameState {
|
|||||||
|
|
||||||
// "auto" and "autopilot" mods: move cursor automatically
|
// "auto" and "autopilot" mods: move cursor automatically
|
||||||
// TODO: this should really be in update(), not render()
|
// TODO: this should really be in update(), not render()
|
||||||
autoMouseX = width / 2;
|
autoMousePosition.set(width / 2, height / 2);
|
||||||
autoMouseY = height / 2;
|
|
||||||
autoMousePressed = false;
|
autoMousePressed = false;
|
||||||
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
||||||
float[] autoXY = null;
|
Vec2f autoPoint = null;
|
||||||
if (isLeadIn()) {
|
if (isLeadIn()) {
|
||||||
// lead-in
|
// lead-in
|
||||||
float progress = Math.max((float) (leadInTime - beatmap.audioLeadIn) / approachTime, 0f);
|
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) {
|
} else if (objectIndex == 0 && trackPosition < firstObjectTime) {
|
||||||
// before first object
|
// before first object
|
||||||
timeDiff = firstObjectTime - trackPosition;
|
timeDiff = firstObjectTime - trackPosition;
|
||||||
if (timeDiff < approachTime) {
|
if (timeDiff < approachTime) {
|
||||||
float[] xy = gameObjects[0].getPointAt(trackPosition);
|
Vec2f point = gameObjects[0].getPointAt(trackPosition);
|
||||||
autoXY = getPointAt(autoMouseX, autoMouseY, xy[0], xy[1], 1f - ((float) timeDiff / approachTime));
|
autoPoint = getPointAt(autoMousePosition.x, autoMousePosition.y, point.x, point.y, 1f - ((float) timeDiff / approachTime));
|
||||||
}
|
}
|
||||||
} else if (objectIndex < beatmap.objects.length) {
|
} else if (objectIndex < beatmap.objects.length) {
|
||||||
// normal object
|
// normal object
|
||||||
int objectTime = beatmap.objects[objectIndex].getTime();
|
int objectTime = beatmap.objects[objectIndex].getTime();
|
||||||
if (trackPosition < objectTime) {
|
if (trackPosition < objectTime) {
|
||||||
float[] xyStart = gameObjects[objectIndex - 1].getPointAt(trackPosition);
|
Vec2f startPoint = gameObjects[objectIndex - 1].getPointAt(trackPosition);
|
||||||
int startTime = gameObjects[objectIndex - 1].getEndTime();
|
int startTime = gameObjects[objectIndex - 1].getEndTime();
|
||||||
if (beatmap.breaks != null && breakIndex < beatmap.breaks.size()) {
|
if (beatmap.breaks != null && breakIndex < beatmap.breaks.size()) {
|
||||||
// starting a break: keep cursor at previous hit object position
|
// starting a break: keep cursor at previous hit object position
|
||||||
if (breakTime > 0 || objectTime > beatmap.breaks.get(breakIndex))
|
if (breakTime > 0 || objectTime > beatmap.breaks.get(breakIndex))
|
||||||
autoXY = xyStart;
|
autoPoint = startPoint;
|
||||||
|
|
||||||
// after a break ends: move startTime to break end time
|
// after a break ends: move startTime to break end time
|
||||||
else if (breakIndex > 1) {
|
else if (breakIndex > 1) {
|
||||||
@@ -324,10 +351,10 @@ public class Game extends BasicGameState {
|
|||||||
startTime = lastBreakEndTime;
|
startTime = lastBreakEndTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (autoXY == null) {
|
if (autoPoint == null) {
|
||||||
float[] xyEnd = gameObjects[objectIndex].getPointAt(trackPosition);
|
Vec2f endPoint = gameObjects[objectIndex].getPointAt(trackPosition);
|
||||||
int totalTime = objectTime - startTime;
|
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
|
// hit circles: show a mouse press
|
||||||
int offset300 = hitResultOffset[GameData.HIT_300];
|
int offset300 = hitResultOffset[GameData.HIT_300];
|
||||||
@@ -336,19 +363,17 @@ public class Game extends BasicGameState {
|
|||||||
autoMousePressed = true;
|
autoMousePressed = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
autoXY = gameObjects[objectIndex].getPointAt(trackPosition);
|
autoPoint = gameObjects[objectIndex].getPointAt(trackPosition);
|
||||||
autoMousePressed = true;
|
autoMousePressed = true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// last object
|
// last object
|
||||||
autoXY = gameObjects[objectIndex - 1].getPointAt(trackPosition);
|
autoPoint = gameObjects[objectIndex - 1].getPointAt(trackPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
// set mouse coordinates
|
// set mouse coordinates
|
||||||
if (autoXY != null) {
|
if (autoPoint != null)
|
||||||
autoMouseX = (int) autoXY[0];
|
autoMousePosition.set(autoPoint.x, autoPoint.y);
|
||||||
autoMouseY = (int) autoXY[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// "flashlight" mod: restricted view of hit objects around cursor
|
// "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.setDrawMode(Graphics.MODE_ALPHA_MAP);
|
||||||
g.clearAlphaMap();
|
g.clearAlphaMap();
|
||||||
int mouseX, mouseY;
|
int mouseX, mouseY;
|
||||||
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
|
if (pauseTime > -1 && pausedMousePosition != null) {
|
||||||
mouseX = pausedMouseX;
|
mouseX = (int) pausedMousePosition.x;
|
||||||
mouseY = pausedMouseY;
|
mouseY = (int) pausedMousePosition.y;
|
||||||
} else if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
} else if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
||||||
mouseX = autoMouseX;
|
mouseX = (int) autoMousePosition.x;
|
||||||
mouseY = autoMouseY;
|
mouseY = (int) autoMousePosition.y;
|
||||||
} else if (isReplay) {
|
} else if (isReplay) {
|
||||||
mouseX = replayX;
|
mouseX = replayX;
|
||||||
mouseY = replayY;
|
mouseY = replayY;
|
||||||
@@ -455,15 +480,15 @@ public class Game extends BasicGameState {
|
|||||||
GameImage.SCOREBAR_BG.getImage().getHeight(),
|
GameImage.SCOREBAR_BG.getImage().getHeight(),
|
||||||
GameImage.SCOREBAR_KI.getImage().getHeight()
|
GameImage.SCOREBAR_KI.getImage().getHeight()
|
||||||
);
|
);
|
||||||
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
|
float oldAlpha = Colors.WHITE_FADE.a;
|
||||||
if (timeDiff < -500)
|
if (timeDiff < -500)
|
||||||
Utils.COLOR_WHITE_FADE.a = (1000 + timeDiff) / 500f;
|
Colors.WHITE_FADE.a = (1000 + timeDiff) / 500f;
|
||||||
Utils.FONT_MEDIUM.drawString(
|
Fonts.MEDIUM.drawString(
|
||||||
2 + (width / 100), retryHeight,
|
2 + (width / 100), retryHeight,
|
||||||
String.format("%d retries and counting...", retries),
|
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())
|
if (isLeadIn())
|
||||||
@@ -525,28 +550,40 @@ public class Game extends BasicGameState {
|
|||||||
if (isReplay || GameMod.AUTO.isActive())
|
if (isReplay || GameMod.AUTO.isActive())
|
||||||
playbackSpeed.getButton().draw();
|
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
|
// returning from pause screen
|
||||||
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
|
if (pauseTime > -1 && pausedMousePosition != null) {
|
||||||
// darken the screen
|
// darken the screen
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
g.fillRect(0, 0, width, height);
|
g.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
// draw glowing hit select circle and pulse effect
|
// draw glowing hit select circle and pulse effect
|
||||||
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth();
|
int circleDiameter = GameImage.HITCIRCLE.getImage().getWidth();
|
||||||
Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleRadius, circleRadius);
|
Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleDiameter, circleDiameter);
|
||||||
cursorCircle.setAlpha(1.0f);
|
cursorCircle.setAlpha(1.0f);
|
||||||
cursorCircle.drawCentered(pausedMouseX, pausedMouseY);
|
cursorCircle.drawCentered(pausedMousePosition.x, pausedMousePosition.y);
|
||||||
Image cursorCirclePulse = cursorCircle.getScaledCopy(1f + pausePulse);
|
Image cursorCirclePulse = cursorCircle.getScaledCopy(1f + pausePulse);
|
||||||
cursorCirclePulse.setAlpha(1f - pausePulse);
|
cursorCirclePulse.setAlpha(1f - pausePulse);
|
||||||
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
|
cursorCirclePulse.drawCentered(pausedMousePosition.x, pausedMousePosition.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isReplay)
|
if (isReplay)
|
||||||
UI.draw(g, replayX, replayY, replayKeyPressed);
|
UI.draw(g, replayX, replayY, replayKeyPressed);
|
||||||
else if (GameMod.AUTO.isActive())
|
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())
|
else if (GameMod.AUTOPILOT.isActive())
|
||||||
UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed());
|
UI.draw(g, (int) autoMousePosition.x, (int) autoMousePosition.y, Utils.isGameKeyPressed());
|
||||||
else
|
else
|
||||||
UI.draw(g);
|
UI.draw(g);
|
||||||
}
|
}
|
||||||
@@ -564,8 +601,7 @@ public class Game extends BasicGameState {
|
|||||||
// returning from pause screen: must click previous mouse position
|
// returning from pause screen: must click previous mouse position
|
||||||
if (pauseTime > -1) {
|
if (pauseTime > -1) {
|
||||||
// paused during lead-in or break, or "relax" or "autopilot": continue immediately
|
// paused during lead-in or break, or "relax" or "autopilot": continue immediately
|
||||||
if ((pausedMouseX < 0 && pausedMouseY < 0) ||
|
if (pausedMousePosition == null || (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
|
||||||
(GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
|
|
||||||
pauseTime = -1;
|
pauseTime = -1;
|
||||||
if (!isLeadIn())
|
if (!isLeadIn())
|
||||||
MusicController.resume();
|
MusicController.resume();
|
||||||
@@ -603,6 +639,17 @@ public class Game extends BasicGameState {
|
|||||||
return;
|
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
|
// normal game update
|
||||||
if (!isReplay)
|
if (!isReplay)
|
||||||
addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
|
addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
|
||||||
@@ -613,7 +660,7 @@ public class Game extends BasicGameState {
|
|||||||
if (replayIndex >= replay.frames.length)
|
if (replayIndex >= replay.frames.length)
|
||||||
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
|
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 &&
|
if (isSeeking && replayIndex - 1 >= 1 && replayIndex < replay.frames.length &&
|
||||||
trackPosition < replay.frames[replayIndex - 1].getTime()) {
|
trackPosition < replay.frames[replayIndex - 1].getTime()) {
|
||||||
replayIndex = 0;
|
replayIndex = 0;
|
||||||
@@ -633,7 +680,6 @@ public class Game extends BasicGameState {
|
|||||||
timingPointIndex++;
|
timingPointIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
isSeeking = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update and run replay frames
|
// update and run replay frames
|
||||||
@@ -648,6 +694,12 @@ public class Game extends BasicGameState {
|
|||||||
}
|
}
|
||||||
mouseX = replayX;
|
mouseX = replayX;
|
||||||
mouseY = replayY;
|
mouseY = replayY;
|
||||||
|
|
||||||
|
// unmute sounds
|
||||||
|
if (isSeeking) {
|
||||||
|
isSeeking = false;
|
||||||
|
SoundController.mute(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data.updateDisplays(delta);
|
data.updateDisplays(delta);
|
||||||
@@ -662,16 +714,6 @@ public class Game extends BasicGameState {
|
|||||||
* @param keys the keys that are pressed
|
* @param keys the keys that are pressed
|
||||||
*/
|
*/
|
||||||
private void updateGame(int mouseX, int mouseY, int delta, int trackPosition, int keys) {
|
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!
|
// map complete!
|
||||||
if (objectIndex >= gameObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
|
if (objectIndex >= gameObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
|
||||||
// track ended before last object was processed: force a hit result
|
// track ended before last object was processed: force a hit result
|
||||||
@@ -699,6 +741,7 @@ public class Game extends BasicGameState {
|
|||||||
r.save();
|
r.save();
|
||||||
}
|
}
|
||||||
ScoreData score = data.getScoreData(beatmap);
|
ScoreData score = data.getScoreData(beatmap);
|
||||||
|
data.setGameplay(!isReplay);
|
||||||
|
|
||||||
// add score to database
|
// add score to database
|
||||||
if (!unranked && !isReplay)
|
if (!unranked && !isReplay)
|
||||||
@@ -745,8 +788,7 @@ public class Game extends BasicGameState {
|
|||||||
// pause game if focus lost
|
// pause game if focus lost
|
||||||
if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
|
if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
|
||||||
if (pauseTime < 0) {
|
if (pauseTime < 0) {
|
||||||
pausedMouseX = mouseX;
|
pausedMousePosition = new Vec2f(mouseX, mouseY);
|
||||||
pausedMouseY = mouseY;
|
|
||||||
pausePulse = 0f;
|
pausePulse = 0f;
|
||||||
}
|
}
|
||||||
if (MusicController.isPlaying() || isLeadIn())
|
if (MusicController.isPlaying() || isLeadIn())
|
||||||
@@ -819,8 +861,7 @@ public class Game extends BasicGameState {
|
|||||||
|
|
||||||
// pause game
|
// pause game
|
||||||
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
|
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
|
||||||
pausedMouseX = mouseX;
|
pausedMousePosition = new Vec2f(mouseX, mouseY);
|
||||||
pausedMouseY = mouseY;
|
|
||||||
pausePulse = 0f;
|
pausePulse = 0f;
|
||||||
}
|
}
|
||||||
if (MusicController.isPlaying() || isLeadIn())
|
if (MusicController.isPlaying() || isLeadIn())
|
||||||
@@ -888,6 +929,13 @@ public class Game extends BasicGameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
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:
|
case Input.KEY_UP:
|
||||||
UI.changeVolume(1);
|
UI.changeVolume(1);
|
||||||
break;
|
break;
|
||||||
@@ -923,9 +971,10 @@ public class Game extends BasicGameState {
|
|||||||
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
|
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// replay seeking
|
||||||
else if (!GameMod.AUTO.isActive() && y < 50) {
|
else if (Options.isReplaySeekingEnabled() && !GameMod.AUTO.isActive() && musicPositionBarContains(x, y)) {
|
||||||
float pos = (float) x / container.getWidth() * beatmap.endTime;
|
SoundController.mute(true); // mute sounds while seeking
|
||||||
|
float pos = (y - musicBarY) / musicBarHeight * beatmap.endTime;
|
||||||
MusicController.setPosition((int) pos);
|
MusicController.setPosition((int) pos);
|
||||||
isSeeking = true;
|
isSeeking = true;
|
||||||
}
|
}
|
||||||
@@ -939,8 +988,7 @@ public class Game extends BasicGameState {
|
|||||||
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
|
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
|
||||||
int trackPosition = MusicController.getPosition();
|
int trackPosition = MusicController.getPosition();
|
||||||
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
|
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
|
||||||
pausedMouseX = x;
|
pausedMousePosition = new Vec2f(x, y);
|
||||||
pausedMouseY = y;
|
|
||||||
pausePulse = 0f;
|
pausePulse = 0f;
|
||||||
}
|
}
|
||||||
if (MusicController.isPlaying() || isLeadIn())
|
if (MusicController.isPlaying() || isLeadIn())
|
||||||
@@ -969,13 +1017,12 @@ public class Game extends BasicGameState {
|
|||||||
private void gameKeyPressed(int keys, int x, int y, int trackPosition) {
|
private void gameKeyPressed(int keys, int x, int y, int trackPosition) {
|
||||||
// returning from pause screen
|
// returning from pause screen
|
||||||
if (pauseTime > -1) {
|
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;
|
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
|
||||||
if (distance < circleRadius) {
|
if (distance < circleRadius) {
|
||||||
// unpause the game
|
// unpause the game
|
||||||
pauseTime = -1;
|
pauseTime = -1;
|
||||||
pausedMouseX = -1;
|
pausedMousePosition = null;
|
||||||
pausedMouseY = -1;
|
|
||||||
if (!isLeadIn())
|
if (!isLeadIn())
|
||||||
MusicController.resume();
|
MusicController.resume();
|
||||||
}
|
}
|
||||||
@@ -1065,6 +1112,15 @@ public class Game extends BasicGameState {
|
|||||||
|
|
||||||
// restart the game
|
// restart the game
|
||||||
if (restart != Restart.FALSE) {
|
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) {
|
if (restart == Restart.NEW) {
|
||||||
// new game
|
// new game
|
||||||
loadImages();
|
loadImages();
|
||||||
@@ -1149,10 +1205,6 @@ public class Game extends BasicGameState {
|
|||||||
|
|
||||||
// load replay frames
|
// load replay frames
|
||||||
if (isReplay) {
|
if (isReplay) {
|
||||||
// load mods
|
|
||||||
previousMods = GameMod.getModState();
|
|
||||||
GameMod.loadModState(replay.mods);
|
|
||||||
|
|
||||||
// load initial data
|
// load initial data
|
||||||
replayX = container.getWidth() / 2;
|
replayX = container.getWidth() / 2;
|
||||||
replayY = container.getHeight() / 2;
|
replayY = container.getHeight() / 2;
|
||||||
@@ -1188,6 +1240,8 @@ public class Game extends BasicGameState {
|
|||||||
MusicController.setPosition(0);
|
MusicController.setPosition(0);
|
||||||
MusicController.setPitch(GameMod.getSpeedMultiplier());
|
MusicController.setPitch(GameMod.getSpeedMultiplier());
|
||||||
MusicController.pause();
|
MusicController.pause();
|
||||||
|
|
||||||
|
SoundController.mute(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
skipButton.resetHover();
|
skipButton.resetHover();
|
||||||
@@ -1242,10 +1296,10 @@ public class Game extends BasicGameState {
|
|||||||
final int followPointInterval = container.getHeight() / 14;
|
final int followPointInterval = container.getHeight() / 14;
|
||||||
int lastObjectEndTime = gameObjects[lastObjectIndex].getEndTime() + 1;
|
int lastObjectEndTime = gameObjects[lastObjectIndex].getEndTime() + 1;
|
||||||
int objectStartTime = beatmap.objects[index].getTime();
|
int objectStartTime = beatmap.objects[index].getTime();
|
||||||
float[] startXY = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
|
Vec2f startPoint = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
|
||||||
float[] endXY = gameObjects[index].getPointAt(objectStartTime);
|
Vec2f endPoint = gameObjects[index].getPointAt(objectStartTime);
|
||||||
float xDiff = endXY[0] - startXY[0];
|
float xDiff = endPoint.x - startPoint.x;
|
||||||
float yDiff = endXY[1] - startXY[1];
|
float yDiff = endPoint.y - startPoint.y;
|
||||||
float dist = (float) Math.hypot(xDiff, yDiff);
|
float dist = (float) Math.hypot(xDiff, yDiff);
|
||||||
int numPoints = (int) ((dist - GameImage.HITCIRCLE.getImage().getWidth()) / followPointInterval);
|
int numPoints = (int) ((dist - GameImage.HITCIRCLE.getImage().getWidth()) / followPointInterval);
|
||||||
if (numPoints > 0) {
|
if (numPoints > 0) {
|
||||||
@@ -1266,8 +1320,8 @@ public class Game extends BasicGameState {
|
|||||||
float step = 1f / (numPoints + 1);
|
float step = 1f / (numPoints + 1);
|
||||||
float t = step;
|
float t = step;
|
||||||
for (int i = 0; i < numPoints; i++) {
|
for (int i = 0; i < numPoints; i++) {
|
||||||
float x = startXY[0] + xDiff * t;
|
float x = startPoint.x + xDiff * t;
|
||||||
float y = startXY[1] + yDiff * t;
|
float y = startPoint.y + yDiff * t;
|
||||||
float nextT = t + step;
|
float nextT = t + step;
|
||||||
if (lastObjectIndex < objectIndex) { // fade the previous trail
|
if (lastObjectIndex < objectIndex) { // fade the previous trail
|
||||||
if (progress < nextT) {
|
if (progress < nextT) {
|
||||||
@@ -1321,8 +1375,7 @@ public class Game extends BasicGameState {
|
|||||||
timingPointIndex = 0;
|
timingPointIndex = 0;
|
||||||
beatLengthBase = beatLength = 1;
|
beatLengthBase = beatLength = 1;
|
||||||
pauseTime = -1;
|
pauseTime = -1;
|
||||||
pausedMouseX = -1;
|
pausedMousePosition = null;
|
||||||
pausedMouseY = -1;
|
|
||||||
countdownReadySound = false;
|
countdownReadySound = false;
|
||||||
countdown3Sound = false;
|
countdown3Sound = false;
|
||||||
countdown1Sound = false;
|
countdown1Sound = false;
|
||||||
@@ -1333,8 +1386,7 @@ public class Game extends BasicGameState {
|
|||||||
deathTime = -1;
|
deathTime = -1;
|
||||||
replayFrames = null;
|
replayFrames = null;
|
||||||
lastReplayTime = 0;
|
lastReplayTime = 0;
|
||||||
autoMouseX = 0;
|
autoMousePosition = new Vec2f();
|
||||||
autoMouseY = 0;
|
|
||||||
autoMousePressed = false;
|
autoMousePressed = false;
|
||||||
flashlightRadius = container.getHeight() * 2 / 3;
|
flashlightRadius = container.getHeight() * 2 / 3;
|
||||||
|
|
||||||
@@ -1376,9 +1428,9 @@ public class Game extends BasicGameState {
|
|||||||
// set images
|
// set images
|
||||||
File parent = beatmap.getFile().getParentFile();
|
File parent = beatmap.getFile().getParentFile();
|
||||||
for (GameImage img : GameImage.values()) {
|
for (GameImage img : GameImage.values()) {
|
||||||
if (img.isSkinnable()) {
|
if (img.isBeatmapSkinnable()) {
|
||||||
img.setDefaultImage();
|
img.setDefaultImage();
|
||||||
img.setSkinImage(parent);
|
img.setBeatmapSkinImage(parent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1390,6 +1442,8 @@ public class Game extends BasicGameState {
|
|||||||
Image skip = GameImage.SKIP.getImage();
|
Image skip = GameImage.SKIP.getImage();
|
||||||
skipButton = new MenuButton(skip, width - skip.getWidth() / 2f, height - (skip.getHeight() / 2f));
|
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);
|
skipButton.setHoverExpand(1.1f, MenuButton.Expand.UP_LEFT);
|
||||||
|
|
||||||
// load other images...
|
// load other images...
|
||||||
@@ -1420,14 +1474,15 @@ public class Game extends BasicGameState {
|
|||||||
|
|
||||||
// Stack modifier scales with hit object size
|
// Stack modifier scales with hit object size
|
||||||
// StackOffset = HitObjectRadius / 10
|
// 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);
|
HitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER);
|
||||||
|
|
||||||
// initialize objects
|
// initialize objects
|
||||||
Circle.init(container, circleSize);
|
Circle.init(container, diameter);
|
||||||
Slider.init(container, circleSize, beatmap);
|
Slider.init(container, diameter, beatmap);
|
||||||
Spinner.init(container, overallDifficulty);
|
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());
|
Options.getSkin().getSliderBorderColor() : beatmap.getSliderBorderColor());
|
||||||
|
|
||||||
// approachRate (hit object approach time)
|
// approachRate (hit object approach time)
|
||||||
@@ -1438,9 +1493,9 @@ public class Game extends BasicGameState {
|
|||||||
|
|
||||||
// overallDifficulty (hit result time offsets)
|
// overallDifficulty (hit result time offsets)
|
||||||
hitResultOffset = new int[GameData.HIT_MAX];
|
hitResultOffset = new int[GameData.HIT_MAX];
|
||||||
hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6));
|
hitResultOffset[GameData.HIT_300] = (int) (79.5f - (overallDifficulty * 6));
|
||||||
hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8));
|
hitResultOffset[GameData.HIT_100] = (int) (139.5f - (overallDifficulty * 8));
|
||||||
hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10));
|
hitResultOffset[GameData.HIT_50] = (int) (199.5f - (overallDifficulty * 10));
|
||||||
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
|
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
|
||||||
//final float mult = 0.608f;
|
//final float mult = 0.608f;
|
||||||
//hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6)) * mult);
|
//hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6)) * mult);
|
||||||
@@ -1454,6 +1509,14 @@ public class Game extends BasicGameState {
|
|||||||
|
|
||||||
// difficulty multiplier (scoring)
|
// difficulty multiplier (scoring)
|
||||||
data.calculateDifficultyMultiplier(beatmap.HPDrainRate, beatmap.circleSize, beatmap.overallDifficulty);
|
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; }
|
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).
|
* 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){
|
public synchronized void addReplayFrameAndRun(int x, int y, int keys, int time){
|
||||||
// "auto" and "autopilot" mods: use automatic cursor coordinates
|
// "auto" and "autopilot" mods: use automatic cursor coordinates
|
||||||
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
||||||
x = autoMouseX;
|
x = (int) autoMousePosition.x;
|
||||||
y = autoMouseY;
|
y = (int) autoMousePosition.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReplayFrame frame = addReplayFrame(x, y, keys, time);
|
ReplayFrame frame = addReplayFrame(x, y, keys, time);
|
||||||
@@ -1611,17 +1690,13 @@ public class Game extends BasicGameState {
|
|||||||
* @param endX the ending x coordinate
|
* @param endX the ending x coordinate
|
||||||
* @param endY the ending y coordinate
|
* @param endY the ending y coordinate
|
||||||
* @param t the t value [0, 1]
|
* @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
|
// "autopilot" mod: move quicker between objects
|
||||||
if (GameMod.AUTOPILOT.isActive())
|
if (GameMod.AUTOPILOT.isActive())
|
||||||
t = Utils.clamp(t * 2f, 0f, 1f);
|
t = Utils.clamp(t * 2f, 0f, 1f);
|
||||||
|
return new Vec2f(startX + (endX - startX) * t, startY + (endY - startY) * t);
|
||||||
float[] xy = new float[2];
|
|
||||||
xy[0] = startX + (endX - startX) * t;
|
|
||||||
xy[1] = startY + (endY - startY) * t;
|
|
||||||
return xy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1715,9 +1790,9 @@ public class Game extends BasicGameState {
|
|||||||
// possible special case: if slider end in the stack,
|
// possible special case: if slider end in the stack,
|
||||||
// all next hit objects in stack move right down
|
// all next hit objects in stack move right down
|
||||||
if (hitObjectN.isSlider()) {
|
if (hitObjectN.isSlider()) {
|
||||||
float[] p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
|
Vec2f p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
|
||||||
float[] p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
|
Vec2f p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
|
||||||
float distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
|
float distance = Utils.distance(p1.x, p1.y, p2.x, p2.y);
|
||||||
|
|
||||||
// check if hit object part of this stack
|
// check if hit object part of this stack
|
||||||
if (distance < STACK_LENIENCE * HitObject.getXMultiplier()) {
|
if (distance < STACK_LENIENCE * HitObject.getXMultiplier()) {
|
||||||
@@ -1725,7 +1800,7 @@ public class Game extends BasicGameState {
|
|||||||
for (int j = n + 1; j <= i; j++) {
|
for (int j = n + 1; j <= i; j++) {
|
||||||
HitObject hitObjectJ = beatmap.objects[j];
|
HitObject hitObjectJ = beatmap.objects[j];
|
||||||
p1 = gameObjects[j].getPointAt(hitObjectJ.getTime());
|
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
|
// hit object below slider end
|
||||||
if (distance < STACK_LENIENCE * HitObject.getXMultiplier())
|
if (distance < STACK_LENIENCE * HitObject.getXMultiplier())
|
||||||
@@ -1753,4 +1828,14 @@ public class Game extends BasicGameState {
|
|||||||
gameObjects[i].updatePosition();
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import itdelatrisu.opsu.audio.SoundController;
|
|||||||
import itdelatrisu.opsu.audio.SoundEffect;
|
import itdelatrisu.opsu.audio.SoundEffect;
|
||||||
import itdelatrisu.opsu.ui.MenuButton;
|
import itdelatrisu.opsu.ui.MenuButton;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||||
|
|
||||||
import org.lwjgl.input.Keyboard;
|
import org.lwjgl.input.Keyboard;
|
||||||
import org.newdawn.slick.Color;
|
import org.newdawn.slick.Color;
|
||||||
@@ -61,7 +62,7 @@ public class GamePauseMenu extends BasicGameState {
|
|||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
private Input input;
|
private Input input;
|
||||||
private int state;
|
private final int state;
|
||||||
private Game gameState;
|
private Game gameState;
|
||||||
|
|
||||||
public GamePauseMenu(int state) {
|
public GamePauseMenu(int state) {
|
||||||
@@ -86,10 +87,10 @@ public class GamePauseMenu extends BasicGameState {
|
|||||||
|
|
||||||
// don't draw default background if button skinned and background unskinned
|
// don't draw default background if button skinned and background unskinned
|
||||||
boolean buttonsSkinned =
|
boolean buttonsSkinned =
|
||||||
GameImage.PAUSE_CONTINUE.hasSkinImage() ||
|
GameImage.PAUSE_CONTINUE.hasBeatmapSkinImage() ||
|
||||||
GameImage.PAUSE_RETRY.hasSkinImage() ||
|
GameImage.PAUSE_RETRY.hasBeatmapSkinImage() ||
|
||||||
GameImage.PAUSE_BACK.hasSkinImage();
|
GameImage.PAUSE_BACK.hasBeatmapSkinImage();
|
||||||
if (!buttonsSkinned || bg.hasSkinImage())
|
if (!buttonsSkinned || bg.hasBeatmapSkinImage())
|
||||||
bg.getImage().draw();
|
bg.getImage().draw();
|
||||||
else
|
else
|
||||||
g.setBackground(Color.black);
|
g.setBackground(Color.black);
|
||||||
@@ -133,7 +134,7 @@ public class GamePauseMenu extends BasicGameState {
|
|||||||
SoundController.playSound(SoundEffect.MENUBACK);
|
SoundController.playSound(SoundEffect.MENUBACK);
|
||||||
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
|
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
|
||||||
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
|
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
|
||||||
if (UI.getCursor().isSkinned())
|
if (UI.getCursor().isBeatmapSkinned())
|
||||||
UI.getCursor().reset();
|
UI.getCursor().reset();
|
||||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||||
} else {
|
} else {
|
||||||
@@ -187,7 +188,7 @@ public class GamePauseMenu extends BasicGameState {
|
|||||||
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
|
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
|
||||||
else
|
else
|
||||||
MusicController.resume();
|
MusicController.resume();
|
||||||
if (UI.getCursor().isSkinned())
|
if (UI.getCursor().isBeatmapSkinned())
|
||||||
UI.getCursor().reset();
|
UI.getCursor().reset();
|
||||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
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);
|
continueButton = new MenuButton(GameImage.PAUSE_CONTINUE.getImage(), width / 2f, height * 0.25f);
|
||||||
retryButton = new MenuButton(GameImage.PAUSE_RETRY.getImage(), width / 2f, height * 0.5f);
|
retryButton = new MenuButton(GameImage.PAUSE_RETRY.getImage(), width / 2f, height * 0.5f);
|
||||||
backButton = new MenuButton(GameImage.PAUSE_BACK.getImage(), width / 2f, height * 0.75f);
|
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();
|
continueButton.setHoverExpand();
|
||||||
retryButton.setHoverExpand();
|
retryButton.setHoverExpand();
|
||||||
backButton.setHoverExpand();
|
backButton.setHoverExpand();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package itdelatrisu.opsu.states;
|
|||||||
|
|
||||||
import itdelatrisu.opsu.GameData;
|
import itdelatrisu.opsu.GameData;
|
||||||
import itdelatrisu.opsu.GameImage;
|
import itdelatrisu.opsu.GameImage;
|
||||||
|
import itdelatrisu.opsu.GameMod;
|
||||||
import itdelatrisu.opsu.Opsu;
|
import itdelatrisu.opsu.Opsu;
|
||||||
import itdelatrisu.opsu.Options;
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
@@ -68,7 +69,7 @@ public class GameRanking extends BasicGameState {
|
|||||||
// game-related variables
|
// game-related variables
|
||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
private int state;
|
private final int state;
|
||||||
private Input input;
|
private Input input;
|
||||||
|
|
||||||
public GameRanking(int state) {
|
public GameRanking(int state) {
|
||||||
@@ -105,7 +106,7 @@ public class GameRanking extends BasicGameState {
|
|||||||
Beatmap beatmap = MusicController.getBeatmap();
|
Beatmap beatmap = MusicController.getBeatmap();
|
||||||
|
|
||||||
// background
|
// background
|
||||||
if (!beatmap.drawBG(width, height, 0.7f, true))
|
if (!beatmap.drawBackground(width, height, 0.7f, true))
|
||||||
GameImage.PLAYFIELD.getImage().draw(0,0);
|
GameImage.PLAYFIELD.getImage().draw(0,0);
|
||||||
|
|
||||||
// ranking screen elements
|
// ranking screen elements
|
||||||
@@ -113,7 +114,7 @@ public class GameRanking extends BasicGameState {
|
|||||||
|
|
||||||
// buttons
|
// buttons
|
||||||
replayButton.draw();
|
replayButton.draw();
|
||||||
if (data.isGameplay())
|
if (data.isGameplay() && !GameMod.AUTO.isActive())
|
||||||
retryButton.draw();
|
retryButton.draw();
|
||||||
UI.getBackButton().draw();
|
UI.getBackButton().draw();
|
||||||
|
|
||||||
@@ -175,7 +176,8 @@ public class GameRanking extends BasicGameState {
|
|||||||
// replay
|
// replay
|
||||||
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
|
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
|
||||||
boolean returnToGame = false;
|
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);
|
Replay r = data.getReplay(null, null);
|
||||||
if (r != null) {
|
if (r != null) {
|
||||||
try {
|
try {
|
||||||
@@ -194,7 +196,9 @@ public class GameRanking extends BasicGameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// retry
|
// 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.setReplay(null);
|
||||||
gameState.setRestart(Game.Restart.MANUAL);
|
gameState.setRestart(Game.Restart.MANUAL);
|
||||||
returnToGame = true;
|
returnToGame = true;
|
||||||
@@ -221,7 +225,7 @@ public class GameRanking extends BasicGameState {
|
|||||||
} else {
|
} else {
|
||||||
SoundController.playSound(SoundEffect.APPLAUSE);
|
SoundController.playSound(SoundEffect.APPLAUSE);
|
||||||
retryButton.resetHover();
|
retryButton.resetHover();
|
||||||
replayButton.setY(replayY);
|
replayButton.setY(!GameMod.AUTO.isActive() ? replayY : retryY);
|
||||||
}
|
}
|
||||||
replayButton.resetHover();
|
replayButton.resetHover();
|
||||||
}
|
}
|
||||||
@@ -239,12 +243,11 @@ public class GameRanking extends BasicGameState {
|
|||||||
*/
|
*/
|
||||||
private void returnToSongMenu() {
|
private void returnToSongMenu() {
|
||||||
SoundController.playSound(SoundEffect.MENUBACK);
|
SoundController.playSound(SoundEffect.MENUBACK);
|
||||||
if (data.isGameplay()) {
|
|
||||||
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
|
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
|
||||||
songMenu.resetGameDataOnLoad();
|
if (data.isGameplay())
|
||||||
songMenu.resetTrackOnLoad();
|
songMenu.resetTrackOnLoad();
|
||||||
}
|
songMenu.resetGameDataOnLoad();
|
||||||
if (UI.getCursor().isSkinned())
|
if (UI.getCursor().isBeatmapSkinned())
|
||||||
UI.getCursor().reset();
|
UI.getCursor().reset();
|
||||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,13 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
|||||||
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||||
import itdelatrisu.opsu.downloads.Updater;
|
import itdelatrisu.opsu.downloads.Updater;
|
||||||
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
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;
|
||||||
import itdelatrisu.opsu.ui.MenuButton.Expand;
|
import itdelatrisu.opsu.ui.MenuButton.Expand;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||||
|
|
||||||
import java.awt.Desktop;
|
import java.awt.Desktop;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -61,7 +65,7 @@ import org.newdawn.slick.state.transition.FadeOutTransition;
|
|||||||
*/
|
*/
|
||||||
public class MainMenu extends BasicGameState {
|
public class MainMenu extends BasicGameState {
|
||||||
/** Idle time, in milliseconds, before returning the logo to its original position. */
|
/** 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. */
|
/** Max alpha level of the menu background. */
|
||||||
private static final float BG_MAX_ALPHA = 0.9f;
|
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. */
|
/** Logo button that reveals other buttons on click. */
|
||||||
private MenuButton logo;
|
private MenuButton logo;
|
||||||
|
|
||||||
/** Whether or not the logo has been clicked. */
|
/** Logo states. */
|
||||||
private boolean logoClicked = false;
|
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. */
|
/** Delay timer, in milliseconds, before starting to move the logo back to the center. */
|
||||||
private int logoTimer = 0;
|
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. */
|
/** Main "Play" and "Exit" buttons. */
|
||||||
private MenuButton playButton, exitButton;
|
private MenuButton playButton, exitButton;
|
||||||
|
|
||||||
@@ -87,8 +100,8 @@ public class MainMenu extends BasicGameState {
|
|||||||
/** Button linking to repository. */
|
/** Button linking to repository. */
|
||||||
private MenuButton repoButton;
|
private MenuButton repoButton;
|
||||||
|
|
||||||
/** Button for installing updates. */
|
/** Buttons for installing updates. */
|
||||||
private MenuButton updateButton;
|
private MenuButton updateButton, restartButton;
|
||||||
|
|
||||||
/** Application start time, for drawing the total running time. */
|
/** Application start time, for drawing the total running time. */
|
||||||
private long programStartTime;
|
private long programStartTime;
|
||||||
@@ -97,7 +110,7 @@ public class MainMenu extends BasicGameState {
|
|||||||
private Stack<Integer> previous;
|
private Stack<Integer> previous;
|
||||||
|
|
||||||
/** Background alpha level (for fade-in effect). */
|
/** 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. */
|
/** Whether or not a notification was already sent upon entering. */
|
||||||
private boolean enterNotification = false;
|
private boolean enterNotification = false;
|
||||||
@@ -105,16 +118,11 @@ public class MainMenu extends BasicGameState {
|
|||||||
/** Music position bar coordinates and dimensions. */
|
/** Music position bar coordinates and dimensions. */
|
||||||
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
|
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
|
// game-related variables
|
||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
private Input input;
|
private Input input;
|
||||||
private int state;
|
private final int state;
|
||||||
|
|
||||||
public MainMenu(int state) {
|
public MainMenu(int state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
@@ -145,9 +153,18 @@ public class MainMenu extends BasicGameState {
|
|||||||
exitButton = new MenuButton(exitImg,
|
exitButton = new MenuButton(exitImg,
|
||||||
width * 0.75f - exitOffset, (height / 2) + (exitImg.getHeight() / 2f)
|
width * 0.75f - exitOffset, (height / 2) + (exitImg.getHeight() / 2f)
|
||||||
);
|
);
|
||||||
logo.setHoverExpand(1.05f);
|
final int logoAnimationDuration = 350;
|
||||||
playButton.setHoverExpand(1.05f);
|
logo.setHoverAnimationDuration(logoAnimationDuration);
|
||||||
exitButton.setHoverExpand(1.05f);
|
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
|
// initialize music buttons
|
||||||
int musicWidth = GameImage.MUSIC_PLAY.getImage().getWidth();
|
int musicWidth = GameImage.MUSIC_PLAY.getImage().getWidth();
|
||||||
@@ -170,24 +187,40 @@ public class MainMenu extends BasicGameState {
|
|||||||
// initialize downloads button
|
// initialize downloads button
|
||||||
Image dlImg = GameImage.DOWNLOADS.getImage();
|
Image dlImg = GameImage.DOWNLOADS.getImage();
|
||||||
downloadsButton = new MenuButton(dlImg, width - dlImg.getWidth() / 2f, height / 2f);
|
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);
|
downloadsButton.setHoverExpand(1.03f, Expand.LEFT);
|
||||||
|
|
||||||
// initialize repository button
|
// initialize repository button
|
||||||
float startX = width * 0.997f, startY = height * 0.997f;
|
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();
|
Image repoImg = GameImage.REPOSITORY.getImage();
|
||||||
repoButton = new MenuButton(repoImg,
|
repoButton = new MenuButton(repoImg,
|
||||||
startX - repoImg.getWidth(), startY - repoImg.getHeight()
|
startX - repoImg.getWidth(), startY - repoImg.getHeight()
|
||||||
);
|
);
|
||||||
|
repoButton.setHoverAnimationDuration(350);
|
||||||
|
repoButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
|
||||||
repoButton.setHoverExpand();
|
repoButton.setHoverExpand();
|
||||||
startX -= repoImg.getWidth() * 1.75f;
|
}
|
||||||
} else
|
|
||||||
startX -= width * 0.005f;
|
|
||||||
|
|
||||||
// initialize update button
|
// initialize update buttons
|
||||||
Image bangImg = GameImage.BANG.getImage();
|
float updateX = width / 2f, updateY = height * 17 / 18f;
|
||||||
updateButton = new MenuButton(bangImg, startX - bangImg.getWidth(), startY - bangImg.getHeight());
|
Image downloadImg = GameImage.DOWNLOAD.getImage();
|
||||||
updateButton.setHoverExpand(1.15f);
|
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();
|
reset();
|
||||||
}
|
}
|
||||||
@@ -201,27 +234,27 @@ public class MainMenu extends BasicGameState {
|
|||||||
// draw background
|
// draw background
|
||||||
Beatmap beatmap = MusicController.getBeatmap();
|
Beatmap beatmap = MusicController.getBeatmap();
|
||||||
if (Options.isDynamicBackgroundEnabled() &&
|
if (Options.isDynamicBackgroundEnabled() &&
|
||||||
beatmap != null && beatmap.drawBG(width, height, bgAlpha, true))
|
beatmap != null && beatmap.drawBackground(width, height, bgAlpha.getValue(), true))
|
||||||
;
|
;
|
||||||
else {
|
else {
|
||||||
Image bg = GameImage.MENU_BG.getImage();
|
Image bg = GameImage.MENU_BG.getImage();
|
||||||
bg.setAlpha(bgAlpha);
|
bg.setAlpha(bgAlpha.getValue());
|
||||||
bg.draw();
|
bg.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
// top/bottom horizontal bars
|
// top/bottom horizontal bars
|
||||||
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
|
float oldAlpha = Colors.BLACK_ALPHA.a;
|
||||||
Utils.COLOR_BLACK_ALPHA.a = 0.2f;
|
Colors.BLACK_ALPHA.a = 0.2f;
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
g.fillRect(0, 0, width, height / 9f);
|
g.fillRect(0, 0, width, height / 9f);
|
||||||
g.fillRect(0, height * 8 / 9f, 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
|
// draw downloads button
|
||||||
downloadsButton.draw();
|
downloadsButton.draw();
|
||||||
|
|
||||||
// draw buttons
|
// draw buttons
|
||||||
if (logoTimer > 0) {
|
if (logoState == LogoState.OPEN || logoState == LogoState.CLOSING) {
|
||||||
playButton.draw();
|
playButton.draw();
|
||||||
exitButton.draw();
|
exitButton.draw();
|
||||||
}
|
}
|
||||||
@@ -237,7 +270,7 @@ public class MainMenu extends BasicGameState {
|
|||||||
|
|
||||||
// draw music position bar
|
// draw music position bar
|
||||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
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.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
|
||||||
g.setColor(Color.white);
|
g.setColor(Color.white);
|
||||||
if (!MusicController.isTrackLoading() && beatmap != null) {
|
if (!MusicController.isTrackLoading() && beatmap != null) {
|
||||||
@@ -251,35 +284,26 @@ public class MainMenu extends BasicGameState {
|
|||||||
|
|
||||||
// draw update button
|
// draw update button
|
||||||
if (Updater.get().showButton()) {
|
if (Updater.get().showButton()) {
|
||||||
Color updateColor = null;
|
Updater.Status status = Updater.get().getStatus();
|
||||||
switch (Updater.get().getStatus()) {
|
if (status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING)
|
||||||
case UPDATE_AVAILABLE:
|
updateButton.draw();
|
||||||
updateColor = Color.red;
|
else if (status == Updater.Status.UPDATE_DOWNLOADED)
|
||||||
break;
|
restartButton.draw();
|
||||||
case UPDATE_DOWNLOADED:
|
|
||||||
updateColor = Color.green;
|
|
||||||
break;
|
|
||||||
case UPDATE_DOWNLOADING:
|
|
||||||
updateColor = Color.yellow;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
updateColor = Color.white;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
updateButton.draw(updateColor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw text
|
// draw text
|
||||||
float marginX = width * 0.015f, topMarginY = height * 0.01f, bottomMarginY = height * 0.015f;
|
float marginX = width * 0.015f, topMarginY = height * 0.01f, bottomMarginY = height * 0.015f;
|
||||||
g.setFont(Utils.FONT_MEDIUM);
|
g.setFont(Fonts.MEDIUM);
|
||||||
float lineHeight = Utils.FONT_MEDIUM.getLineHeight() * 0.925f;
|
float lineHeight = Fonts.MEDIUM.getLineHeight() * 0.925f;
|
||||||
g.drawString(String.format("Loaded %d songs and %d beatmaps.",
|
g.drawString(String.format("Loaded %d songs and %d beatmaps.",
|
||||||
BeatmapSetList.get().getMapSetCount(), BeatmapSetList.get().getMapCount()), marginX, topMarginY);
|
BeatmapSetList.get().getMapSetCount(), BeatmapSetList.get().getMapCount()), marginX, topMarginY);
|
||||||
if (MusicController.isTrackLoading())
|
if (MusicController.isTrackLoading())
|
||||||
g.drawString("Track loading...", marginX, topMarginY + lineHeight);
|
g.drawString("Track loading...", marginX, topMarginY + lineHeight);
|
||||||
else if (MusicController.trackExists()) {
|
else if (MusicController.trackExists()) {
|
||||||
if (Options.useUnicodeMetadata()) // load glyphs
|
if (Options.useUnicodeMetadata()) { // load glyphs
|
||||||
Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, beatmap.artistUnicode);
|
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.titleUnicode);
|
||||||
|
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.artistUnicode);
|
||||||
|
}
|
||||||
g.drawString((MusicController.isPlaying()) ? "Now Playing:" : "Paused:", marginX, topMarginY + lineHeight);
|
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));
|
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);
|
exitButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
|
||||||
if (repoButton != null)
|
if (repoButton != null)
|
||||||
repoButton.hoverUpdate(delta, mouseX, mouseY);
|
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);
|
downloadsButton.hoverUpdate(delta, mouseX, mouseY);
|
||||||
// ensure only one button is in hover state at once
|
// ensure only one button is in hover state at once
|
||||||
boolean noHoverUpdate = musicPositionBarContains(mouseX, mouseY);
|
boolean noHoverUpdate = musicPositionBarContains(mouseX, mouseY);
|
||||||
@@ -322,46 +349,46 @@ public class MainMenu extends BasicGameState {
|
|||||||
MusicController.toggleTrackDimmed(0.33f);
|
MusicController.toggleTrackDimmed(0.33f);
|
||||||
|
|
||||||
// fade in background
|
// fade in background
|
||||||
if (bgAlpha < BG_MAX_ALPHA) {
|
Beatmap beatmap = MusicController.getBeatmap();
|
||||||
bgAlpha += delta / 1000f;
|
if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading()))
|
||||||
if (bgAlpha > BG_MAX_ALPHA)
|
bgAlpha.update(delta);
|
||||||
bgAlpha = BG_MAX_ALPHA;
|
|
||||||
}
|
|
||||||
|
|
||||||
// buttons
|
// buttons
|
||||||
if (logoClicked) {
|
int centerX = container.getWidth() / 2;
|
||||||
if (logoTimer == 0) { // shifting to left
|
float currentLogoButtonAlpha;
|
||||||
if (logo.getX() > container.getWidth() / 3.3f)
|
switch (logoState) {
|
||||||
logo.setX(logo.getX() - delta);
|
case DEFAULT:
|
||||||
else
|
break;
|
||||||
logoTimer = 1;
|
case OPENING:
|
||||||
} else if (logoTimer >= MOVE_DELAY) // timer over: shift back to center
|
if (logoOpen.update(delta)) // shifting to left
|
||||||
logoClicked = false;
|
logo.setX(centerX - logoOpen.getValue());
|
||||||
else { // increment timer
|
else {
|
||||||
logoTimer += delta;
|
logoState = LogoState.OPEN;
|
||||||
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;
|
logoTimer = 0;
|
||||||
|
logoButtonAlpha.setTime(0);
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
// move back to original location
|
case OPEN:
|
||||||
if (logo.getX() < container.getWidth() / 2) {
|
if (logoButtonAlpha.update(delta)) { // fade in buttons
|
||||||
logo.setX(logo.getX() + (delta / 3f));
|
currentLogoButtonAlpha = logoButtonAlpha.getValue();
|
||||||
if (logo.getX() > container.getWidth() / 2)
|
playButton.getImage().setAlpha(currentLogoButtonAlpha);
|
||||||
logo.setX(container.getWidth() / 2);
|
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;
|
||||||
|
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
|
// tooltips
|
||||||
@@ -373,8 +400,12 @@ public class MainMenu extends BasicGameState {
|
|||||||
UI.updateTooltip(delta, "Next track", false);
|
UI.updateTooltip(delta, "Next track", false);
|
||||||
else if (musicPrevious.contains(mouseX, mouseY))
|
else if (musicPrevious.contains(mouseX, mouseY))
|
||||||
UI.updateTooltip(delta, "Previous track", false);
|
UI.updateTooltip(delta, "Previous track", false);
|
||||||
else if (Updater.get().showButton() && updateButton.contains(mouseX, mouseY))
|
else if (Updater.get().showButton()) {
|
||||||
UI.updateTooltip(delta, Updater.get().getStatus().getDescription(), true);
|
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
|
@Override
|
||||||
@@ -412,8 +443,8 @@ public class MainMenu extends BasicGameState {
|
|||||||
musicPrevious.resetHover();
|
musicPrevious.resetHover();
|
||||||
if (repoButton != null && !repoButton.contains(mouseX, mouseY))
|
if (repoButton != null && !repoButton.contains(mouseX, mouseY))
|
||||||
repoButton.resetHover();
|
repoButton.resetHover();
|
||||||
if (!updateButton.contains(mouseX, mouseY))
|
|
||||||
updateButton.resetHover();
|
updateButton.resetHover();
|
||||||
|
restartButton.resetHover();
|
||||||
if (!downloadsButton.contains(mouseX, mouseY))
|
if (!downloadsButton.contains(mouseX, mouseY))
|
||||||
downloadsButton.resetHover();
|
downloadsButton.resetHover();
|
||||||
}
|
}
|
||||||
@@ -449,71 +480,85 @@ public class MainMenu extends BasicGameState {
|
|||||||
MusicController.resume();
|
MusicController.resume();
|
||||||
UI.sendBarNotification("Play");
|
UI.sendBarNotification("Play");
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
} else if (musicNext.contains(x, y)) {
|
} else if (musicNext.contains(x, y)) {
|
||||||
nextTrack();
|
nextTrack();
|
||||||
UI.sendBarNotification(">> Next");
|
UI.sendBarNotification(">> Next");
|
||||||
|
return;
|
||||||
} else if (musicPrevious.contains(x, y)) {
|
} else if (musicPrevious.contains(x, y)) {
|
||||||
if (!previous.isEmpty()) {
|
if (!previous.isEmpty()) {
|
||||||
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
|
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
|
||||||
menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false);
|
menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false);
|
||||||
if (Options.isDynamicBackgroundEnabled())
|
if (Options.isDynamicBackgroundEnabled())
|
||||||
bgAlpha = 0f;
|
bgAlpha.setTime(0);
|
||||||
} else
|
} else
|
||||||
MusicController.setPosition(0);
|
MusicController.setPosition(0);
|
||||||
UI.sendBarNotification("<< Previous");
|
UI.sendBarNotification("<< Previous");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloads button actions
|
// downloads button actions
|
||||||
else if (downloadsButton.contains(x, y)) {
|
if (downloadsButton.contains(x, y)) {
|
||||||
SoundController.playSound(SoundEffect.MENUHIT);
|
SoundController.playSound(SoundEffect.MENUHIT);
|
||||||
game.enterState(Opsu.STATE_DOWNLOADSMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
game.enterState(Opsu.STATE_DOWNLOADSMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// repository button actions
|
// repository button actions
|
||||||
else if (repoButton != null && repoButton.contains(x, y)) {
|
if (repoButton != null && repoButton.contains(x, y)) {
|
||||||
try {
|
try {
|
||||||
Desktop.getDesktop().browse(Options.REPOSITORY_URI);
|
Desktop.getDesktop().browse(Options.REPOSITORY_URI);
|
||||||
|
} catch (UnsupportedOperationException e) {
|
||||||
|
UI.sendBarNotification("The repository web page could not be opened.");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
ErrorHandler.error("Could not browse to repository URI.", e, false);
|
ErrorHandler.error("Could not browse to repository URI.", e, false);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update button actions
|
// update button actions
|
||||||
else if (Updater.get().showButton() && updateButton.contains(x, y)) {
|
if (Updater.get().showButton()) {
|
||||||
switch (Updater.get().getStatus()) {
|
Updater.Status status = Updater.get().getStatus();
|
||||||
case UPDATE_AVAILABLE:
|
if (updateButton.contains(x, y) && status == Updater.Status.UPDATE_AVAILABLE) {
|
||||||
SoundController.playSound(SoundEffect.MENUHIT);
|
SoundController.playSound(SoundEffect.MENUHIT);
|
||||||
Updater.get().startDownload();
|
Updater.get().startDownload();
|
||||||
break;
|
updateButton.removeHoverEffects();
|
||||||
case UPDATE_DOWNLOADED:
|
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);
|
SoundController.playSound(SoundEffect.MENUHIT);
|
||||||
Updater.get().prepareUpdate();
|
Updater.get().prepareUpdate();
|
||||||
container.setForceExit(false);
|
container.setForceExit(false);
|
||||||
container.exit();
|
container.exit();
|
||||||
break;
|
return;
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// start moving logo (if clicked)
|
// start moving logo (if clicked)
|
||||||
else if (!logoClicked) {
|
if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) {
|
||||||
if (logo.contains(x, y, 0.25f)) {
|
if (logo.contains(x, y, 0.25f)) {
|
||||||
logoClicked = true;
|
logoState = LogoState.OPENING;
|
||||||
|
logoOpen.setTime(0);
|
||||||
logoTimer = 0;
|
logoTimer = 0;
|
||||||
playButton.getImage().setAlpha(0f);
|
playButton.getImage().setAlpha(0f);
|
||||||
exitButton.getImage().setAlpha(0f);
|
exitButton.getImage().setAlpha(0f);
|
||||||
SoundController.playSound(SoundEffect.MENUHIT);
|
SoundController.playSound(SoundEffect.MENUHIT);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// other button actions (if visible)
|
// 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)) {
|
if (logo.contains(x, y, 0.25f) || playButton.contains(x, y, 0.25f)) {
|
||||||
SoundController.playSound(SoundEffect.MENUHIT);
|
SoundController.playSound(SoundEffect.MENUHIT);
|
||||||
enterSongMenu();
|
enterSongMenu();
|
||||||
} else if (exitButton.contains(x, y, 0.25f))
|
return;
|
||||||
|
} else if (exitButton.contains(x, y, 0.25f)) {
|
||||||
container.exit();
|
container.exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,8 +577,9 @@ public class MainMenu extends BasicGameState {
|
|||||||
break;
|
break;
|
||||||
case Input.KEY_P:
|
case Input.KEY_P:
|
||||||
SoundController.playSound(SoundEffect.MENUHIT);
|
SoundController.playSound(SoundEffect.MENUHIT);
|
||||||
if (!logoClicked) {
|
if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) {
|
||||||
logoClicked = true;
|
logoState = LogoState.OPENING;
|
||||||
|
logoOpen.setTime(0);
|
||||||
logoTimer = 0;
|
logoTimer = 0;
|
||||||
playButton.getImage().setAlpha(0f);
|
playButton.getImage().setAlpha(0f);
|
||||||
exitButton.getImage().setAlpha(0f);
|
exitButton.getImage().setAlpha(0f);
|
||||||
@@ -581,8 +627,11 @@ public class MainMenu extends BasicGameState {
|
|||||||
public void reset() {
|
public void reset() {
|
||||||
// reset logo
|
// reset logo
|
||||||
logo.setX(container.getWidth() / 2);
|
logo.setX(container.getWidth() / 2);
|
||||||
logoClicked = false;
|
logoOpen.setTime(0);
|
||||||
|
logoClose.setTime(0);
|
||||||
|
logoButtonAlpha.setTime(0);
|
||||||
logoTimer = 0;
|
logoTimer = 0;
|
||||||
|
logoState = LogoState.DEFAULT;
|
||||||
|
|
||||||
logo.resetHover();
|
logo.resetHover();
|
||||||
playButton.resetHover();
|
playButton.resetHover();
|
||||||
@@ -594,6 +643,7 @@ public class MainMenu extends BasicGameState {
|
|||||||
if (repoButton != null)
|
if (repoButton != null)
|
||||||
repoButton.resetHover();
|
repoButton.resetHover();
|
||||||
updateButton.resetHover();
|
updateButton.resetHover();
|
||||||
|
restartButton.resetHover();
|
||||||
downloadsButton.resetHover();
|
downloadsButton.resetHover();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +661,7 @@ public class MainMenu extends BasicGameState {
|
|||||||
previous.add(node.index);
|
previous.add(node.index);
|
||||||
}
|
}
|
||||||
if (Options.isDynamicBackgroundEnabled() && !sameAudio && !MusicController.isThemePlaying())
|
if (Options.isDynamicBackgroundEnabled() && !sameAudio && !MusicController.isThemePlaying())
|
||||||
bgAlpha = 0f;
|
bgAlpha.setTime(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import itdelatrisu.opsu.Utils;
|
|||||||
import itdelatrisu.opsu.audio.MusicController;
|
import itdelatrisu.opsu.audio.MusicController;
|
||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.audio.SoundEffect;
|
import itdelatrisu.opsu.audio.SoundEffect;
|
||||||
|
import itdelatrisu.opsu.ui.Colors;
|
||||||
|
import itdelatrisu.opsu.ui.Fonts;
|
||||||
import itdelatrisu.opsu.ui.MenuButton;
|
import itdelatrisu.opsu.ui.MenuButton;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
|
||||||
@@ -86,14 +88,18 @@ public class OptionsMenu extends BasicGameState {
|
|||||||
GameOption.DISABLE_MOUSE_WHEEL,
|
GameOption.DISABLE_MOUSE_WHEEL,
|
||||||
GameOption.DISABLE_MOUSE_BUTTONS,
|
GameOption.DISABLE_MOUSE_BUTTONS,
|
||||||
GameOption.CURSOR_SIZE,
|
GameOption.CURSOR_SIZE,
|
||||||
GameOption.NEW_CURSOR
|
GameOption.NEW_CURSOR,
|
||||||
|
GameOption.DISABLE_CURSOR
|
||||||
}),
|
}),
|
||||||
CUSTOM ("Custom", new GameOption[] {
|
CUSTOM ("Custom", new GameOption[] {
|
||||||
GameOption.FIXED_CS,
|
GameOption.FIXED_CS,
|
||||||
GameOption.FIXED_HP,
|
GameOption.FIXED_HP,
|
||||||
GameOption.FIXED_AR,
|
GameOption.FIXED_AR,
|
||||||
GameOption.FIXED_OD,
|
GameOption.FIXED_OD,
|
||||||
GameOption.CHECKPOINT
|
GameOption.CHECKPOINT,
|
||||||
|
GameOption.REPLAY_SEEKING,
|
||||||
|
GameOption.DISABLE_UPDATER,
|
||||||
|
GameOption.ENABLE_WATCH_SERVICE
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Total number of tabs. */
|
/** Total number of tabs. */
|
||||||
@@ -110,10 +116,10 @@ public class OptionsMenu extends BasicGameState {
|
|||||||
private static OptionTab[] values = values();
|
private static OptionTab[] values = values();
|
||||||
|
|
||||||
/** Tab name. */
|
/** Tab name. */
|
||||||
private String name;
|
private final String name;
|
||||||
|
|
||||||
/** Options array. */
|
/** Options array. */
|
||||||
public GameOption[] options;
|
public final GameOption[] options;
|
||||||
|
|
||||||
/** Associated tab button. */
|
/** Associated tab button. */
|
||||||
public MenuButton button;
|
public MenuButton button;
|
||||||
@@ -163,7 +169,7 @@ public class OptionsMenu extends BasicGameState {
|
|||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
private Input input;
|
private Input input;
|
||||||
private Graphics g;
|
private Graphics g;
|
||||||
private int state;
|
private final int state;
|
||||||
|
|
||||||
public OptionsMenu(int state) {
|
public OptionsMenu(int state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
@@ -182,8 +188,8 @@ public class OptionsMenu extends BasicGameState {
|
|||||||
|
|
||||||
// option tabs
|
// option tabs
|
||||||
Image tabImage = GameImage.MENU_TAB.getImage();
|
Image tabImage = GameImage.MENU_TAB.getImage();
|
||||||
float tabX = width * 0.032f + Utils.FONT_DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
|
float tabX = width * 0.032f + Fonts.DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
|
||||||
float tabY = Utils.FONT_XLARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
|
float tabY = Fonts.XLARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
|
||||||
height * 0.015f - (tabImage.getHeight() / 2f);
|
height * 0.015f - (tabImage.getHeight() / 2f);
|
||||||
int tabOffset = Math.min(tabImage.getWidth(), width / OptionTab.SIZE);
|
int tabOffset = Math.min(tabImage.getWidth(), width / OptionTab.SIZE);
|
||||||
for (OptionTab tab : OptionTab.values())
|
for (OptionTab tab : OptionTab.values())
|
||||||
@@ -198,22 +204,19 @@ public class OptionsMenu extends BasicGameState {
|
|||||||
@Override
|
@Override
|
||||||
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
||||||
throws SlickException {
|
throws SlickException {
|
||||||
g.setBackground(Utils.COLOR_BLACK_ALPHA);
|
|
||||||
|
|
||||||
int width = container.getWidth();
|
int width = container.getWidth();
|
||||||
int height = container.getHeight();
|
int height = container.getHeight();
|
||||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
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
|
// title
|
||||||
float marginX = width * 0.015f, marginY = height * 0.01f;
|
float marginX = width * 0.015f, marginY = height * 0.01f;
|
||||||
Utils.FONT_XLARGE.drawString(marginX, marginY, "Options", Color.white);
|
Fonts.XLARGE.drawString(marginX, marginY, "Options", Color.white);
|
||||||
Utils.FONT_DEFAULT.drawString(marginX, marginY + Utils.FONT_XLARGE.getLineHeight() * 0.92f,
|
Fonts.DEFAULT.drawString(marginX, marginY + Fonts.XLARGE.getLineHeight() * 0.92f,
|
||||||
"Change the way opsu! behaves", Color.white);
|
"Change the way opsu! behaves", Color.white);
|
||||||
|
|
||||||
// background
|
|
||||||
GameImage.OPTIONS_BG.getImage().draw(0, lineY);
|
|
||||||
|
|
||||||
// game options
|
// game options
|
||||||
g.setLineWidth(1f);
|
g.setLineWidth(1f);
|
||||||
GameOption hoverOption = (keyEntryLeft) ? GameOption.KEY_LEFT :
|
GameOption hoverOption = (keyEntryLeft) ? GameOption.KEY_LEFT :
|
||||||
@@ -241,6 +244,7 @@ public class OptionsMenu extends BasicGameState {
|
|||||||
currentTab.getName(), true, false);
|
currentTab.getName(), true, false);
|
||||||
g.setColor(Color.white);
|
g.setColor(Color.white);
|
||||||
g.setLineWidth(2f);
|
g.setLineWidth(2f);
|
||||||
|
float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
|
||||||
g.drawLine(0, lineY, width, lineY);
|
g.drawLine(0, lineY, width, lineY);
|
||||||
g.resetLineWidth();
|
g.resetLineWidth();
|
||||||
|
|
||||||
@@ -248,15 +252,15 @@ public class OptionsMenu extends BasicGameState {
|
|||||||
|
|
||||||
// key entry state
|
// key entry state
|
||||||
if (keyEntryLeft || keyEntryRight) {
|
if (keyEntryLeft || keyEntryRight) {
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
g.fillRect(0, 0, width, height);
|
g.fillRect(0, 0, width, height);
|
||||||
g.setColor(Color.white);
|
g.setColor(Color.white);
|
||||||
String prompt = (keyEntryLeft) ?
|
String prompt = (keyEntryLeft) ?
|
||||||
"Please press the new left-click key." :
|
"Please press the new left-click key." :
|
||||||
"Please press the new right-click key.";
|
"Please press the new right-click key.";
|
||||||
Utils.FONT_LARGE.drawString(
|
Fonts.LARGE.drawString(
|
||||||
(width / 2) - (Utils.FONT_LARGE.getWidth(prompt) / 2),
|
(width / 2) - (Fonts.LARGE.getWidth(prompt) / 2),
|
||||||
(height / 2) - Utils.FONT_LARGE.getLineHeight(), prompt
|
(height / 2) - Fonts.LARGE.getLineHeight(), prompt
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,14 +417,14 @@ public class OptionsMenu extends BasicGameState {
|
|||||||
*/
|
*/
|
||||||
private void drawOption(GameOption option, int pos, boolean focus) {
|
private void drawOption(GameOption option, int pos, boolean focus) {
|
||||||
int width = container.getWidth();
|
int width = container.getWidth();
|
||||||
int textHeight = Utils.FONT_LARGE.getLineHeight();
|
int textHeight = Fonts.LARGE.getLineHeight();
|
||||||
float y = textY + (pos * offsetY);
|
float y = textY + (pos * offsetY);
|
||||||
Color color = (focus) ? Color.cyan : Color.white;
|
Color color = (focus) ? Color.cyan : Color.white;
|
||||||
|
|
||||||
Utils.FONT_LARGE.drawString(width / 30, y, option.getName(), color);
|
Fonts.LARGE.drawString(width / 30, y, option.getName(), color);
|
||||||
Utils.FONT_LARGE.drawString(width / 2, y, option.getValueString(), color);
|
Fonts.LARGE.drawString(width / 2, y, option.getValueString(), color);
|
||||||
Utils.FONT_SMALL.drawString(width / 30, y + textHeight, option.getDescription(), color);
|
Fonts.SMALL.drawString(width / 30, y + textHeight, option.getDescription(), color);
|
||||||
g.setColor(Utils.COLOR_WHITE_ALPHA);
|
g.setColor(Colors.WHITE_ALPHA);
|
||||||
g.drawLine(0, y + textHeight, width, y + textHeight);
|
g.drawLine(0, y + textHeight, width, y + textHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,7 +437,7 @@ public class OptionsMenu extends BasicGameState {
|
|||||||
if (y < textY || y > textY + (offsetY * maxOptionsScreen))
|
if (y < textY || y > textY + (offsetY * maxOptionsScreen))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
int index = (y - textY + Utils.FONT_LARGE.getLineHeight()) / offsetY;
|
int index = (y - textY + Fonts.LARGE.getLineHeight()) / offsetY;
|
||||||
if (index >= currentTab.options.length)
|
if (index >= currentTab.options.length)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import itdelatrisu.opsu.GameImage;
|
|||||||
import itdelatrisu.opsu.GameMod;
|
import itdelatrisu.opsu.GameMod;
|
||||||
import itdelatrisu.opsu.Opsu;
|
import itdelatrisu.opsu.Opsu;
|
||||||
import itdelatrisu.opsu.Options;
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.OszUnpacker;
|
|
||||||
import itdelatrisu.opsu.ScoreData;
|
import itdelatrisu.opsu.ScoreData;
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.audio.MultiClip;
|
import itdelatrisu.opsu.audio.MultiClip;
|
||||||
@@ -32,18 +31,32 @@ import itdelatrisu.opsu.audio.MusicController;
|
|||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.audio.SoundEffect;
|
import itdelatrisu.opsu.audio.SoundEffect;
|
||||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||||
|
import itdelatrisu.opsu.beatmap.BeatmapDifficultyCalculator;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||||
|
import itdelatrisu.opsu.beatmap.BeatmapSet;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
|
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.BeatmapDB;
|
||||||
import itdelatrisu.opsu.db.ScoreDB;
|
import itdelatrisu.opsu.db.ScoreDB;
|
||||||
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
||||||
import itdelatrisu.opsu.ui.KinecticScrolling;
|
import itdelatrisu.opsu.ui.KinecticScrolling;
|
||||||
|
import itdelatrisu.opsu.ui.Colors;
|
||||||
|
import itdelatrisu.opsu.ui.Fonts;
|
||||||
import itdelatrisu.opsu.ui.MenuButton;
|
import itdelatrisu.opsu.ui.MenuButton;
|
||||||
|
import itdelatrisu.opsu.ui.StarStream;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||||
|
|
||||||
import java.io.File;
|
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.Map;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
|
|
||||||
@@ -145,8 +158,8 @@ public class SongMenu extends BasicGameState {
|
|||||||
/** Button coordinate values. */
|
/** Button coordinate values. */
|
||||||
private float buttonX, buttonY, buttonOffset, buttonWidth, buttonHeight;
|
private float buttonX, buttonY, buttonOffset, buttonWidth, buttonHeight;
|
||||||
|
|
||||||
/** Current x offset of song buttons for mouse hover, in pixels. */
|
/** Horizontal offset of song buttons for mouse hover, in pixels. */
|
||||||
private float hoverOffset = 0f;
|
private AnimatedValue hoverOffset = new AnimatedValue(250, 0, MAX_HOVER_OFFSET, AnimationEquation.OUT_QUART);
|
||||||
|
|
||||||
/** Current index of hovered song button. */
|
/** Current index of hovered song button. */
|
||||||
private BeatmapSetNode hoverIndex = null;
|
private BeatmapSetNode hoverIndex = null;
|
||||||
@@ -209,11 +222,52 @@ public class SongMenu extends BasicGameState {
|
|||||||
/** The text length of the last string in the search TextField. */
|
/** The text length of the last string in the search TextField. */
|
||||||
private int lastSearchTextLength = -1;
|
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
|
// game-related variables
|
||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private StateBasedGame game;
|
private StateBasedGame game;
|
||||||
private Input input;
|
private Input input;
|
||||||
private int state;
|
private final int state;
|
||||||
|
|
||||||
public SongMenu(int state) {
|
public SongMenu(int state) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
@@ -231,8 +285,8 @@ public class SongMenu extends BasicGameState {
|
|||||||
|
|
||||||
// header/footer coordinates
|
// header/footer coordinates
|
||||||
headerY = height * 0.0075f + GameImage.MENU_MUSICNOTE.getImage().getHeight() +
|
headerY = height * 0.0075f + GameImage.MENU_MUSICNOTE.getImage().getHeight() +
|
||||||
Utils.FONT_BOLD.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
|
Fonts.BOLD.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
|
||||||
Utils.FONT_SMALL.getLineHeight();
|
Fonts.SMALL.getLineHeight();
|
||||||
footerY = height - GameImage.SELECTION_MODS.getImage().getHeight();
|
footerY = height - GameImage.SELECTION_MODS.getImage().getHeight();
|
||||||
|
|
||||||
// initialize sorts
|
// initialize sorts
|
||||||
@@ -253,11 +307,11 @@ public class SongMenu extends BasicGameState {
|
|||||||
buttonOffset = (footerY - headerY - DIVIDER_LINE_WIDTH) / MAX_SONG_BUTTONS;
|
buttonOffset = (footerY - headerY - DIVIDER_LINE_WIDTH) / MAX_SONG_BUTTONS;
|
||||||
|
|
||||||
// search
|
// search
|
||||||
int textFieldX = (int) (width * 0.7125f + Utils.FONT_BOLD.getWidth("Search: "));
|
int textFieldX = (int) (width * 0.7125f + Fonts.BOLD.getWidth("Search: "));
|
||||||
int textFieldY = (int) (headerY + Utils.FONT_BOLD.getLineHeight() / 2);
|
int textFieldY = (int) (headerY + Fonts.BOLD.getLineHeight() / 2);
|
||||||
search = new TextField(
|
search = new TextField(
|
||||||
container, Utils.FONT_BOLD, textFieldX, textFieldY,
|
container, Fonts.BOLD, textFieldX, textFieldY,
|
||||||
(int) (width * 0.99f) - textFieldX, Utils.FONT_BOLD.getLineHeight()
|
(int) (width * 0.99f) - textFieldX, Fonts.BOLD.getLineHeight()
|
||||||
);
|
);
|
||||||
search.setBackgroundColor(Color.transparent);
|
search.setBackgroundColor(Color.transparent);
|
||||||
search.setBorderColor(Color.transparent);
|
search.setBorderColor(Color.transparent);
|
||||||
@@ -287,6 +341,22 @@ public class SongMenu extends BasicGameState {
|
|||||||
int loaderDim = GameImage.MENU_MUSICNOTE.getImage().getWidth();
|
int loaderDim = GameImage.MENU_MUSICNOTE.getImage().getWidth();
|
||||||
SpriteSheet spr = new SpriteSheet(GameImage.MENU_LOADER.getImage(), loaderDim, loaderDim);
|
SpriteSheet spr = new SpriteSheet(GameImage.MENU_LOADER.getImage(), loaderDim, loaderDim);
|
||||||
loader = new Animation(spr, 50);
|
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
|
@Override
|
||||||
@@ -300,22 +370,29 @@ public class SongMenu extends BasicGameState {
|
|||||||
|
|
||||||
// background
|
// background
|
||||||
if (focusNode != null) {
|
if (focusNode != null) {
|
||||||
Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
|
Beatmap focusNodeBeatmap = focusNode.getSelectedBeatmap();
|
||||||
if (!focusNodeBeatmap.drawBG(width, height, 1.0f, true))
|
if (!focusNodeBeatmap.drawBackground(width, height, bgAlpha.getValue(), true))
|
||||||
GameImage.PLAYFIELD.getImage().draw();
|
GameImage.PLAYFIELD.getImage().draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// star stream
|
||||||
|
starStream.draw();
|
||||||
|
|
||||||
// song buttons
|
// song buttons
|
||||||
BeatmapSetNode node = startNode;
|
BeatmapSetNode node = startNode;
|
||||||
|
int startNodeOffsetoffset = 0;
|
||||||
|
if (node.prev != null) {
|
||||||
|
startNodeOffsetoffset = -1;
|
||||||
|
node = node.prev;
|
||||||
|
}
|
||||||
g.setClip(0, (int) (headerY + DIVIDER_LINE_WIDTH / 2), width, (int) (footerY - headerY));
|
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 + startNodeOffsetoffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) {
|
||||||
// draw the node
|
// draw the node
|
||||||
float offset = (node == hoverIndex) ? hoverOffset : 0f;
|
float offset = (node == hoverIndex) ? hoverOffset.getValue() : 0f;
|
||||||
float ypos = buttonY + (i*buttonOffset) ;
|
float ypos = buttonY + (i*buttonOffset) ;
|
||||||
float mid = height/2 - ypos - buttonOffset/2;
|
float mid = height/2 - ypos - buttonOffset/2;
|
||||||
final float circleRadi = 1000 * GameImage.getUIscale();
|
final float circleRadi = 700 * GameImage.getUIscale();
|
||||||
//finds points along a very large circle
|
//finds points along a very large circle (x^2 = h^2 - y^2)
|
||||||
// x^2 = h^2 - y^2
|
|
||||||
float t = circleRadi * circleRadi - (mid * mid);
|
float t = circleRadi * circleRadi - (mid * mid);
|
||||||
float xpos = (float)(t>0?Math.sqrt(t):0) - circleRadi + 50 * GameImage.getUIscale();
|
float xpos = (float)(t>0?Math.sqrt(t):0) - circleRadi + 50 * GameImage.getUIscale();
|
||||||
ScoreData[] scores = getScoreDataForNode(node, false);
|
ScoreData[] scores = getScoreDataForNode(node, false);
|
||||||
@@ -336,7 +413,7 @@ public class SongMenu extends BasicGameState {
|
|||||||
MAX_SONG_BUTTONS * buttonOffset,
|
MAX_SONG_BUTTONS * buttonOffset,
|
||||||
width, headerY + DIVIDER_LINE_WIDTH / 2,
|
width, headerY + DIVIDER_LINE_WIDTH / 2,
|
||||||
0, MAX_SONG_BUTTONS * buttonOffset,
|
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);
|
ScoreData.clipToDownloadArea(g);
|
||||||
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
|
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
|
||||||
int offset = (int) (-startScorePos.getPosition() + startScore * 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)
|
if (rank < 0)
|
||||||
continue;
|
continue;
|
||||||
if (rank >= focusScores.length)
|
|
||||||
break;
|
|
||||||
long prevScore = (rank + 1 < focusScores.length) ? focusScores[rank + 1].score : -1;
|
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();
|
g.clearClip();
|
||||||
|
|
||||||
@@ -361,12 +443,11 @@ public class SongMenu extends BasicGameState {
|
|||||||
ScoreData.drawScrollbar(g, startScorePos.getPosition() , focusScores.length * ScoreData.getButtonOffset());
|
ScoreData.drawScrollbar(g, startScorePos.getPosition() , focusScores.length * ScoreData.getButtonOffset());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// top/bottom bars
|
// top/bottom bars
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
g.fillRect(0, 0, width, headerY);
|
g.fillRect(0, 0, width, headerY);
|
||||||
g.fillRect(0, footerY, width, height - footerY);
|
g.fillRect(0, footerY, width, height - footerY);
|
||||||
g.setColor(Utils.COLOR_BLUE_DIVIDER);
|
g.setColor(Colors.BLUE_DIVIDER);
|
||||||
g.setLineWidth(DIVIDER_LINE_WIDTH);
|
g.setLineWidth(DIVIDER_LINE_WIDTH);
|
||||||
g.drawLine(0, headerY, width, headerY);
|
g.drawLine(0, headerY, width, headerY);
|
||||||
g.drawLine(0, footerY, width, footerY);
|
g.drawLine(0, footerY, width, footerY);
|
||||||
@@ -379,8 +460,13 @@ public class SongMenu extends BasicGameState {
|
|||||||
Image musicNote = GameImage.MENU_MUSICNOTE.getImage();
|
Image musicNote = GameImage.MENU_MUSICNOTE.getImage();
|
||||||
if (MusicController.isTrackLoading())
|
if (MusicController.isTrackLoading())
|
||||||
loader.draw(marginX, marginY);
|
loader.draw(marginX, marginY);
|
||||||
else
|
else {
|
||||||
musicNote.draw(marginX, marginY);
|
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();
|
int iconWidth = musicNote.getWidth();
|
||||||
|
|
||||||
// song info text
|
// song info text
|
||||||
@@ -388,26 +474,49 @@ public class SongMenu extends BasicGameState {
|
|||||||
songInfo = focusNode.getInfo();
|
songInfo = focusNode.getInfo();
|
||||||
if (Options.useUnicodeMetadata()) { // load glyphs
|
if (Options.useUnicodeMetadata()) { // load glyphs
|
||||||
Beatmap beatmap = focusNode.getBeatmapSet().get(0);
|
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;
|
marginX += 5;
|
||||||
|
Color c = Colors.WHITE_FADE;
|
||||||
|
float oldAlpha = c.a;
|
||||||
|
float t = AnimationEquation.OUT_QUAD.calc(songChangeTimer.getValue());
|
||||||
float headerTextY = marginY * 0.2f;
|
float headerTextY = marginY * 0.2f;
|
||||||
Utils.FONT_LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], Color.white);
|
c.a = Math.min(t * songInfo.length / 1.5f, 1f);
|
||||||
headerTextY += Utils.FONT_LARGE.getLineHeight() - 6;
|
if (c.a > 0)
|
||||||
Utils.FONT_DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], Color.white);
|
Fonts.LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], c);
|
||||||
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 2;
|
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();
|
float speedModifier = GameMod.getSpeedMultiplier();
|
||||||
Color color2 = (speedModifier == 1f) ? Color.white :
|
Color color2 = (speedModifier == 1f) ? c :
|
||||||
(speedModifier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
|
(speedModifier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT;
|
||||||
Utils.FONT_BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
|
float oldAlpha2 = color2.a;
|
||||||
headerTextY += Utils.FONT_BOLD.getLineHeight() - 4;
|
color2.a = c.a;
|
||||||
Utils.FONT_DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white);
|
Fonts.BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
|
||||||
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 4;
|
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();
|
float multiplier = GameMod.getDifficultyMultiplier();
|
||||||
Color color4 = (multiplier == 1f) ? Color.white :
|
Color color4 = (multiplier == 1f) ? c :
|
||||||
(multiplier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
|
(multiplier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT;
|
||||||
Utils.FONT_SMALL.drawString(marginX, headerTextY, songInfo[4], color4);
|
float oldAlpha4 = color4.a;
|
||||||
|
color4.a = c.a;
|
||||||
|
Fonts.SMALL.drawString(marginX, headerTextY, songInfo[4], color4);
|
||||||
|
color4.a = oldAlpha4;
|
||||||
|
}
|
||||||
|
c.a = oldAlpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
// selection buttons
|
// selection buttons
|
||||||
@@ -440,38 +549,41 @@ public class SongMenu extends BasicGameState {
|
|||||||
int searchX = search.getX(), searchY = search.getY();
|
int searchX = search.getX(), searchY = search.getY();
|
||||||
float searchBaseX = width * 0.7f;
|
float searchBaseX = width * 0.7f;
|
||||||
float searchTextX = width * 0.7125f;
|
float searchTextX = width * 0.7125f;
|
||||||
float searchRectHeight = Utils.FONT_BOLD.getLineHeight() * 2;
|
float searchRectHeight = Fonts.BOLD.getLineHeight() * 2;
|
||||||
float searchExtraHeight = Utils.FONT_DEFAULT.getLineHeight() * 0.7f;
|
float searchExtraHeight = Fonts.DEFAULT.getLineHeight() * 0.7f;
|
||||||
float searchProgress = (searchTransitionTimer < SEARCH_TRANSITION_TIME) ?
|
float searchProgress = (searchTransitionTimer < SEARCH_TRANSITION_TIME) ?
|
||||||
((float) searchTransitionTimer / SEARCH_TRANSITION_TIME) : 1f;
|
((float) searchTransitionTimer / SEARCH_TRANSITION_TIME) : 1f;
|
||||||
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
|
float oldAlpha = Colors.BLACK_ALPHA.a;
|
||||||
if (searchEmpty) {
|
if (searchEmpty) {
|
||||||
searchRectHeight += (1f - searchProgress) * searchExtraHeight;
|
searchRectHeight += (1f - searchProgress) * searchExtraHeight;
|
||||||
Utils.COLOR_BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f;
|
Colors.BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f;
|
||||||
} else {
|
} else {
|
||||||
searchRectHeight += searchProgress * searchExtraHeight;
|
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);
|
g.fillRect(searchBaseX, headerY + DIVIDER_LINE_WIDTH / 2, width - searchBaseX, searchRectHeight);
|
||||||
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
|
Colors.BLACK_ALPHA.a = oldAlpha;
|
||||||
Utils.FONT_BOLD.drawString(searchTextX, searchY, "Search:", Utils.COLOR_GREEN_SEARCH);
|
Fonts.BOLD.drawString(searchTextX, searchY, "Search:", Colors.GREEN_SEARCH);
|
||||||
if (searchEmpty)
|
if (searchEmpty)
|
||||||
Utils.FONT_BOLD.drawString(searchX, searchY, "Type to search!", Color.white);
|
Fonts.BOLD.drawString(searchX, searchY, "Type to search!", Color.white);
|
||||||
else {
|
else {
|
||||||
g.setColor(Color.white);
|
g.setColor(Color.white);
|
||||||
// TODO: why is this needed to correctly position the TextField?
|
// TODO: why is this needed to correctly position the TextField?
|
||||||
search.setLocation(searchX - 3, searchY - 1);
|
search.setLocation(searchX - 3, searchY - 1);
|
||||||
search.render(container, g);
|
search.render(container, g);
|
||||||
search.setLocation(searchX, searchY);
|
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);
|
(searchResultString == null) ? "Searching..." : searchResultString, Color.white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// reloading beatmaps
|
// reloading beatmaps
|
||||||
if (reloadThread != null) {
|
if (reloadThread != null) {
|
||||||
// darken the screen
|
// darken the screen
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
g.fillRect(0, 0, width, height);
|
g.fillRect(0, 0, width, height);
|
||||||
|
|
||||||
UI.drawLoadingProgress(g);
|
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
|
||||||
search.setFocus(true);
|
search.setFocus(true);
|
||||||
searchTimer += delta;
|
searchTimer += delta;
|
||||||
@@ -574,14 +701,10 @@ public class SongMenu extends BasicGameState {
|
|||||||
if ((mouseX > cx && mouseX < cx + buttonWidth) &&
|
if ((mouseX > cx && mouseX < cx + buttonWidth) &&
|
||||||
(mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) {
|
(mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) {
|
||||||
if (node == hoverIndex) {
|
if (node == hoverIndex) {
|
||||||
if (hoverOffset < MAX_HOVER_OFFSET) {
|
hoverOffset.update(delta);
|
||||||
hoverOffset += delta / 3f;
|
|
||||||
if (hoverOffset > MAX_HOVER_OFFSET)
|
|
||||||
hoverOffset = MAX_HOVER_OFFSET;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
hoverIndex = node ;
|
hoverIndex = node ;
|
||||||
hoverOffset = 0f;
|
hoverOffset.setTime(0);
|
||||||
}
|
}
|
||||||
isHover = true;
|
isHover = true;
|
||||||
break;
|
break;
|
||||||
@@ -589,21 +712,19 @@ public class SongMenu extends BasicGameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isHover) {
|
if (!isHover) {
|
||||||
hoverOffset = 0f;
|
hoverOffset.setTime(0);
|
||||||
hoverIndex = null;
|
hoverIndex = null;
|
||||||
} else
|
} else
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// tooltips
|
// tooltips
|
||||||
if (focusScores != null) {
|
if (focusScores != null && ScoreData.areaContains(mouseX, mouseY)) {
|
||||||
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
|
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
|
||||||
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
|
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
|
||||||
for (int i = 0; i < MAX_SCORE_BUTTONS; i++) {
|
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
|
||||||
int rank = startScore + i;
|
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
|
||||||
if (rank < 0)
|
if (rank < 0)
|
||||||
continue;
|
continue;
|
||||||
if (rank >= focusScores.length)
|
|
||||||
break;
|
|
||||||
if (ScoreData.buttonContains(mouseX, mouseY - offset, i)) {
|
if (ScoreData.buttonContains(mouseX, mouseY - offset, i)) {
|
||||||
UI.updateTooltip(delta, focusScores[rank].getTooltipString(), true);
|
UI.updateTooltip(delta, focusScores[rank].getTooltipString(), true);
|
||||||
break;
|
break;
|
||||||
@@ -689,7 +810,7 @@ public class SongMenu extends BasicGameState {
|
|||||||
float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX;
|
float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX;
|
||||||
if ((x > cx && x < cx + buttonWidth) &&
|
if ((x > cx && x < cx + buttonWidth) &&
|
||||||
(y > buttonY + (i * buttonOffset) && y < buttonY + (i * buttonOffset) + buttonHeight)) {
|
(y > buttonY + (i * buttonOffset) && y < buttonY + (i * buttonOffset) + buttonHeight)) {
|
||||||
float oldHoverOffset = hoverOffset;
|
int oldHoverOffsetTime = hoverOffset.getTime();
|
||||||
BeatmapSetNode oldHoverIndex = hoverIndex;
|
BeatmapSetNode oldHoverIndex = hoverIndex;
|
||||||
|
|
||||||
// clicked node is already expanded
|
// clicked node is already expanded
|
||||||
@@ -714,7 +835,7 @@ public class SongMenu extends BasicGameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// restore hover data
|
// restore hover data
|
||||||
hoverOffset = oldHoverOffset;
|
hoverOffset.setTime(oldHoverOffsetTime);
|
||||||
hoverIndex = oldHoverIndex;
|
hoverIndex = oldHoverIndex;
|
||||||
|
|
||||||
// open beatmap menu
|
// open beatmap menu
|
||||||
@@ -728,12 +849,10 @@ public class SongMenu extends BasicGameState {
|
|||||||
|
|
||||||
// score buttons
|
// score buttons
|
||||||
if (focusScores != null && ScoreData.areaContains(x, y)) {
|
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 startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
|
||||||
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
|
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
|
||||||
int rank = startScore + i;
|
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
|
||||||
if (rank >= focusScores.length)
|
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
|
||||||
break;
|
|
||||||
if (ScoreData.buttonContains(x, y - offset, i)) {
|
if (ScoreData.buttonContains(x, y - offset, i)) {
|
||||||
SoundController.playSound(SoundEffect.MENUHIT);
|
SoundController.playSound(SoundEffect.MENUHIT);
|
||||||
if (button != Input.MOUSE_RIGHT_BUTTON) {
|
if (button != Input.MOUSE_RIGHT_BUTTON) {
|
||||||
@@ -805,8 +924,12 @@ public class SongMenu extends BasicGameState {
|
|||||||
break;
|
break;
|
||||||
case Input.KEY_F5:
|
case Input.KEY_F5:
|
||||||
SoundController.playSound(SoundEffect.MENUHIT);
|
SoundController.playSound(SoundEffect.MENUHIT);
|
||||||
|
if (songFolderChanged)
|
||||||
|
reloadBeatmaps(false);
|
||||||
|
else {
|
||||||
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD);
|
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD);
|
||||||
game.enterState(Opsu.STATE_BUTTONMENU);
|
game.enterState(Opsu.STATE_BUTTONMENU);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Input.KEY_DELETE:
|
case Input.KEY_DELETE:
|
||||||
if (focusNode == null)
|
if (focusNode == null)
|
||||||
@@ -851,11 +974,11 @@ public class SongMenu extends BasicGameState {
|
|||||||
if (next != null) {
|
if (next != null) {
|
||||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||||
BeatmapSetNode oldStartNode = startNode;
|
BeatmapSetNode oldStartNode = startNode;
|
||||||
float oldHoverOffset = hoverOffset;
|
int oldHoverOffsetTime = hoverOffset.getTime();
|
||||||
BeatmapSetNode oldHoverIndex = hoverIndex;
|
BeatmapSetNode oldHoverIndex = hoverIndex;
|
||||||
setFocus(next, 0, false, true);
|
setFocus(next, 0, false, true);
|
||||||
if (startNode == oldStartNode) {
|
if (startNode == oldStartNode) {
|
||||||
hoverOffset = oldHoverOffset;
|
hoverOffset.setTime(oldHoverOffsetTime);
|
||||||
hoverIndex = oldHoverIndex;
|
hoverIndex = oldHoverIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -867,11 +990,11 @@ public class SongMenu extends BasicGameState {
|
|||||||
if (prev != null) {
|
if (prev != null) {
|
||||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||||
BeatmapSetNode oldStartNode = startNode;
|
BeatmapSetNode oldStartNode = startNode;
|
||||||
float oldHoverOffset = hoverOffset;
|
int oldHoverOffsetTime = hoverOffset.getTime();
|
||||||
BeatmapSetNode oldHoverIndex = hoverIndex;
|
BeatmapSetNode oldHoverIndex = hoverIndex;
|
||||||
setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.getBeatmapSet().size() - 1, false, true);
|
setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.getBeatmapSet().size() - 1, false, true);
|
||||||
if (startNode == oldStartNode) {
|
if (startNode == oldStartNode) {
|
||||||
hoverOffset = oldHoverOffset;
|
hoverOffset.setTime(oldHoverOffsetTime);
|
||||||
hoverIndex = oldHoverIndex;
|
hoverIndex = oldHoverIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -965,18 +1088,26 @@ public class SongMenu extends BasicGameState {
|
|||||||
selectRandomButton.resetHover();
|
selectRandomButton.resetHover();
|
||||||
selectMapOptionsButton.resetHover();
|
selectMapOptionsButton.resetHover();
|
||||||
selectOptionsButton.resetHover();
|
selectOptionsButton.resetHover();
|
||||||
hoverOffset = 0f;
|
hoverOffset.setTime(0);
|
||||||
hoverIndex = null;
|
hoverIndex = null;
|
||||||
startScorePos.setPosition(0);
|
startScorePos.setPosition(0);
|
||||||
beatmapMenuTimer = -1;
|
beatmapMenuTimer = -1;
|
||||||
searchTransitionTimer = SEARCH_TRANSITION_TIME;
|
searchTransitionTimer = SEARCH_TRANSITION_TIME;
|
||||||
songInfo = null;
|
songInfo = null;
|
||||||
|
bgAlpha.setTime(bgAlpha.getDuration());
|
||||||
|
songChangeTimer.setTime(songChangeTimer.getDuration());
|
||||||
|
musicIconBounceTimer.setTime(musicIconBounceTimer.getDuration());
|
||||||
|
starStream.clear();
|
||||||
|
|
||||||
// reset song stack
|
// reset song stack
|
||||||
randomStack = new Stack<SongNode>();
|
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)
|
// 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);
|
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
|
||||||
|
|
||||||
// reset music track
|
// reset music track
|
||||||
@@ -1003,13 +1134,13 @@ public class SongMenu extends BasicGameState {
|
|||||||
|
|
||||||
// destroy skin images, if any
|
// destroy skin images, if any
|
||||||
for (GameImage img : GameImage.values()) {
|
for (GameImage img : GameImage.values()) {
|
||||||
if (img.isSkinnable())
|
if (img.isBeatmapSkinnable())
|
||||||
img.destroySkinImage();
|
img.destroyBeatmapSkinImage();
|
||||||
}
|
}
|
||||||
|
|
||||||
// reload scores
|
// reload scores
|
||||||
if (focusNode != null) {
|
if (focusNode != null) {
|
||||||
scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
|
scoreMap = ScoreDB.getMapSetScores(focusNode.getSelectedBeatmap());
|
||||||
focusScores = getScoreDataForNode(focusNode, true);
|
focusScores = getScoreDataForNode(focusNode, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1022,7 +1153,7 @@ public class SongMenu extends BasicGameState {
|
|||||||
case BEATMAP: // clear all scores
|
case BEATMAP: // clear all scores
|
||||||
if (stateActionNode == null || stateActionNode.beatmapIndex == -1)
|
if (stateActionNode == null || stateActionNode.beatmapIndex == -1)
|
||||||
break;
|
break;
|
||||||
Beatmap beatmap = stateActionNode.getBeatmapSet().get(stateActionNode.beatmapIndex);
|
Beatmap beatmap = stateActionNode.getSelectedBeatmap();
|
||||||
ScoreDB.deleteScore(beatmap);
|
ScoreDB.deleteScore(beatmap);
|
||||||
if (stateActionNode == focusNode) {
|
if (stateActionNode == focusNode) {
|
||||||
focusScores = null;
|
focusScores = null;
|
||||||
@@ -1033,7 +1164,7 @@ public class SongMenu extends BasicGameState {
|
|||||||
if (stateActionScore == null)
|
if (stateActionScore == null)
|
||||||
break;
|
break;
|
||||||
ScoreDB.deleteScore(stateActionScore);
|
ScoreDB.deleteScore(stateActionScore);
|
||||||
scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
|
scoreMap = ScoreDB.getMapSetScores(focusNode.getSelectedBeatmap());
|
||||||
focusScores = getScoreDataForNode(focusNode, true);
|
focusScores = getScoreDataForNode(focusNode, true);
|
||||||
startScorePos.setPosition(0);
|
startScorePos.setPosition(0);
|
||||||
break;
|
break;
|
||||||
@@ -1095,44 +1226,7 @@ public class SongMenu extends BasicGameState {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case RELOAD: // reload beatmaps
|
case RELOAD: // reload beatmaps
|
||||||
// reset state and node references
|
reloadBeatmaps(true);
|
||||||
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();
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@@ -1210,15 +1304,25 @@ public class SongMenu extends BasicGameState {
|
|||||||
if (node == null)
|
if (node == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
hoverOffset = 0f;
|
hoverOffset.setTime(0);
|
||||||
hoverIndex = null;
|
hoverIndex = null;
|
||||||
songInfo = null;
|
songInfo = null;
|
||||||
|
songChangeTimer.setTime(0);
|
||||||
|
musicIconBounceTimer.setTime(0);
|
||||||
BeatmapSetNode oldFocus = focusNode;
|
BeatmapSetNode oldFocus = focusNode;
|
||||||
|
|
||||||
// expand node before focusing it
|
// expand node before focusing it
|
||||||
int expandedIndex = BeatmapSetList.get().getExpandedIndex();
|
int expandedIndex = BeatmapSetList.get().getExpandedIndex();
|
||||||
if (node.index != expandedIndex) {
|
if (node.index != expandedIndex) {
|
||||||
node = BeatmapSetList.get().expand(node.index);
|
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
|
// check beatmapIndex bounds
|
||||||
@@ -1227,7 +1331,7 @@ public class SongMenu extends BasicGameState {
|
|||||||
beatmapIndex = (int) (Math.random() * length);
|
beatmapIndex = (int) (Math.random() * length);
|
||||||
|
|
||||||
focusNode = BeatmapSetList.get().getNode(node, beatmapIndex);
|
focusNode = BeatmapSetList.get().getNode(node, beatmapIndex);
|
||||||
Beatmap beatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
|
Beatmap beatmap = focusNode.getSelectedBeatmap();
|
||||||
MusicController.play(beatmap, false, preview);
|
MusicController.play(beatmap, false, preview);
|
||||||
|
|
||||||
// load scores
|
// load scores
|
||||||
@@ -1286,6 +1390,14 @@ public class SongMenu extends BasicGameState {
|
|||||||
songScrolling.scrollToPosition((focusNode.index + focusNode.getBeatmapSet().size() ) * buttonOffset - (footerY - headerY));
|
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;
|
return oldFocus;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1346,7 +1458,7 @@ public class SongMenu extends BasicGameState {
|
|||||||
if (scoreMap == null || scoreMap.isEmpty() || node.beatmapIndex == -1) // node not expanded
|
if (scoreMap == null || scoreMap.isEmpty() || node.beatmapIndex == -1) // node not expanded
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
Beatmap beatmap = node.getBeatmapSet().get(node.beatmapIndex);
|
Beatmap beatmap = node.getSelectedBeatmap();
|
||||||
ScoreData[] scores = scoreMap.get(beatmap.version);
|
ScoreData[] scores = scoreMap.get(beatmap.version);
|
||||||
if (scores == null || scores.length < 1) // no scores
|
if (scores == null || scores.length < 1) // no scores
|
||||||
return null;
|
return null;
|
||||||
@@ -1364,6 +1476,86 @@ public class SongMenu extends BasicGameState {
|
|||||||
return null; // incorrect map
|
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.
|
* Starts the game.
|
||||||
*/
|
*/
|
||||||
@@ -1372,8 +1564,12 @@ public class SongMenu extends BasicGameState {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
SoundController.playSound(SoundEffect.MENUHIT);
|
SoundController.playSound(SoundEffect.MENUHIT);
|
||||||
MultiClip.destroyExtraClips();
|
|
||||||
Beatmap beatmap = MusicController.getBeatmap();
|
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);
|
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
|
||||||
gameState.loadBeatmap(beatmap);
|
gameState.loadBeatmap(beatmap);
|
||||||
gameState.setRestart(Game.Restart.NEW);
|
gameState.setRestart(Game.Restart.NEW);
|
||||||
|
|||||||
@@ -21,21 +21,23 @@ package itdelatrisu.opsu.states;
|
|||||||
import itdelatrisu.opsu.GameImage;
|
import itdelatrisu.opsu.GameImage;
|
||||||
import itdelatrisu.opsu.Opsu;
|
import itdelatrisu.opsu.Opsu;
|
||||||
import itdelatrisu.opsu.Options;
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.OszUnpacker;
|
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.audio.MusicController;
|
import itdelatrisu.opsu.audio.MusicController;
|
||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||||
|
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
|
||||||
|
import itdelatrisu.opsu.beatmap.OszUnpacker;
|
||||||
import itdelatrisu.opsu.replay.ReplayImporter;
|
import itdelatrisu.opsu.replay.ReplayImporter;
|
||||||
import itdelatrisu.opsu.ui.UI;
|
import itdelatrisu.opsu.ui.UI;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
|
||||||
import org.newdawn.slick.Color;
|
import org.newdawn.slick.Color;
|
||||||
import org.newdawn.slick.GameContainer;
|
import org.newdawn.slick.GameContainer;
|
||||||
import org.newdawn.slick.Graphics;
|
import org.newdawn.slick.Graphics;
|
||||||
import org.newdawn.slick.Image;
|
|
||||||
import org.newdawn.slick.Input;
|
import org.newdawn.slick.Input;
|
||||||
import org.newdawn.slick.SlickException;
|
import org.newdawn.slick.SlickException;
|
||||||
import org.newdawn.slick.state.BasicGameState;
|
import org.newdawn.slick.state.BasicGameState;
|
||||||
@@ -47,6 +49,9 @@ import org.newdawn.slick.state.StateBasedGame;
|
|||||||
* Loads game resources and enters "Main Menu" state.
|
* Loads game resources and enters "Main Menu" state.
|
||||||
*/
|
*/
|
||||||
public class Splash extends BasicGameState {
|
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. */
|
/** Whether or not loading has completed. */
|
||||||
private boolean finished = false;
|
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). */
|
/** Whether the skin being loaded is a new skin (for program restarts). */
|
||||||
private boolean newSkin = false;
|
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
|
// game-related variables
|
||||||
private int state;
|
private final int state;
|
||||||
private GameContainer container;
|
private GameContainer container;
|
||||||
private boolean init = false;
|
private boolean init = false;
|
||||||
|
|
||||||
@@ -77,9 +88,14 @@ public class Splash extends BasicGameState {
|
|||||||
if (Options.getSkin() != null)
|
if (Options.getSkin() != null)
|
||||||
this.newSkin = (Options.getSkin().getDirectory() != Options.getSkinDir());
|
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)
|
// load Utils class first (needed in other 'init' methods)
|
||||||
Utils.init(container, game);
|
Utils.init(container, game);
|
||||||
|
|
||||||
|
// fade in logo
|
||||||
|
this.logoAlpha = new AnimatedValue(MIN_SPLASH_TIME, 0f, 1f, AnimationEquation.LINEAR);
|
||||||
GameImage.MENU_LOGO.getImage().setAlpha(0f);
|
GameImage.MENU_LOGO.getImage().setAlpha(0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,12 +115,17 @@ public class Splash extends BasicGameState {
|
|||||||
|
|
||||||
// resources already loaded (from application restart)
|
// resources already loaded (from application restart)
|
||||||
if (BeatmapSetList.get() != null) {
|
if (BeatmapSetList.get() != null) {
|
||||||
// reload sounds if skin changed
|
if (newSkin || watchServiceChange) { // need to reload resources
|
||||||
if (newSkin) {
|
|
||||||
thread = new Thread() {
|
thread = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
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?
|
// TODO: only reload each sound if actually needed?
|
||||||
|
if (newSkin)
|
||||||
SoundController.init();
|
SoundController.init();
|
||||||
|
|
||||||
finished = true;
|
finished = true;
|
||||||
@@ -144,13 +165,11 @@ public class Splash extends BasicGameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fade in logo
|
// fade in logo
|
||||||
Image logo = GameImage.MENU_LOGO.getImage();
|
if (logoAlpha.update(delta))
|
||||||
float alpha = logo.getAlpha();
|
GameImage.MENU_LOGO.getImage().setAlpha(logoAlpha.getValue());
|
||||||
if (alpha < 1f)
|
|
||||||
logo.setAlpha(alpha + (delta / 500f));
|
|
||||||
|
|
||||||
// change states when loading complete
|
// change states when loading complete
|
||||||
if (finished && alpha >= 1f) {
|
if (finished && logoAlpha.getValue() >= 1f) {
|
||||||
// initialize song list
|
// initialize song list
|
||||||
if (BeatmapSetList.get().size() > 0) {
|
if (BeatmapSetList.get().size() > 0) {
|
||||||
BeatmapSetList.get().init();
|
BeatmapSetList.get().init();
|
||||||
|
|||||||
51
src/itdelatrisu/opsu/ui/Colors.java
Normal 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() {}
|
||||||
|
}
|
||||||
@@ -24,9 +24,10 @@ import itdelatrisu.opsu.Opsu;
|
|||||||
import itdelatrisu.opsu.Options;
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.skins.Skin;
|
import itdelatrisu.opsu.skins.Skin;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||||
|
|
||||||
|
import java.awt.Point;
|
||||||
import java.nio.IntBuffer;
|
import java.nio.IntBuffer;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
|
||||||
import org.lwjgl.BufferUtils;
|
import org.lwjgl.BufferUtils;
|
||||||
@@ -45,13 +46,25 @@ public class Cursor {
|
|||||||
private static org.lwjgl.input.Cursor emptyCursor;
|
private static org.lwjgl.input.Cursor emptyCursor;
|
||||||
|
|
||||||
/** Last cursor coordinates. */
|
/** Last cursor coordinates. */
|
||||||
private int lastX = -1, lastY = -1;
|
private Point lastPosition;
|
||||||
|
|
||||||
/** Cursor rotation angle. */
|
/** Cursor rotation angle. */
|
||||||
private float cursorAngle = 0f;
|
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. */
|
/** Stores all previous cursor locations to display a trail. */
|
||||||
private LinkedList<Integer> cursorX, cursorY;
|
private LinkedList<Point> trail = new LinkedList<Point>();
|
||||||
|
|
||||||
// game-related variables
|
// game-related variables
|
||||||
private static GameContainer container;
|
private static GameContainer container;
|
||||||
@@ -81,10 +94,7 @@ public class Cursor {
|
|||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
*/
|
*/
|
||||||
public Cursor() {
|
public Cursor() {}
|
||||||
cursorX = new LinkedList<Integer>();
|
|
||||||
cursorY = new LinkedList<Integer>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the cursor.
|
* Draws the cursor.
|
||||||
@@ -105,85 +115,90 @@ public class Cursor {
|
|||||||
* @param mousePressed whether or not the mouse button is pressed
|
* @param mousePressed whether or not the mouse button is pressed
|
||||||
*/
|
*/
|
||||||
public void draw(int mouseX, int mouseY, boolean mousePressed) {
|
public void draw(int mouseX, int mouseY, boolean mousePressed) {
|
||||||
|
if (Options.isCursorDisabled())
|
||||||
|
return;
|
||||||
|
|
||||||
// determine correct cursor image
|
// determine correct cursor image
|
||||||
Image cursor = null, cursorMiddle = null, cursorTrail = null;
|
Image cursor = null, cursorMiddle = null, cursorTrail = null;
|
||||||
boolean skinned = GameImage.CURSOR.hasSkinImage();
|
boolean beatmapSkinned = GameImage.CURSOR.hasBeatmapSkinImage();
|
||||||
boolean newStyle, hasMiddle;
|
boolean newStyle, hasMiddle;
|
||||||
if (skinned) {
|
Skin skin = Options.getSkin();
|
||||||
|
if (beatmapSkinned) {
|
||||||
newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors
|
newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors
|
||||||
hasMiddle = GameImage.CURSOR_MIDDLE.hasSkinImage();
|
hasMiddle = GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage();
|
||||||
} else
|
} else
|
||||||
newStyle = hasMiddle = Options.isNewCursorEnabled();
|
newStyle = hasMiddle = Options.isNewCursorEnabled();
|
||||||
if (skinned || newStyle) {
|
if (newStyle || beatmapSkinned) {
|
||||||
cursor = GameImage.CURSOR.getImage();
|
cursor = GameImage.CURSOR.getImage();
|
||||||
cursorTrail = GameImage.CURSOR_TRAIL.getImage();
|
cursorTrail = GameImage.CURSOR_TRAIL.getImage();
|
||||||
} else {
|
} else {
|
||||||
cursor = GameImage.CURSOR_OLD.getImage();
|
cursor = GameImage.CURSOR.hasGameSkinImage() ? GameImage.CURSOR.getImage() : GameImage.CURSOR_OLD.getImage();
|
||||||
cursorTrail = GameImage.CURSOR_TRAIL_OLD.getImage();
|
cursorTrail = GameImage.CURSOR_TRAIL.hasGameSkinImage() ? GameImage.CURSOR_TRAIL.getImage() : GameImage.CURSOR_TRAIL_OLD.getImage();
|
||||||
}
|
}
|
||||||
if (hasMiddle)
|
if (hasMiddle)
|
||||||
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
|
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
|
||||||
|
|
||||||
int removeCount = 0;
|
|
||||||
int FPSmod = (Options.getTargetFPS() / 60);
|
|
||||||
Skin skin = Options.getSkin();
|
|
||||||
|
|
||||||
// scale cursor
|
// scale cursor
|
||||||
float cursorScale = Options.getCursorScale();
|
float cursorScaleAnimated = 1f;
|
||||||
if (mousePressed && skin.isCursorExpanded())
|
if (skin.isCursorExpanded()) {
|
||||||
cursorScale *= 1.25f; // increase the cursor size if pressed
|
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) {
|
if (cursorScale != 1f) {
|
||||||
cursor = cursor.getScaledCopy(cursorScale);
|
cursor = cursor.getScaledCopy(cursorScale);
|
||||||
cursorTrail = cursorTrail.getScaledCopy(cursorScale);
|
cursorTrail = cursorTrail.getScaledCopy(cursorScale);
|
||||||
if (hasMiddle)
|
|
||||||
cursorMiddle = cursorMiddle.getScaledCopy(cursorScale);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: use an image buffer
|
// TODO: use an image buffer
|
||||||
|
int removeCount = 0;
|
||||||
|
float FPSmod = Math.max(container.getFPS(), 1) / 60f;
|
||||||
if (newStyle) {
|
if (newStyle) {
|
||||||
// new style: add all points between cursor movements
|
// new style: add all points between cursor movements
|
||||||
if (lastX < 0) {
|
if (lastPosition == null) {
|
||||||
lastX = mouseX;
|
lastPosition = new Point(mouseX, mouseY);
|
||||||
lastY = mouseY;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addCursorPoints(lastX, lastY, mouseX, mouseY);
|
addCursorPoints(lastPosition.x, lastPosition.y, mouseX, mouseY);
|
||||||
lastX = mouseX;
|
lastPosition.move(mouseX, mouseY);
|
||||||
lastY = mouseY;
|
|
||||||
|
|
||||||
removeCount = (cursorX.size() / (6 * FPSmod)) + 1;
|
removeCount = (int) (trail.size() / (6 * FPSmod)) + 1;
|
||||||
} else {
|
} else {
|
||||||
// old style: sample one point at a time
|
// old style: sample one point at a time
|
||||||
cursorX.add(mouseX);
|
trail.add(new Point(mouseX, mouseY));
|
||||||
cursorY.add(mouseY);
|
|
||||||
|
|
||||||
int max = 10 * FPSmod;
|
int max = (int) (10 * FPSmod);
|
||||||
if (cursorX.size() > max)
|
if (trail.size() > max)
|
||||||
removeCount = cursorX.size() - max;
|
removeCount = trail.size() - max;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove points from the lists
|
// remove points from the lists
|
||||||
for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) {
|
for (int i = 0; i < removeCount && !trail.isEmpty(); i++)
|
||||||
cursorX.remove();
|
trail.remove();
|
||||||
cursorY.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw a fading trail
|
// draw a fading trail
|
||||||
float alpha = 0f;
|
float alpha = 0f;
|
||||||
float t = 2f / cursorX.size();
|
float t = 2f / trail.size();
|
||||||
if (skin.isCursorTrailRotated())
|
int cursorTrailWidth = cursorTrail.getWidth(), cursorTrailHeight = cursorTrail.getHeight();
|
||||||
cursorTrail.setRotation(cursorAngle);
|
float cursorTrailRotation = (skin.isCursorTrailRotated()) ? cursorAngle : 0;
|
||||||
Iterator<Integer> iterX = cursorX.iterator();
|
cursorTrail.startUse();
|
||||||
Iterator<Integer> iterY = cursorY.iterator();
|
for (Point p : trail) {
|
||||||
while (iterX.hasNext()) {
|
|
||||||
int cx = iterX.next();
|
|
||||||
int cy = iterY.next();
|
|
||||||
alpha += t;
|
alpha += t;
|
||||||
cursorTrail.setAlpha(alpha);
|
cursorTrail.setImageColor(1f, 1f, 1f, alpha);
|
||||||
// if (cx != x || cy != y)
|
cursorTrail.drawEmbedded(
|
||||||
cursorTrail.drawCentered(cx, cy);
|
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
|
// draw the other components
|
||||||
if (newStyle && skin.isCursorRotated())
|
if (newStyle && skin.isCursorRotated())
|
||||||
@@ -212,8 +227,7 @@ public class Cursor {
|
|||||||
if (dy <= dx) {
|
if (dy <= dx) {
|
||||||
for (int i = 0; ; i++) {
|
for (int i = 0; ; i++) {
|
||||||
if (i == k) {
|
if (i == k) {
|
||||||
cursorX.add(x1);
|
trail.add(new Point(x1, y1));
|
||||||
cursorY.add(y1);
|
|
||||||
i = 0;
|
i = 0;
|
||||||
}
|
}
|
||||||
if (x1 == x2)
|
if (x1 == x2)
|
||||||
@@ -228,8 +242,7 @@ public class Cursor {
|
|||||||
} else {
|
} else {
|
||||||
for (int i = 0; ; i++) {
|
for (int i = 0; ; i++) {
|
||||||
if (i == k) {
|
if (i == k) {
|
||||||
cursorX.add(x1);
|
trail.add(new Point(x1, y1));
|
||||||
cursorY.add(y1);
|
|
||||||
i = 0;
|
i = 0;
|
||||||
}
|
}
|
||||||
if (y1 == y2)
|
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() {
|
public void reset() {
|
||||||
// destroy skin images
|
// destroy skin images
|
||||||
GameImage.CURSOR.destroySkinImage();
|
GameImage.CURSOR.destroyBeatmapSkinImage();
|
||||||
GameImage.CURSOR_MIDDLE.destroySkinImage();
|
GameImage.CURSOR_MIDDLE.destroyBeatmapSkinImage();
|
||||||
GameImage.CURSOR_TRAIL.destroySkinImage();
|
GameImage.CURSOR_TRAIL.destroyBeatmapSkinImage();
|
||||||
|
|
||||||
// reset locations
|
// reset locations
|
||||||
resetLocations();
|
resetLocations();
|
||||||
@@ -276,18 +289,17 @@ public class Cursor {
|
|||||||
* Resets all cursor location data.
|
* Resets all cursor location data.
|
||||||
*/
|
*/
|
||||||
public void resetLocations() {
|
public void resetLocations() {
|
||||||
lastX = lastY = -1;
|
lastPosition = null;
|
||||||
cursorX.clear();
|
trail.clear();
|
||||||
cursorY.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether or not the cursor is skinned.
|
* Returns whether or not the cursor is skinned.
|
||||||
*/
|
*/
|
||||||
public boolean isSkinned() {
|
public boolean isBeatmapSkinned() {
|
||||||
return (GameImage.CURSOR.hasSkinImage() ||
|
return (GameImage.CURSOR.hasBeatmapSkinImage() ||
|
||||||
GameImage.CURSOR_MIDDLE.hasSkinImage() ||
|
GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage() ||
|
||||||
GameImage.CURSOR_TRAIL.hasSkinImage());
|
GameImage.CURSOR_TRAIL.hasBeatmapSkinImage());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
424
src/itdelatrisu/opsu/ui/DropdownMenu.java
Normal 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; }
|
||||||
|
}
|
||||||
156
src/itdelatrisu/opsu/ui/Fonts.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
package itdelatrisu.opsu.ui;
|
package itdelatrisu.opsu.ui;
|
||||||
|
|
||||||
import itdelatrisu.opsu.Utils;
|
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.Animation;
|
||||||
import org.newdawn.slick.Color;
|
import org.newdawn.slick.Color;
|
||||||
@@ -63,11 +65,26 @@ public class MenuButton {
|
|||||||
/** The hover actions for this button. */
|
/** The hover actions for this button. */
|
||||||
private int hoverEffect = 0;
|
private int hoverEffect = 0;
|
||||||
|
|
||||||
/** The current and max scale of the button. */
|
/** The hover animation duration, in milliseconds. */
|
||||||
private float scale = 1f, hoverScale = 1.25f;
|
private int animationDuration = 100;
|
||||||
|
|
||||||
/** The current and base alpha level of the button. */
|
/** The hover animation equation. */
|
||||||
private float alpha = 1f, baseAlpha = 0.75f;
|
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. */
|
/** The scaled expansion direction for the button. */
|
||||||
private Expand dir = Expand.CENTER;
|
private Expand dir = Expand.CENTER;
|
||||||
@@ -75,8 +92,11 @@ public class MenuButton {
|
|||||||
/** Scaled expansion directions. */
|
/** Scaled expansion directions. */
|
||||||
public enum Expand { CENTER, UP, RIGHT, LEFT, DOWN, UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT; }
|
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. */
|
/** The rotation angle of the button. */
|
||||||
private float angle = 0f, maxAngle = 30f;
|
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.
|
* Creates a new button from an Image.
|
||||||
@@ -126,11 +146,13 @@ public class MenuButton {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a new center x coordinate.
|
* Sets a new center x coordinate.
|
||||||
|
* @param x the x coordinate
|
||||||
*/
|
*/
|
||||||
public void setX(float x) { this.x = x; }
|
public void setX(float x) { this.x = x; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets a new center y coordinate.
|
* Sets a new center y coordinate.
|
||||||
|
* @param y the y coordinate
|
||||||
*/
|
*/
|
||||||
public void setY(float y) { this.y = y; }
|
public void setY(float y) { this.y = y; }
|
||||||
|
|
||||||
@@ -192,15 +214,15 @@ public class MenuButton {
|
|||||||
float oldAlpha = image.getAlpha();
|
float oldAlpha = image.getAlpha();
|
||||||
float oldAngle = image.getRotation();
|
float oldAngle = image.getRotation();
|
||||||
if ((hoverEffect & EFFECT_EXPAND) > 0) {
|
if ((hoverEffect & EFFECT_EXPAND) > 0) {
|
||||||
if (scale != 1f) {
|
if (scale.getValue() != 1f) {
|
||||||
image = image.getScaledCopy(scale);
|
image = image.getScaledCopy(scale.getValue());
|
||||||
image.setAlpha(oldAlpha);
|
image.setAlpha(oldAlpha);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((hoverEffect & EFFECT_FADE) > 0)
|
if ((hoverEffect & EFFECT_FADE) > 0)
|
||||||
image.setAlpha(alpha);
|
image.setAlpha(alpha.getValue());
|
||||||
if ((hoverEffect & EFFECT_ROTATE) > 0)
|
if ((hoverEffect & EFFECT_ROTATE) > 0)
|
||||||
image.setRotation(angle);
|
image.setRotation(angle.getValue());
|
||||||
image.draw(x - xRadius, y - yRadius, filter);
|
image.draw(x - xRadius, y - yRadius, filter);
|
||||||
if (image == this.img) {
|
if (image == this.img) {
|
||||||
image.setAlpha(oldAlpha);
|
image.setAlpha(oldAlpha);
|
||||||
@@ -217,9 +239,10 @@ public class MenuButton {
|
|||||||
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
|
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
|
||||||
} else if ((hoverEffect & EFFECT_FADE) > 0) {
|
} else if ((hoverEffect & EFFECT_FADE) > 0) {
|
||||||
float a = image.getAlpha(), aL = imgL.getAlpha(), aR = imgR.getAlpha();
|
float a = image.getAlpha(), aL = imgL.getAlpha(), aR = imgR.getAlpha();
|
||||||
image.setAlpha(alpha);
|
float currentAlpha = alpha.getValue();
|
||||||
imgL.setAlpha(alpha);
|
image.setAlpha(currentAlpha);
|
||||||
imgR.setAlpha(alpha);
|
imgL.setAlpha(currentAlpha);
|
||||||
|
imgR.setAlpha(currentAlpha);
|
||||||
image.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter);
|
image.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter);
|
||||||
imgL.draw(x - xRadius, y - yRadius, filter);
|
imgL.draw(x - xRadius, y - yRadius, filter);
|
||||||
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
|
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
|
||||||
@@ -267,28 +290,63 @@ public class MenuButton {
|
|||||||
*/
|
*/
|
||||||
public void resetHover() {
|
public void resetHover() {
|
||||||
if ((hoverEffect & EFFECT_EXPAND) > 0) {
|
if ((hoverEffect & EFFECT_EXPAND) > 0) {
|
||||||
this.scale = 1f;
|
scale.setTime(0);
|
||||||
setHoverRadius();
|
setHoverRadius();
|
||||||
}
|
}
|
||||||
if ((hoverEffect & EFFECT_FADE) > 0)
|
if ((hoverEffect & EFFECT_FADE) > 0)
|
||||||
this.alpha = baseAlpha;
|
alpha.setTime(0);
|
||||||
if ((hoverEffect & EFFECT_ROTATE) > 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.
|
* 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.
|
* 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.
|
* 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); }
|
public void setHoverExpand(float scale) { setHoverExpand(scale, this.dir); }
|
||||||
|
|
||||||
@@ -296,45 +354,45 @@ public class MenuButton {
|
|||||||
* Sets the "expand" hover effect.
|
* Sets the "expand" hover effect.
|
||||||
* @param dir the expansion direction
|
* @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.
|
* 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
|
* @param dir the expansion direction
|
||||||
*/
|
*/
|
||||||
public void setHoverExpand(float scale, Expand dir) {
|
public void setHoverExpand(float scale, Expand dir) {
|
||||||
hoverEffect |= EFFECT_EXPAND;
|
hoverEffect |= EFFECT_EXPAND;
|
||||||
this.hoverScale = scale;
|
this.scale = new AnimatedValue(animationDuration, 1f, scale, animationEqn);
|
||||||
this.dir = dir;
|
this.dir = dir;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the "fade" hover effect.
|
* Sets the "fade" hover effect.
|
||||||
*/
|
*/
|
||||||
public void setHoverFade() { hoverEffect |= EFFECT_FADE; }
|
public void setHoverFade() { setHoverFade(DEFAULT_ALPHA_BASE); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the "fade" hover effect.
|
* 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) {
|
public void setHoverFade(float baseAlpha) {
|
||||||
hoverEffect |= EFFECT_FADE;
|
hoverEffect |= EFFECT_FADE;
|
||||||
this.baseAlpha = baseAlpha;
|
this.alpha = new AnimatedValue(animationDuration, baseAlpha, 1f, animationEqn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the "rotate" hover effect.
|
* Sets the "rotate" hover effect.
|
||||||
*/
|
*/
|
||||||
public void setHoverRotate() { hoverEffect |= EFFECT_ROTATE; }
|
public void setHoverRotate() { setHoverRotate(DEFAULT_ANGLE_MAX); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the "rotate" hover effect.
|
* 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) {
|
public void setHoverRotate(float maxAngle) {
|
||||||
hoverEffect |= EFFECT_ROTATE;
|
hoverEffect |= EFFECT_ROTATE;
|
||||||
this.maxAngle = maxAngle;
|
this.angle = new AnimatedValue(animationDuration, 0f, maxAngle, animationEqn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -371,45 +429,53 @@ public class MenuButton {
|
|||||||
if (hoverEffect == 0)
|
if (hoverEffect == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
int d = delta * (isHover ? 1 : -1);
|
||||||
|
|
||||||
// scale the button
|
// scale the button
|
||||||
if ((hoverEffect & EFFECT_EXPAND) > 0) {
|
if ((hoverEffect & EFFECT_EXPAND) > 0) {
|
||||||
int sign = 0;
|
if (scale.update(d))
|
||||||
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);
|
|
||||||
setHoverRadius();
|
setHoverRadius();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// fade the button
|
// fade the button
|
||||||
if ((hoverEffect & EFFECT_FADE) > 0) {
|
if ((hoverEffect & EFFECT_FADE) > 0)
|
||||||
int sign = 0;
|
alpha.update(d);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// rotate the button
|
// rotate the button
|
||||||
if ((hoverEffect & EFFECT_ROTATE) > 0) {
|
if ((hoverEffect & EFFECT_ROTATE) > 0)
|
||||||
int sign = 0;
|
angle.update(d);
|
||||||
boolean right = (maxAngle > 0);
|
}
|
||||||
if (isHover && angle != maxAngle)
|
|
||||||
sign = (right) ? 1 : -1;
|
/**
|
||||||
else if (!isHover && angle != 0)
|
* Automatically advances the hover animation in a loop.
|
||||||
sign = (right) ? -1 : 1;
|
* @param delta the delta interval
|
||||||
if (sign != 0) {
|
* @param reverseAtEnd whether to reverse or restart the animation upon reaching the end
|
||||||
float diff = sign * Math.abs(maxAngle) * delta / 125f;
|
*/
|
||||||
angle = (right) ?
|
public void autoHoverUpdate(int delta, boolean reverseAtEnd) {
|
||||||
Utils.getBoundedValue(angle, diff, 0, maxAngle) :
|
if (hoverEffect == 0)
|
||||||
Utils.getBoundedValue(angle, diff, maxAngle, 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();
|
image = anim.getCurrentFrame();
|
||||||
|
|
||||||
int xOffset = 0, yOffset = 0;
|
int xOffset = 0, yOffset = 0;
|
||||||
|
float currentScale = scale.getValue();
|
||||||
if (dir != Expand.CENTER) {
|
if (dir != Expand.CENTER) {
|
||||||
// offset by difference between normal/scaled image dimensions
|
// offset by difference between normal/scaled image dimensions
|
||||||
xOffset = (int) ((scale - 1f) * image.getWidth());
|
xOffset = (int) ((currentScale - 1f) * image.getWidth());
|
||||||
yOffset = (int) ((scale - 1f) * image.getHeight());
|
yOffset = (int) ((currentScale - 1f) * image.getHeight());
|
||||||
if (dir == Expand.UP || dir == Expand.DOWN)
|
if (dir == Expand.UP || dir == Expand.DOWN)
|
||||||
xOffset = 0; // no horizontal offset
|
xOffset = 0; // no horizontal offset
|
||||||
if (dir == Expand.RIGHT || dir == Expand.LEFT)
|
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)
|
if (dir == Expand.DOWN || dir == Expand.DOWN_LEFT || dir == Expand.DOWN_RIGHT)
|
||||||
yOffset *= -1; // flip y for down
|
yOffset *= -1; // flip y for down
|
||||||
}
|
}
|
||||||
this.xRadius = ((image.getWidth() * scale) + xOffset) / 2f;
|
this.xRadius = ((image.getWidth() * currentScale) + xOffset) / 2f;
|
||||||
this.yRadius = ((image.getHeight() * scale) + yOffset) / 2f;
|
this.yRadius = ((image.getHeight() * currentScale) + yOffset) / 2f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
164
src/itdelatrisu/opsu/ui/StarStream.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,11 +21,13 @@ package itdelatrisu.opsu.ui;
|
|||||||
import itdelatrisu.opsu.ErrorHandler;
|
import itdelatrisu.opsu.ErrorHandler;
|
||||||
import itdelatrisu.opsu.GameImage;
|
import itdelatrisu.opsu.GameImage;
|
||||||
import itdelatrisu.opsu.Options;
|
import itdelatrisu.opsu.Options;
|
||||||
import itdelatrisu.opsu.OszUnpacker;
|
|
||||||
import itdelatrisu.opsu.Utils;
|
import itdelatrisu.opsu.Utils;
|
||||||
import itdelatrisu.opsu.audio.SoundController;
|
import itdelatrisu.opsu.audio.SoundController;
|
||||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||||
|
import itdelatrisu.opsu.beatmap.OszUnpacker;
|
||||||
import itdelatrisu.opsu.replay.ReplayImporter;
|
import itdelatrisu.opsu.replay.ReplayImporter;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||||
|
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||||
|
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.UIManager;
|
import javax.swing.UIManager;
|
||||||
@@ -36,7 +38,6 @@ import org.newdawn.slick.GameContainer;
|
|||||||
import org.newdawn.slick.Graphics;
|
import org.newdawn.slick.Graphics;
|
||||||
import org.newdawn.slick.Image;
|
import org.newdawn.slick.Image;
|
||||||
import org.newdawn.slick.Input;
|
import org.newdawn.slick.Input;
|
||||||
import org.newdawn.slick.SlickException;
|
|
||||||
import org.newdawn.slick.state.StateBasedGame;
|
import org.newdawn.slick.state.StateBasedGame;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,7 +63,7 @@ public class UI {
|
|||||||
private static int barNotifTimer = -1;
|
private static int barNotifTimer = -1;
|
||||||
|
|
||||||
/** Duration, in milliseconds, to display bar notifications. */
|
/** 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. */
|
/** The current tooltip. */
|
||||||
private static String tooltip;
|
private static String tooltip;
|
||||||
@@ -70,11 +71,8 @@ public class UI {
|
|||||||
/** Whether or not to check the current tooltip for line breaks. */
|
/** Whether or not to check the current tooltip for line breaks. */
|
||||||
private static boolean tooltipNewlines;
|
private static boolean tooltipNewlines;
|
||||||
|
|
||||||
/** The current tooltip timer. */
|
/** The alpha level of the current tooltip (if any). */
|
||||||
private static int tooltipTimer = -1;
|
private static AnimatedValue tooltipAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);
|
||||||
|
|
||||||
/** Duration, in milliseconds, to fade tooltips. */
|
|
||||||
private static final int TOOLTIP_FADE_TIME = 200;
|
|
||||||
|
|
||||||
// game-related variables
|
// game-related variables
|
||||||
private static GameContainer container;
|
private static GameContainer container;
|
||||||
@@ -87,10 +85,8 @@ public class UI {
|
|||||||
* Initializes UI data.
|
* Initializes UI data.
|
||||||
* @param container the game container
|
* @param container the game container
|
||||||
* @param game the game object
|
* @param game the game object
|
||||||
* @throws SlickException
|
|
||||||
*/
|
*/
|
||||||
public static void init(GameContainer container, StateBasedGame game)
|
public static void init(GameContainer container, StateBasedGame game) {
|
||||||
throws SlickException {
|
|
||||||
UI.container = container;
|
UI.container = container;
|
||||||
UI.input = container.getInput();
|
UI.input = container.getInput();
|
||||||
|
|
||||||
@@ -106,6 +102,8 @@ public class UI {
|
|||||||
Image back = GameImage.MENU_BACK.getImage();
|
Image back = GameImage.MENU_BACK.getImage();
|
||||||
backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f));
|
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);
|
backButton.setHoverExpand(MenuButton.Expand.UP_RIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,12 +115,11 @@ public class UI {
|
|||||||
cursor.update(delta);
|
cursor.update(delta);
|
||||||
updateVolumeDisplay(delta);
|
updateVolumeDisplay(delta);
|
||||||
updateBarNotification(delta);
|
updateBarNotification(delta);
|
||||||
if (tooltipTimer > 0)
|
tooltipAlpha.update(-delta);
|
||||||
tooltipTimer -= 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
|
* @param g the graphics context
|
||||||
*/
|
*/
|
||||||
public static void draw(Graphics g) {
|
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 g the graphics context
|
||||||
* @param mouseX the mouse x coordinate
|
* @param mouseX the mouse x coordinate
|
||||||
* @param mouseY the mouse y 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) {
|
public static void drawTab(float x, float y, String text, boolean selected, boolean isHover) {
|
||||||
Image tabImage = GameImage.MENU_TAB.getImage();
|
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);
|
float tabTextY = y - (tabImage.getHeight() / 2);
|
||||||
Color filter, textColor;
|
Color filter, textColor;
|
||||||
if (selected) {
|
if (selected) {
|
||||||
filter = Color.white;
|
filter = Color.white;
|
||||||
textColor = Color.black;
|
textColor = Color.black;
|
||||||
} else {
|
} else {
|
||||||
filter = (isHover) ? Utils.COLOR_RED_HOVER : Color.red;
|
filter = (isHover) ? Colors.RED_HOVER : Color.red;
|
||||||
textColor = Color.white;
|
textColor = Color.white;
|
||||||
}
|
}
|
||||||
tabImage.drawCentered(x, y, filter);
|
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;
|
return;
|
||||||
|
|
||||||
String fps = String.format("%dFPS", container.getFPS());
|
String fps = String.format("%dFPS", container.getFPS());
|
||||||
Utils.FONT_BOLD.drawString(
|
Fonts.BOLD.drawString(
|
||||||
container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth(fps),
|
container.getWidth() * 0.997f - Fonts.BOLD.getWidth(fps),
|
||||||
container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight(fps),
|
container.getHeight() * 0.997f - Fonts.BOLD.getHeight(fps),
|
||||||
Integer.toString(container.getFPS()), Color.white
|
Integer.toString(container.getFPS()), Color.white
|
||||||
);
|
);
|
||||||
Utils.FONT_DEFAULT.drawString(
|
Fonts.DEFAULT.drawString(
|
||||||
container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth("FPS"),
|
container.getWidth() * 0.997f - Fonts.BOLD.getWidth("FPS"),
|
||||||
container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight("FPS"),
|
container.getHeight() * 0.997f - Fonts.BOLD.getHeight("FPS"),
|
||||||
"FPS", Color.white
|
"FPS", Color.white
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -263,7 +260,7 @@ public class UI {
|
|||||||
*/
|
*/
|
||||||
public static void changeVolume(int units) {
|
public static void changeVolume(int units) {
|
||||||
final float UNIT_OFFSET = 0.05f;
|
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)
|
if (volumeDisplay == -1)
|
||||||
volumeDisplay = 0;
|
volumeDisplay = 0;
|
||||||
else if (volumeDisplay >= VOLUME_DISPLAY_TIME / 10)
|
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.
|
* at the bottom of the screen.
|
||||||
|
* @param g the graphics context
|
||||||
*/
|
*/
|
||||||
public static void drawLoadingProgress(Graphics g) {
|
public static void drawLoadingProgress(Graphics g) {
|
||||||
String text, file;
|
String text, file;
|
||||||
@@ -298,16 +296,16 @@ public class UI {
|
|||||||
// draw loading info
|
// draw loading info
|
||||||
float marginX = container.getWidth() * 0.02f, marginY = container.getHeight() * 0.02f;
|
float marginX = container.getWidth() * 0.02f, marginY = container.getHeight() * 0.02f;
|
||||||
float lineY = container.getHeight() - marginY;
|
float lineY = container.getHeight() - marginY;
|
||||||
int lineOffsetY = Utils.FONT_MEDIUM.getLineHeight();
|
int lineOffsetY = Fonts.MEDIUM.getLineHeight();
|
||||||
if (Options.isLoadVerbose()) {
|
if (Options.isLoadVerbose()) {
|
||||||
// verbose: display percentages and file names
|
// verbose: display percentages and file names
|
||||||
Utils.FONT_MEDIUM.drawString(
|
Fonts.MEDIUM.drawString(
|
||||||
marginX, lineY - (lineOffsetY * 2),
|
marginX, lineY - (lineOffsetY * 2),
|
||||||
String.format("%s (%d%%)", text, progress), Color.white);
|
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 {
|
} else {
|
||||||
// draw loading bar
|
// 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.setColor(Color.white);
|
||||||
g.fillRoundRect(marginX, lineY - (lineOffsetY / 2f),
|
g.fillRoundRect(marginX, lineY - (lineOffsetY / 2f),
|
||||||
(container.getWidth() - (marginX * 2f)) * progress / 100f, lineOffsetY / 4f, 4
|
(container.getWidth() - (marginX * 2f)) * progress / 100f, lineOffsetY / 4f, 4
|
||||||
@@ -357,12 +355,7 @@ public class UI {
|
|||||||
if (s != null) {
|
if (s != null) {
|
||||||
tooltip = s;
|
tooltip = s;
|
||||||
tooltipNewlines = newlines;
|
tooltipNewlines = newlines;
|
||||||
if (tooltipTimer <= 0)
|
tooltipAlpha.update(delta * 2);
|
||||||
tooltipTimer = delta;
|
|
||||||
else
|
|
||||||
tooltipTimer += delta * 2;
|
|
||||||
if (tooltipTimer > TOOLTIP_FADE_TIME)
|
|
||||||
tooltipTimer = TOOLTIP_FADE_TIME;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,26 +365,26 @@ public class UI {
|
|||||||
* @param g the graphics context
|
* @param g the graphics context
|
||||||
*/
|
*/
|
||||||
public static void drawTooltip(Graphics g) {
|
public static void drawTooltip(Graphics g) {
|
||||||
if (tooltipTimer <= 0 || tooltip == null)
|
if (tooltipAlpha.getTime() == 0 || tooltip == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int containerWidth = container.getWidth(), containerHeight = container.getHeight();
|
int containerWidth = container.getWidth(), containerHeight = container.getHeight();
|
||||||
int margin = containerWidth / 100, textMarginX = 2;
|
int margin = containerWidth / 100, textMarginX = 2;
|
||||||
int offset = GameImage.CURSOR_MIDDLE.getImage().getWidth() / 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;
|
int textWidth = textMarginX * 2, textHeight = lineHeight;
|
||||||
if (tooltipNewlines) {
|
if (tooltipNewlines) {
|
||||||
String[] lines = tooltip.split("\\n");
|
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++) {
|
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)
|
if (w > maxWidth)
|
||||||
maxWidth = w;
|
maxWidth = w;
|
||||||
}
|
}
|
||||||
textWidth += maxWidth;
|
textWidth += maxWidth;
|
||||||
textHeight += lineHeight * (lines.length - 1);
|
textHeight += lineHeight * (lines.length - 1);
|
||||||
} else
|
} else
|
||||||
textWidth += Utils.FONT_SMALL.getWidth(tooltip);
|
textWidth += Fonts.SMALL.getWidth(tooltip);
|
||||||
|
|
||||||
// get drawing coordinates
|
// get drawing coordinates
|
||||||
int x = input.getMouseX() + offset, y = input.getMouseY() + offset;
|
int x = input.getMouseX() + offset, y = input.getMouseY() + offset;
|
||||||
@@ -405,29 +398,29 @@ public class UI {
|
|||||||
y = margin;
|
y = margin;
|
||||||
|
|
||||||
// draw tooltip text inside a filled rectangle
|
// draw tooltip text inside a filled rectangle
|
||||||
float alpha = (float) tooltipTimer / TOOLTIP_FADE_TIME;
|
float alpha = tooltipAlpha.getValue();
|
||||||
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
|
float oldAlpha = Colors.BLACK_ALPHA.a;
|
||||||
Utils.COLOR_BLACK_ALPHA.a = alpha;
|
Colors.BLACK_ALPHA.a = alpha;
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
|
Colors.BLACK_ALPHA.a = oldAlpha;
|
||||||
g.fillRect(x, y, textWidth, textHeight);
|
g.fillRect(x, y, textWidth, textHeight);
|
||||||
oldAlpha = Utils.COLOR_DARK_GRAY.a;
|
oldAlpha = Colors.DARK_GRAY.a;
|
||||||
Utils.COLOR_DARK_GRAY.a = alpha;
|
Colors.DARK_GRAY.a = alpha;
|
||||||
g.setColor(Utils.COLOR_DARK_GRAY);
|
g.setColor(Colors.DARK_GRAY);
|
||||||
g.setLineWidth(1);
|
g.setLineWidth(1);
|
||||||
g.drawRect(x, y, textWidth, textHeight);
|
g.drawRect(x, y, textWidth, textHeight);
|
||||||
Utils.COLOR_DARK_GRAY.a = oldAlpha;
|
Colors.DARK_GRAY.a = oldAlpha;
|
||||||
oldAlpha = Utils.COLOR_WHITE_ALPHA.a;
|
oldAlpha = Colors.WHITE_ALPHA.a;
|
||||||
Utils.COLOR_WHITE_ALPHA.a = alpha;
|
Colors.WHITE_ALPHA.a = alpha;
|
||||||
Utils.FONT_SMALL.drawString(x + textMarginX, y, tooltip, Utils.COLOR_WHITE_ALPHA);
|
Fonts.SMALL.drawString(x + textMarginX, y, tooltip, Colors.WHITE_ALPHA);
|
||||||
Utils.COLOR_WHITE_ALPHA.a = oldAlpha;
|
Colors.WHITE_ALPHA.a = oldAlpha;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resets the tooltip.
|
* Resets the tooltip.
|
||||||
*/
|
*/
|
||||||
public static void resetTooltip() {
|
public static void resetTooltip() {
|
||||||
tooltipTimer = -1;
|
tooltipAlpha.setTime(0);
|
||||||
tooltip = null;
|
tooltip = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,18 +468,18 @@ public class UI {
|
|||||||
if (barNotifTimer >= BAR_NOTIFICATION_TIME * 0.9f)
|
if (barNotifTimer >= BAR_NOTIFICATION_TIME * 0.9f)
|
||||||
alpha -= 1 - ((BAR_NOTIFICATION_TIME - barNotifTimer) / (BAR_NOTIFICATION_TIME * 0.1f));
|
alpha -= 1 - ((BAR_NOTIFICATION_TIME - barNotifTimer) / (BAR_NOTIFICATION_TIME * 0.1f));
|
||||||
int midX = container.getWidth() / 2, midY = container.getHeight() / 2;
|
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 barHeight = Fonts.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;
|
float oldAlphaB = Colors.BLACK_ALPHA.a, oldAlphaW = Colors.WHITE_ALPHA.a;
|
||||||
Utils.COLOR_BLACK_ALPHA.a *= alpha;
|
Colors.BLACK_ALPHA.a *= alpha;
|
||||||
Utils.COLOR_WHITE_ALPHA.a = alpha;
|
Colors.WHITE_ALPHA.a = alpha;
|
||||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
g.setColor(Colors.BLACK_ALPHA);
|
||||||
g.fillRect(0, midY - barHeight / 2f, container.getWidth(), barHeight);
|
g.fillRect(0, midY - barHeight / 2f, container.getWidth(), barHeight);
|
||||||
Utils.FONT_LARGE.drawString(
|
Fonts.LARGE.drawString(
|
||||||
midX - Utils.FONT_LARGE.getWidth(barNotif) / 2f,
|
midX - Fonts.LARGE.getWidth(barNotif) / 2f,
|
||||||
midY - Utils.FONT_LARGE.getLineHeight() / 2.2f,
|
midY - Fonts.LARGE.getLineHeight() / 2.2f,
|
||||||
barNotif, Utils.COLOR_WHITE_ALPHA);
|
barNotif, Colors.WHITE_ALPHA);
|
||||||
Utils.COLOR_BLACK_ALPHA.a = oldAlphaB;
|
Colors.BLACK_ALPHA.a = oldAlphaB;
|
||||||
Utils.COLOR_WHITE_ALPHA.a = oldAlphaW;
|
Colors.WHITE_ALPHA.a = oldAlphaW;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
134
src/itdelatrisu/opsu/ui/animations/AnimatedValue.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
308
src/itdelatrisu/opsu/ui/animations/AnimationEquation.java
Normal 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);
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ import org.newdawn.slick.opengl.TextureImpl;
|
|||||||
import org.newdawn.slick.opengl.pbuffer.GraphicsFactory;
|
import org.newdawn.slick.opengl.pbuffer.GraphicsFactory;
|
||||||
import org.newdawn.slick.opengl.renderer.Renderer;
|
import org.newdawn.slick.opengl.renderer.Renderer;
|
||||||
import org.newdawn.slick.opengl.renderer.SGL;
|
import org.newdawn.slick.opengl.renderer.SGL;
|
||||||
|
import org.newdawn.slick.util.FastTrig;
|
||||||
import org.newdawn.slick.util.Log;
|
import org.newdawn.slick.util.Log;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,7 +105,7 @@ public class Image implements Renderable {
|
|||||||
/** The colours for each of the corners */
|
/** The colours for each of the corners */
|
||||||
protected Color[] corners;
|
protected Color[] corners;
|
||||||
/** The OpenGL max filter */
|
/** The OpenGL max filter */
|
||||||
private int filter = SGL.GL_LINEAR;
|
private int filter = FILTER_LINEAR;
|
||||||
|
|
||||||
/** True if the image should be flipped vertically */
|
/** True if the image should be flipped vertically */
|
||||||
private boolean flipped;
|
private boolean flipped;
|
||||||
@@ -562,7 +563,7 @@ public class Image implements Renderable {
|
|||||||
* @param y The y coordinate to place the image's center at
|
* @param y The y coordinate to place the image's center at
|
||||||
*/
|
*/
|
||||||
public void drawCentered(float x, float y) {
|
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 y The y location to draw the image at
|
||||||
* @param filter The color to filter with when drawing
|
* @param filter The color to filter with when drawing
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public void draw(float x, float y, Color filter) {
|
public void draw(float x, float y, Color filter) {
|
||||||
init();
|
init();
|
||||||
draw(x,y,width,height, filter);
|
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
|
* Draw this image as part of a collection of images
|
||||||
*
|
*
|
||||||
@@ -719,6 +731,7 @@ public class Image implements Renderable {
|
|||||||
* @param height
|
* @param height
|
||||||
* The height to render the image at
|
* The height to render the image at
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public void draw(float x,float y,float width,float height) {
|
public void draw(float x,float y,float width,float height) {
|
||||||
init();
|
init();
|
||||||
draw(x,y,width,height,Color.white);
|
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 height The height to render the image at
|
||||||
* @param filter The color to filter with while drawing
|
* @param filter The color to filter with while drawing
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
public void draw(float x,float y,float width,float height,Color filter) {
|
public void draw(float x,float y,float width,float height,Color filter) {
|
||||||
if (alpha != 1) {
|
if (alpha != 1) {
|
||||||
if (filter == null) {
|
if (filter == null) {
|
||||||
@@ -1160,6 +1174,83 @@ public class Image implements Renderable {
|
|||||||
GL.glVertex3f((x + mywidth),y, 0.0f);
|
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
|
* Draw the image in a warper rectangle. The effects this can
|
||||||
* have are many and varied, might be interesting though.
|
* have are many and varied, might be interesting though.
|
||||||
@@ -1457,7 +1548,7 @@ public class Image implements Renderable {
|
|||||||
if (isDestroyed()) {
|
if (isDestroyed()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
flushPixelData();
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
texture.release();
|
texture.release();
|
||||||
GraphicsFactory.releaseGraphicsForImage(this);
|
GraphicsFactory.releaseGraphicsForImage(this);
|
||||||
|
|||||||
@@ -1078,7 +1078,7 @@ 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 controller - no jinput found - add jinput.jar to your classpath");
|
||||||
}
|
}
|
||||||
throw new SlickException("Unable to create controllers");
|
throw new SlickException("Unable to create controllers");
|
||||||
} catch (NoClassDefFoundError e) {
|
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
||||||
// forget it, no jinput availble
|
// forget it, no jinput availble
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1233,7 +1233,7 @@ public class Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
while (Mouse.next()) {
|
while (Mouse.next()) {
|
||||||
if (Mouse.getEventButton() >= 0) {
|
if (Mouse.getEventButton() >= 0 && Mouse.getEventButton() < mousePressed.length) {
|
||||||
if (Mouse.getEventButtonState()) {
|
if (Mouse.getEventButtonState()) {
|
||||||
consumed = false;
|
consumed = false;
|
||||||
mousePressed[Mouse.getEventButton()] = true;
|
mousePressed[Mouse.getEventButton()] = true;
|
||||||
|
|||||||
@@ -76,8 +76,9 @@ public class Mp3InputStream extends InputStream implements AudioInputStream {
|
|||||||
/**
|
/**
|
||||||
* Create a new stream to decode MP3 data.
|
* Create a new stream to decode MP3 data.
|
||||||
* @param input the input stream from which to read the MP3 file
|
* @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();
|
decoder = new Decoder();
|
||||||
bitstream = new Bitstream(input);
|
bitstream = new Bitstream(input);
|
||||||
try {
|
try {
|
||||||
@@ -85,6 +86,10 @@ public class Mp3InputStream extends InputStream implements AudioInputStream {
|
|||||||
} catch (BitstreamException e) {
|
} catch (BitstreamException e) {
|
||||||
Log.error(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;
|
channels = (header.mode() == Header.SINGLE_CHANNEL) ? 1 : 2;
|
||||||
sampleRate = header.frequency();
|
sampleRate = header.frequency();
|
||||||
|
|||||||