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
8
.gitignore
vendored
@@ -15,11 +15,17 @@
|
||||
.metadata
|
||||
.classpath
|
||||
.project
|
||||
.externalToolBuilders/
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
|
||||
# Gradle
|
||||
.gradle
|
||||
build/
|
||||
|
||||
Thumbs.db
|
||||
/target
|
||||
/target
|
||||
84
README.md
@@ -1,16 +1,18 @@
|
||||
# [opsu!](http://itdelatrisu.github.io/opsu/)
|
||||
**opsu!** is an unofficial open-source client for [osu!](https://osu.ppy.sh/),
|
||||
a rhythm game based on popular commercial games such as *Ouendan* and
|
||||
*Elite Beat Agents*. It is written in Java using [Slick2D](http://slick.ninjacave.com/)
|
||||
and [LWJGL](http://lwjgl.org/), wrappers around the OpenGL and OpenAL libraries.
|
||||
**opsu!** is an unofficial open-source client for the rhythm game
|
||||
[osu!](https://osu.ppy.sh/). It is written in Java using
|
||||
[Slick2D](http://slick.ninjacave.com/) and [LWJGL](http://lwjgl.org/),
|
||||
wrappers around the OpenGL and OpenAL libraries.
|
||||
|
||||
opsu! runs on Windows, OS X, and Linux platforms. A [libGDX port](https://github.com/fluddokt/opsu)
|
||||
additionally supports Android devices.
|
||||
opsu! runs on Windows, OS X, and Linux platforms.
|
||||
A [libGDX port](https://github.com/fluddokt/opsu) additionally supports Android
|
||||
devices.
|
||||
|
||||
## Getting Started
|
||||
Precompiled binaries for opsu! can be found on the
|
||||
[releases](https://github.com/itdelatrisu/opsu/releases) page, with the latest
|
||||
builds at the top. APK releases can be found [here](https://github.com/fluddokt/opsu/releases).
|
||||
builds at the top. APK releases can be found
|
||||
[here](https://github.com/fluddokt/opsu/releases).
|
||||
|
||||
### Java Setup
|
||||
The Java Runtime Environment (JRE) must be installed in order to run opsu!.
|
||||
@@ -19,34 +21,70 @@ The download page is located [here](https://www.java.com/en/download/).
|
||||
### Beatmaps
|
||||
opsu! requires beatmaps to run, which are available for download on the
|
||||
[osu! website](https://osu.ppy.sh/p/beatmaplist) and mirror sites such as
|
||||
[osu!Mirror](https://osu.yas-online.net/) or [Bloodcat](http://bloodcat.com/osu/).
|
||||
[osu!Mirror](https://osu.yas-online.net/) and [Bloodcat](http://bloodcat.com/osu/).
|
||||
Beatmaps can also be downloaded directly through opsu! in the downloads menu.
|
||||
|
||||
If osu! is already installed, this application will attempt to load songs
|
||||
directly from the osu! program folder. Otherwise, place songs in the generated
|
||||
`Songs` folder or set the `BeatmapDirectory` value in the generated
|
||||
configuration file to the path of the root song directory.
|
||||
If osu! is already installed, this application will attempt to load beatmaps
|
||||
directly from the osu! program folder. Otherwise, place beatmaps in the
|
||||
generated `Songs` folder or set the "BeatmapDirectory" value in the generated
|
||||
configuration file to the path of the root beatmap directory.
|
||||
|
||||
Note that beatmaps are typically delivered as OSZ files. These can be extracted
|
||||
with any ZIP tool, and opsu! will automatically extract them into the songs
|
||||
with any ZIP tool, and opsu! will automatically extract them into the beatmap
|
||||
folder if placed in the `SongPacks` directory.
|
||||
|
||||
### First Run
|
||||
The `Music Offset` value will likely need to be adjusted when playing for the
|
||||
The "Music Offset" value will likely need to be adjusted when playing for the
|
||||
first time, or whenever hit objects are out of sync with the music. This and
|
||||
other game options can be accessed by clicking the "Other Options" button in
|
||||
the song menu.
|
||||
|
||||
## Building
|
||||
opsu! is distributed as a Maven project.
|
||||
### Directory Structure
|
||||
The following files and folders will be created by opsu! as needed:
|
||||
* `.opsu.cfg`: The configuration file. Most (but not all) of the settings can
|
||||
be changed through the options menu.
|
||||
* `.opsu.db`: The beatmap cache database.
|
||||
* `.opsu_scores.db`: The scores database.
|
||||
* `.opsu.log`: The error log. All critical errors displayed in-game are also
|
||||
logged to this file, and other warnings not shown are logged as well.
|
||||
* `Songs/`: The beatmap directory (not used if an osu! installation is detected).
|
||||
The parser searches all of its subdirectories for .osu files to load.
|
||||
* `SongPacks/`: The beatmap pack directory. The unpacker extracts all .osz
|
||||
files within this directory to the beatmap directory.
|
||||
* `Skins/`: The skins directory. Each skin must be placed in a folder within
|
||||
this directory. Any game resource (in `res/`) can be skinned by placing a
|
||||
file with the same name in a skin folder. Skins can be selected in the
|
||||
options menu.
|
||||
* `Screenshots/`: The screenshot directory. Screenshots can be taken by
|
||||
pressing the F12 key.
|
||||
* `Replays/`: The replay directory. Replays of each completed game are saved
|
||||
as .osr files, and can be viewed at a later time or shared with others.
|
||||
* `ReplayImport/`: The replay import directory. The importer moves all .osr
|
||||
files within this directory to the replay directory and saves the scores in
|
||||
the scores database. Replays can be imported from osu! as well as opsu!.
|
||||
* `Natives/`: The native libraries directory.
|
||||
|
||||
* To run the project, execute the Maven goal `compile exec:exec`.
|
||||
* To create a single executable JAR file, execute the Maven goal
|
||||
`install -Djar`. This will link the LWJGL native libraries using a
|
||||
[modified version](https://github.com/itdelatrisu/JarSplicePlus) of
|
||||
[JarSplice](http://ninjacave.com/jarsplice), which is included in the
|
||||
`tools` directory in both its original and modified forms. The resulting
|
||||
file will be located in `target/opsu-${version}-runnable.jar`.
|
||||
## Building
|
||||
opsu! is distributed as both a [Maven](https://maven.apache.org/) and
|
||||
[Gradle](https://gradle.org/) project.
|
||||
|
||||
### Maven
|
||||
Maven builds are built to the `target` directory.
|
||||
* To run the project, execute the Maven goal `compile`.
|
||||
* To create a single executable jar, execute the Maven goal `package -Djar`.
|
||||
This will compile a jar to `target/opsu-${version}.jar` with the libraries,
|
||||
resources and natives packed inside the jar. Setting the "XDG" property
|
||||
(`-DXDG=true`) will make the application use XDG folders under Unix-like
|
||||
operating systems.
|
||||
|
||||
### Gradle
|
||||
Gradle builds are built to the `build` directory.
|
||||
* To run the project, execute the Gradle task `run`.
|
||||
* To create a single executable jar, execute the Gradle task `jar`.
|
||||
This will compile a jar to `build/libs/opsu-${version}.jar` with the libraries,
|
||||
resources and natives packed inside the jar. Setting the "XDG" property
|
||||
(`-PXDG=true`) will make the application use XDG folders under Unix-like
|
||||
operating systems.
|
||||
|
||||
## Credits
|
||||
This software was created by Jeffrey Han
|
||||
|
||||
109
build.gradle
Normal file
@@ -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
|
||||
76
pom.xml
@@ -1,12 +1,15 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>itdelatrisu</groupId>
|
||||
<artifactId>opsu</artifactId>
|
||||
<version>0.9.0</version>
|
||||
<version>0.11.0</version>
|
||||
<properties>
|
||||
<version>${project.version}</version>
|
||||
<timestamp>${maven.build.timestamp}</timestamp>
|
||||
<maven.build.timestamp.format>yyyy-MM-dd HH:mm</maven.build.timestamp.format>
|
||||
<mainClassName>itdelatrisu.opsu.Opsu</mainClassName>
|
||||
<XDG>false</XDG>
|
||||
</properties>
|
||||
<build>
|
||||
<sourceDirectory>src</sourceDirectory>
|
||||
@@ -67,30 +70,9 @@
|
||||
<skip>${jar}</skip>
|
||||
<executable>java</executable>
|
||||
<arguments>
|
||||
<argument>-Djava.library.path=${project.build.directory}/natives</argument>
|
||||
<argument>-cp</argument>
|
||||
<classpath />
|
||||
<argument>itdelatrisu.opsu.Opsu</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>jarsplice</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<executable>java</executable>
|
||||
<workingDirectory>${basedir}/target</workingDirectory>
|
||||
<arguments>
|
||||
<argument>-jar</argument>
|
||||
<argument>-Dinput=opsu-${project.version}.jar</argument>
|
||||
<argument>-Dmain=itdelatrisu.opsu.Opsu</argument>
|
||||
<argument>-Doutput=opsu-${project.version}-runnable.jar</argument>
|
||||
<!-- uncomment the line below to use XDG base directories in Unix -->
|
||||
<!--<argument>-Dparams=-DXDG=true</argument>-->
|
||||
<argument>${basedir}/tools/JarSplicePlus.jar</argument>
|
||||
<argument>${mainClassName}</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
@@ -98,15 +80,8 @@
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>2.3</version>
|
||||
<version>2.4.1</version>
|
||||
<configuration>
|
||||
<!--
|
||||
<artifactSet>
|
||||
<excludes>
|
||||
<exclude>*:*:natives*</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
-->
|
||||
<filters>
|
||||
<filter>
|
||||
<!-- Overwritten classes -->
|
||||
@@ -124,6 +99,14 @@
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<manifestEntries>
|
||||
<Main-Class>${mainClassName}</Main-Class>
|
||||
<Use-XDG>${XDG}</Use-XDG>
|
||||
</manifestEntries>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
</configuration>
|
||||
<executions>
|
||||
@@ -142,12 +125,24 @@
|
||||
<dependency>
|
||||
<groupId>org.lwjgl.lwjgl</groupId>
|
||||
<artifactId>lwjgl</artifactId>
|
||||
<version>2.9.1</version>
|
||||
<version>2.9.3</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>net.java.jinput</groupId>
|
||||
<artifactId>jinput</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slick2d</groupId>
|
||||
<artifactId>slick2d-core</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<version>1.0.1</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.lwjgl.lwjgl</groupId>
|
||||
<artifactId>lwjgl</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jcraft</groupId>
|
||||
@@ -197,17 +192,22 @@
|
||||
<dependency>
|
||||
<groupId>org.apache.maven</groupId>
|
||||
<artifactId>maven-artifact</artifactId>
|
||||
<version>3.0.3</version>
|
||||
<version>3.3.3</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-compress</artifactId>
|
||||
<version>1.8</version>
|
||||
<version>1.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.tukaani</groupId>
|
||||
<artifactId>xz</artifactId>
|
||||
<version>1.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.jponge</groupId>
|
||||
<artifactId>lzma-java</artifactId>
|
||||
<version>1.2</version>
|
||||
<version>1.3</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
</project>
|
||||
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}
|
||||
file=https://github.com/itdelatrisu/opsu/releases/download/${pom.version}/opsu-${pom.version}.jar
|
||||
version=${version}
|
||||
file=https://github.com/itdelatrisu/opsu/releases/download/${version}/opsu-${version}.jar
|
||||
build.date=${timestamp}
|
||||
1
settings.gradle
Normal file
@@ -0,0 +1 @@
|
||||
rootProject.name = 'opsu'
|
||||
@@ -21,6 +21,7 @@ package itdelatrisu.opsu;
|
||||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
|
||||
import itdelatrisu.opsu.downloads.DownloadList;
|
||||
import itdelatrisu.opsu.downloads.Updater;
|
||||
import itdelatrisu.opsu.render.CurveRenderState;
|
||||
@@ -125,7 +126,7 @@ public class Container extends AppGameContainer {
|
||||
// reset image references
|
||||
GameImage.clearReferences();
|
||||
GameData.Grade.clearReferences();
|
||||
Beatmap.getBackgroundImageCache().clear();
|
||||
Beatmap.clearBackgroundImageCache();
|
||||
|
||||
// prevent loading tracks from re-initializing OpenAL
|
||||
MusicController.reset();
|
||||
@@ -136,6 +137,11 @@ public class Container extends AppGameContainer {
|
||||
|
||||
// delete OpenGL objects involved in the Curve rendering
|
||||
CurveRenderState.shutdown();
|
||||
|
||||
// destroy watch service
|
||||
if (!Options.isWatchServiceEnabled())
|
||||
BeatmapWatchService.destroy();
|
||||
BeatmapWatchService.removeListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,8 +20,10 @@ package itdelatrisu.opsu;
|
||||
|
||||
import java.awt.Cursor;
|
||||
import java.awt.Desktop;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Properties;
|
||||
@@ -44,12 +46,13 @@ public class ErrorHandler {
|
||||
/** Error popup description text. */
|
||||
private static final String
|
||||
desc = "opsu! has encountered an error.",
|
||||
descR = "opsu! has encountered an error. Please report this!";
|
||||
descReport = "opsu! has encountered an error. Please report this!";
|
||||
|
||||
/** Error popup button options. */
|
||||
private static final String[]
|
||||
options = {"View Error Log", "Close"},
|
||||
optionsR = {"Send Report", "View Error Log", "Close"};
|
||||
optionsLog = {"View Error Log", "Close"},
|
||||
optionsReport = {"Send Report", "Close"},
|
||||
optionsLogReport = {"Send Report", "View Error Log", "Close"};
|
||||
|
||||
/** Text area for Exception. */
|
||||
private static final JTextArea textArea = new JTextArea(7, 30);
|
||||
@@ -59,6 +62,7 @@ public class ErrorHandler {
|
||||
textArea.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));
|
||||
textArea.setTabSize(2);
|
||||
textArea.setLineWrap(true);
|
||||
textArea.setWrapStyleWord(true);
|
||||
}
|
||||
|
||||
/** Scroll pane holding JTextArea. */
|
||||
@@ -67,7 +71,7 @@ public class ErrorHandler {
|
||||
/** Error popup objects. */
|
||||
private static final Object[]
|
||||
message = { desc, scroll },
|
||||
messageR = { descR, scroll };
|
||||
messageReport = { descReport, scroll };
|
||||
|
||||
// This class should not be instantiated.
|
||||
private ErrorHandler() {}
|
||||
@@ -107,72 +111,112 @@ public class ErrorHandler {
|
||||
// display popup
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
Desktop desktop = null;
|
||||
boolean isBrowseSupported = false, isOpenSupported = false;
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
// try to open the log file and/or issues webpage
|
||||
if (report) {
|
||||
// ask to report the error
|
||||
int n = JOptionPane.showOptionDialog(null, messageR, title,
|
||||
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
|
||||
null, optionsR, optionsR[2]);
|
||||
if (n == 0) {
|
||||
// auto-fill debug information
|
||||
String issueTitle = (error != null) ? error : e.getMessage();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Properties props = new Properties();
|
||||
props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE));
|
||||
String version = props.getProperty("version");
|
||||
if (version != null && !version.equals("${pom.version}")) {
|
||||
sb.append("**Version:** ");
|
||||
sb.append(version);
|
||||
sb.append('\n');
|
||||
}
|
||||
String timestamp = props.getProperty("build.date");
|
||||
if (timestamp != null &&
|
||||
!timestamp.equals("${maven.build.timestamp}") && !timestamp.equals("${timestamp}")) {
|
||||
sb.append("**Build date:** ");
|
||||
sb.append(timestamp);
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append("**OS:** ");
|
||||
sb.append(System.getProperty("os.name"));
|
||||
sb.append(" (");
|
||||
sb.append(System.getProperty("os.arch"));
|
||||
sb.append(")\n");
|
||||
sb.append("**JRE:** ");
|
||||
sb.append(System.getProperty("java.version"));
|
||||
sb.append('\n');
|
||||
if (error != null) {
|
||||
sb.append("**Error:** `");
|
||||
sb.append(error);
|
||||
sb.append("`\n");
|
||||
}
|
||||
if (trace != null) {
|
||||
sb.append("**Stack trace:**");
|
||||
sb.append("\n```\n");
|
||||
sb.append(trace);
|
||||
sb.append("```");
|
||||
}
|
||||
URI uri = URI.create(String.format(Options.ISSUES_URL,
|
||||
URLEncoder.encode(issueTitle, "UTF-8"),
|
||||
URLEncoder.encode(sb.toString(), "UTF-8")));
|
||||
Desktop.getDesktop().browse(uri);
|
||||
} else if (n == 1)
|
||||
Desktop.getDesktop().open(Options.LOG_FILE);
|
||||
} else {
|
||||
// don't report the error
|
||||
desktop = Desktop.getDesktop();
|
||||
isBrowseSupported = desktop.isSupported(Desktop.Action.BROWSE);
|
||||
isOpenSupported = desktop.isSupported(Desktop.Action.OPEN);
|
||||
}
|
||||
if (desktop != null && (isOpenSupported || (report && isBrowseSupported))) { // try to open the log file and/or issues webpage
|
||||
if (report && isBrowseSupported) { // ask to report the error
|
||||
if (isOpenSupported) { // also ask to open the log
|
||||
int n = JOptionPane.showOptionDialog(null, messageReport, title,
|
||||
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
|
||||
null, optionsLogReport, optionsLogReport[2]);
|
||||
if (n == 0)
|
||||
desktop.browse(getIssueURI(error, e, trace));
|
||||
else if (n == 1)
|
||||
desktop.open(Options.LOG_FILE);
|
||||
} else { // only ask to report the error
|
||||
int n = JOptionPane.showOptionDialog(null, message, title,
|
||||
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
|
||||
null, optionsReport, optionsReport[1]);
|
||||
if (n == 0)
|
||||
desktop.browse(getIssueURI(error, e, trace));
|
||||
}
|
||||
} else { // don't report the error
|
||||
int n = JOptionPane.showOptionDialog(null, message, title,
|
||||
JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE,
|
||||
null, options, options[1]);
|
||||
null, optionsLog, optionsLog[1]);
|
||||
if (n == 0)
|
||||
Desktop.getDesktop().open(Options.LOG_FILE);
|
||||
desktop.open(Options.LOG_FILE);
|
||||
}
|
||||
} else {
|
||||
// display error only
|
||||
JOptionPane.showMessageDialog(null, report ? messageR : message,
|
||||
} else { // display error only
|
||||
JOptionPane.showMessageDialog(null, report ? messageReport : message,
|
||||
title, JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
} catch (Exception e1) {
|
||||
Log.error("Error opening crash popup.", e1);
|
||||
Log.error("An error occurred in the crash popup.", e1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the issue reporting URI.
|
||||
* This will auto-fill the report with the relevant information if possible.
|
||||
* @param error a description of the error
|
||||
* @param e the exception causing the error
|
||||
* @param trace the stack trace
|
||||
* @return the created URI
|
||||
*/
|
||||
private static URI getIssueURI(String error, Throwable e, String trace) {
|
||||
// generate report information
|
||||
String issueTitle = (error != null) ? error : e.getMessage();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try {
|
||||
// read version and build date from version file, if possible
|
||||
Properties props = new Properties();
|
||||
props.load(ResourceLoader.getResourceAsStream(Options.VERSION_FILE));
|
||||
String version = props.getProperty("version");
|
||||
if (version != null && !version.equals("${pom.version}")) {
|
||||
sb.append("**Version:** ");
|
||||
sb.append(version);
|
||||
String hash = Utils.getGitHash();
|
||||
if (hash != null) {
|
||||
sb.append(" (");
|
||||
sb.append(hash.substring(0, 12));
|
||||
sb.append(')');
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
String timestamp = props.getProperty("build.date");
|
||||
if (timestamp != null &&
|
||||
!timestamp.equals("${maven.build.timestamp}") && !timestamp.equals("${timestamp}")) {
|
||||
sb.append("**Build date:** ");
|
||||
sb.append(timestamp);
|
||||
sb.append('\n');
|
||||
}
|
||||
} catch (IOException e1) {
|
||||
Log.warn("Could not read version file.", e1);
|
||||
}
|
||||
sb.append("**OS:** ");
|
||||
sb.append(System.getProperty("os.name"));
|
||||
sb.append(" (");
|
||||
sb.append(System.getProperty("os.arch"));
|
||||
sb.append(")\n");
|
||||
sb.append("**JRE:** ");
|
||||
sb.append(System.getProperty("java.version"));
|
||||
sb.append('\n');
|
||||
if (error != null) {
|
||||
sb.append("**Error:** `");
|
||||
sb.append(error);
|
||||
sb.append("`\n");
|
||||
}
|
||||
if (trace != null) {
|
||||
sb.append("**Stack trace:**");
|
||||
sb.append("\n```\n");
|
||||
sb.append(trace);
|
||||
sb.append("```");
|
||||
}
|
||||
|
||||
// return auto-filled URI
|
||||
try {
|
||||
return URI.create(String.format(Options.ISSUES_URL,
|
||||
URLEncoder.encode(issueTitle, "UTF-8"),
|
||||
URLEncoder.encode(sb.toString(), "UTF-8")));
|
||||
} catch (UnsupportedEncodingException e1) {
|
||||
Log.warn("URLEncoder failed to encode the auto-filled issue report URL.");
|
||||
return URI.create(String.format(Options.ISSUES_URL, "", ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,12 @@ import itdelatrisu.opsu.beatmap.Beatmap;
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
import itdelatrisu.opsu.downloads.Updater;
|
||||
import itdelatrisu.opsu.objects.curves.Curve;
|
||||
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||
import itdelatrisu.opsu.replay.Replay;
|
||||
import itdelatrisu.opsu.replay.ReplayFrame;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
@@ -87,7 +91,7 @@ public class GameData {
|
||||
D (GameImage.RANKING_D, GameImage.RANKING_D_SMALL);
|
||||
|
||||
/** GameImages associated with this grade (large and small sizes). */
|
||||
private GameImage large, small;
|
||||
private final GameImage large, small;
|
||||
|
||||
/** Large-size image scaled for use in song menu. */
|
||||
private Image menuImage;
|
||||
@@ -129,7 +133,7 @@ public class GameData {
|
||||
return menuImage;
|
||||
|
||||
Image img = getSmallImage();
|
||||
if (!small.hasSkinImage()) // save default image only
|
||||
if (!small.hasBeatmapSkinImage()) // save default image only
|
||||
this.menuImage = img;
|
||||
return img;
|
||||
}
|
||||
@@ -199,14 +203,14 @@ public class GameData {
|
||||
*/
|
||||
private class HitErrorInfo {
|
||||
/** The correct hit time. */
|
||||
private int time;
|
||||
private final int time;
|
||||
|
||||
/** The coordinates of the hit. */
|
||||
@SuppressWarnings("unused")
|
||||
private int x, y;
|
||||
private final int x, y;
|
||||
|
||||
/** The difference between the correct and actual hit times. */
|
||||
private int timeDiff;
|
||||
private final int timeDiff;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -232,29 +236,32 @@ public class GameData {
|
||||
/** Hit result helper class. */
|
||||
private class HitObjectResult {
|
||||
/** Object start time. */
|
||||
public int time;
|
||||
public final int time;
|
||||
|
||||
/** Hit result. */
|
||||
public int result;
|
||||
public final int result;
|
||||
|
||||
/** Object coordinates. */
|
||||
public float x, y;
|
||||
public final float x, y;
|
||||
|
||||
/** Combo color. */
|
||||
public Color color;
|
||||
public final Color color;
|
||||
|
||||
/** The type of the hit object. */
|
||||
public HitObjectType hitResultType;
|
||||
public final HitObjectType hitResultType;
|
||||
|
||||
/** Slider curve. */
|
||||
public final Curve curve;
|
||||
|
||||
/** Whether or not to expand when animating. */
|
||||
public final boolean expand;
|
||||
|
||||
/** Whether or not to hide the hit result. */
|
||||
public final boolean hideResult;
|
||||
|
||||
/** Alpha level (for fading out). */
|
||||
public float alpha = 1f;
|
||||
|
||||
/** Slider curve. */
|
||||
public Curve curve;
|
||||
|
||||
/** Whether or not to expand when animating. */
|
||||
public boolean expand;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param time the result's starting track position
|
||||
@@ -262,11 +269,13 @@ public class GameData {
|
||||
* @param x the center x coordinate
|
||||
* @param y the center y coordinate
|
||||
* @param color the color of the hit object
|
||||
* @param hitResultType the hit object type
|
||||
* @param curve the slider curve (or null if not applicable)
|
||||
* @param expand whether or not the hit result animation should expand (if applicable)
|
||||
* @param hideResult whether or not to hide the hit result (but still show the other animations)
|
||||
*/
|
||||
public HitObjectResult(int time, int result, float x, float y, Color color,
|
||||
HitObjectType hitResultType, Curve curve, boolean expand) {
|
||||
HitObjectType hitResultType, Curve curve, boolean expand, boolean hideResult) {
|
||||
this.time = time;
|
||||
this.result = result;
|
||||
this.x = x;
|
||||
@@ -275,6 +284,7 @@ public class GameData {
|
||||
this.hitResultType = hitResultType;
|
||||
this.curve = curve;
|
||||
this.expand = expand;
|
||||
this.hideResult = hideResult;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +326,7 @@ public class GameData {
|
||||
private Replay replay;
|
||||
|
||||
/** Whether this object is used for gameplay (true) or score viewing (false). */
|
||||
private boolean gameplay;
|
||||
private boolean isGameplay;
|
||||
|
||||
/** Container dimensions. */
|
||||
private int width, height;
|
||||
@@ -329,7 +339,7 @@ public class GameData {
|
||||
public GameData(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.gameplay = true;
|
||||
this.isGameplay = true;
|
||||
|
||||
clear();
|
||||
}
|
||||
@@ -345,7 +355,7 @@ public class GameData {
|
||||
public GameData(ScoreData s, int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.gameplay = false;
|
||||
this.isGameplay = false;
|
||||
|
||||
this.scoreData = s;
|
||||
this.score = s.score;
|
||||
@@ -400,8 +410,8 @@ public class GameData {
|
||||
// gameplay-specific images
|
||||
if (isGameplay()) {
|
||||
// combo burst images
|
||||
if (GameImage.COMBO_BURST.hasSkinImages() ||
|
||||
(!GameImage.COMBO_BURST.hasSkinImage() && GameImage.COMBO_BURST.getImages() != null))
|
||||
if (GameImage.COMBO_BURST.hasBeatmapSkinImages() ||
|
||||
(!GameImage.COMBO_BURST.hasBeatmapSkinImage() && GameImage.COMBO_BURST.getImages() != null))
|
||||
comboBurstImages = GameImage.COMBO_BURST.getImages();
|
||||
else
|
||||
comboBurstImages = new Image[]{ GameImage.COMBO_BURST.getImage() };
|
||||
@@ -474,6 +484,7 @@ public class GameData {
|
||||
|
||||
/**
|
||||
* Sets the array of hit result offsets.
|
||||
* @param hitResultOffset the time offset array (of size {@link #HIT_MAX})
|
||||
*/
|
||||
public void setHitResultOffset(int[] hitResultOffset) { this.hitResultOffset = hitResultOffset; }
|
||||
|
||||
@@ -614,7 +625,7 @@ public class GameData {
|
||||
);
|
||||
} else {
|
||||
// lead-in time (yellow)
|
||||
g.setColor(Utils.COLOR_YELLOW_ALPHA);
|
||||
g.setColor(Colors.YELLOW_ALPHA);
|
||||
g.fillArc(circleX, symbolHeight, circleDiameter, circleDiameter,
|
||||
-90 + (int) (360f * trackPosition / firstObjectTime), -90
|
||||
);
|
||||
@@ -651,27 +662,27 @@ public class GameData {
|
||||
float hitErrorY = height / uiScale - margin - 10;
|
||||
float barY = (hitErrorY - 3) * uiScale, barHeight = 6 * uiScale;
|
||||
float tickY = (hitErrorY - 10) * uiScale, tickHeight = 20 * uiScale;
|
||||
float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a;
|
||||
Utils.COLOR_BLACK_ALPHA.a = hitErrorAlpha;
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
float oldAlphaBlack = Colors.BLACK_ALPHA.a;
|
||||
Colors.BLACK_ALPHA.a = hitErrorAlpha;
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, tickY,
|
||||
(hitResultOffset[HIT_50] * 2) * uiScale, tickHeight);
|
||||
Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack;
|
||||
Utils.COLOR_LIGHT_ORANGE.a = hitErrorAlpha;
|
||||
g.setColor(Utils.COLOR_LIGHT_ORANGE);
|
||||
Colors.BLACK_ALPHA.a = oldAlphaBlack;
|
||||
Colors.LIGHT_ORANGE.a = hitErrorAlpha;
|
||||
g.setColor(Colors.LIGHT_ORANGE);
|
||||
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_50]) * uiScale, barY,
|
||||
(hitResultOffset[HIT_50] * 2) * uiScale, barHeight);
|
||||
Utils.COLOR_LIGHT_ORANGE.a = 1f;
|
||||
Utils.COLOR_LIGHT_GREEN.a = hitErrorAlpha;
|
||||
g.setColor(Utils.COLOR_LIGHT_GREEN);
|
||||
Colors.LIGHT_ORANGE.a = 1f;
|
||||
Colors.LIGHT_GREEN.a = hitErrorAlpha;
|
||||
g.setColor(Colors.LIGHT_GREEN);
|
||||
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_100]) * uiScale, barY,
|
||||
(hitResultOffset[HIT_100] * 2) * uiScale, barHeight);
|
||||
Utils.COLOR_LIGHT_GREEN.a = 1f;
|
||||
Utils.COLOR_LIGHT_BLUE.a = hitErrorAlpha;
|
||||
g.setColor(Utils.COLOR_LIGHT_BLUE);
|
||||
Colors.LIGHT_GREEN.a = 1f;
|
||||
Colors.LIGHT_BLUE.a = hitErrorAlpha;
|
||||
g.setColor(Colors.LIGHT_BLUE);
|
||||
g.fillRect((hitErrorX - 3 - hitResultOffset[HIT_300]) * uiScale, barY,
|
||||
(hitResultOffset[HIT_300] * 2) * uiScale, barHeight);
|
||||
Utils.COLOR_LIGHT_BLUE.a = 1f;
|
||||
Colors.LIGHT_BLUE.a = 1f;
|
||||
white.a = hitErrorAlpha;
|
||||
g.setColor(white);
|
||||
g.fillRect((hitErrorX - 1.5f) * uiScale, tickY, 3 * uiScale, tickHeight);
|
||||
@@ -830,16 +841,16 @@ public class GameData {
|
||||
|
||||
// header
|
||||
Image rankingTitle = GameImage.RANKING_TITLE.getImage();
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, 100 * uiScale);
|
||||
rankingTitle.draw((width * 0.97f) - rankingTitle.getWidth(), 0);
|
||||
float marginX = width * 0.01f, marginY = height * 0.002f;
|
||||
Utils.FONT_LARGE.drawString(marginX, marginY,
|
||||
Fonts.LARGE.drawString(marginX, marginY,
|
||||
String.format("%s - %s [%s]", beatmap.getArtist(), beatmap.getTitle(), beatmap.version), Color.white);
|
||||
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() - 6,
|
||||
Fonts.MEDIUM.drawString(marginX, marginY + Fonts.LARGE.getLineHeight() - 6,
|
||||
String.format("Beatmap by %s", beatmap.creator), Color.white);
|
||||
String player = (scoreData.playerName == null) ? "" : String.format(" by %s", scoreData.playerName);
|
||||
Utils.FONT_MEDIUM.drawString(marginX, marginY + Utils.FONT_LARGE.getLineHeight() + Utils.FONT_MEDIUM.getLineHeight() - 10,
|
||||
Fonts.MEDIUM.drawString(marginX, marginY + Fonts.LARGE.getLineHeight() + Fonts.MEDIUM.getLineHeight() - 10,
|
||||
String.format("Played%s on %s.", player, scoreData.getTimeString()), Color.white);
|
||||
|
||||
// mod icons
|
||||
@@ -872,7 +883,7 @@ public class GameData {
|
||||
}
|
||||
|
||||
// hit lighting
|
||||
else if (Options.isHitLightingEnabled() && hitResult.result != HIT_MISS &&
|
||||
else if (Options.isHitLightingEnabled() && !hitResult.hideResult && hitResult.result != HIT_MISS &&
|
||||
hitResult.result != HIT_SLIDER30 && hitResult.result != HIT_SLIDER10) {
|
||||
// TODO: add particle system
|
||||
Image lighting = GameImage.LIGHTING.getImage();
|
||||
@@ -885,47 +896,45 @@ public class GameData {
|
||||
hitResult.hitResultType == HitObjectType.CIRCLE ||
|
||||
hitResult.hitResultType == HitObjectType.SLIDER_FIRST ||
|
||||
hitResult.hitResultType == HitObjectType.SLIDER_LAST)) {
|
||||
float scale = (!hitResult.expand) ? 1f : Utils.easeOut(
|
||||
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME),
|
||||
1f, HITCIRCLE_ANIM_SCALE - 1f, HITCIRCLE_FADE_TIME
|
||||
);
|
||||
float alpha = Utils.easeOut(
|
||||
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME),
|
||||
1f, -1f, HITCIRCLE_FADE_TIME
|
||||
);
|
||||
float progress = AnimationEquation.OUT_CUBIC.calc(
|
||||
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_FADE_TIME) / HITCIRCLE_FADE_TIME);
|
||||
float scale = (!hitResult.expand) ? 1f : 1f + (HITCIRCLE_ANIM_SCALE - 1f) * progress;
|
||||
float alpha = 1f - progress;
|
||||
|
||||
// slider curve
|
||||
if (hitResult.curve != null) {
|
||||
float oldWhiteAlpha = Utils.COLOR_WHITE_FADE.a;
|
||||
float oldWhiteAlpha = Colors.WHITE_FADE.a;
|
||||
float oldColorAlpha = hitResult.color.a;
|
||||
Utils.COLOR_WHITE_FADE.a = alpha;
|
||||
Colors.WHITE_FADE.a = alpha;
|
||||
hitResult.color.a = alpha;
|
||||
hitResult.curve.draw(hitResult.color);
|
||||
Utils.COLOR_WHITE_FADE.a = oldWhiteAlpha;
|
||||
Colors.WHITE_FADE.a = oldWhiteAlpha;
|
||||
hitResult.color.a = oldColorAlpha;
|
||||
}
|
||||
|
||||
// hit circles
|
||||
Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
|
||||
Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
|
||||
scaledHitCircle.setAlpha(alpha);
|
||||
scaledHitCircleOverlay.setAlpha(alpha);
|
||||
scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
|
||||
scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
|
||||
if (!(hitResult.hitResultType == HitObjectType.CIRCLE && GameMod.HIDDEN.isActive())) {
|
||||
// "hidden" mod: expanding animation for only circles not drawn
|
||||
Image scaledHitCircle = GameImage.HITCIRCLE.getImage().getScaledCopy(scale);
|
||||
Image scaledHitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(scale);
|
||||
scaledHitCircle.setAlpha(alpha);
|
||||
scaledHitCircleOverlay.setAlpha(alpha);
|
||||
scaledHitCircle.drawCentered(hitResult.x, hitResult.y, hitResult.color);
|
||||
scaledHitCircleOverlay.drawCentered(hitResult.x, hitResult.y);
|
||||
}
|
||||
}
|
||||
|
||||
// hit result
|
||||
if (hitResult.hitResultType == HitObjectType.CIRCLE ||
|
||||
if (!hitResult.hideResult && (
|
||||
hitResult.hitResultType == HitObjectType.CIRCLE ||
|
||||
hitResult.hitResultType == HitObjectType.SPINNER ||
|
||||
hitResult.curve != null) {
|
||||
float scale = Utils.easeBounce(
|
||||
Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME),
|
||||
1f, HITCIRCLE_TEXT_ANIM_SCALE - 1f, HITCIRCLE_TEXT_BOUNCE_TIME
|
||||
);
|
||||
float alpha = Utils.easeOut(
|
||||
Utils.clamp((trackPosition - hitResult.time) - HITCIRCLE_FADE_TIME, 0, HITCIRCLE_TEXT_FADE_TIME),
|
||||
1f, -1f, HITCIRCLE_TEXT_FADE_TIME
|
||||
);
|
||||
hitResult.curve != null)) {
|
||||
float scaleProgress = AnimationEquation.IN_OUT_BOUNCE.calc(
|
||||
(float) Utils.clamp(trackPosition - hitResult.time, 0, HITCIRCLE_TEXT_BOUNCE_TIME) / HITCIRCLE_TEXT_BOUNCE_TIME);
|
||||
float scale = 1f + (HITCIRCLE_TEXT_ANIM_SCALE - 1f) * scaleProgress;
|
||||
float fadeProgress = AnimationEquation.OUT_CUBIC.calc(
|
||||
(float) Utils.clamp((trackPosition - hitResult.time) - HITCIRCLE_FADE_TIME, 0, HITCIRCLE_TEXT_FADE_TIME) / HITCIRCLE_TEXT_FADE_TIME);
|
||||
float alpha = 1f - fadeProgress;
|
||||
Image scaledHitResult = hitResults[hitResult.result].getScaledCopy(scale);
|
||||
scaledHitResult.setAlpha(alpha);
|
||||
scaledHitResult.drawCentered(hitResult.x, hitResult.y);
|
||||
@@ -1037,10 +1046,13 @@ public class GameData {
|
||||
* or {@code Grade.NULL} if no objects have been processed.
|
||||
*/
|
||||
private Grade getGrade() {
|
||||
boolean silver = (scoreData == null) ?
|
||||
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive()) :
|
||||
(scoreData.mods & (GameMod.HIDDEN.getBit() | GameMod.FLASHLIGHT.getBit())) != 0;
|
||||
return getGrade(
|
||||
hitResultCount[HIT_300], hitResultCount[HIT_100],
|
||||
hitResultCount[HIT_50], hitResultCount[HIT_MISS],
|
||||
(GameMod.HIDDEN.isActive() || GameMod.FLASHLIGHT.isActive())
|
||||
silver
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1179,7 +1191,7 @@ public class GameData {
|
||||
switch (result) {
|
||||
case HIT_SLIDER30:
|
||||
hitValue = 30;
|
||||
changeHealth(1f);
|
||||
changeHealth(2f);
|
||||
SoundController.playHitSound(
|
||||
hitObject.getEdgeHitSoundType(repeat),
|
||||
hitObject.getSampleSet(repeat),
|
||||
@@ -1187,6 +1199,7 @@ public class GameData {
|
||||
break;
|
||||
case HIT_SLIDER10:
|
||||
hitValue = 10;
|
||||
changeHealth(1f);
|
||||
SoundController.playHitSound(HitSound.SLIDERTICK);
|
||||
break;
|
||||
case HIT_MISS:
|
||||
@@ -1204,7 +1217,7 @@ public class GameData {
|
||||
if (!Options.isPerfectHitBurstEnabled())
|
||||
; // hide perfect hit results
|
||||
else
|
||||
hitResultList.add(new HitObjectResult(time, result, x, y, null, HitObjectType.SLIDERTICK, null, false));
|
||||
hitResultList.add(new HitObjectResult(time, result, x, y, null, HitObjectType.SLIDERTICK, null, false, false));
|
||||
}
|
||||
fullObjectCount++;
|
||||
}
|
||||
@@ -1360,20 +1373,18 @@ public class GameData {
|
||||
int hitResult = handleHitResult(time, result, x, y, color, end, hitObject,
|
||||
hitResultType, repeat, (curve != null && !sliderHeldToEnd));
|
||||
|
||||
if ((hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled())
|
||||
; // hide perfect hit results
|
||||
else if (hitResult == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
|
||||
; // "relax" and "autopilot" mods: hide misses
|
||||
else {
|
||||
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand));
|
||||
if (hitResult == HIT_MISS && (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive()))
|
||||
return; // "relax" and "autopilot" mods: hide misses
|
||||
|
||||
// sliders: add the other curve endpoint for the hit animation
|
||||
if (curve != null) {
|
||||
boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST);
|
||||
float[] p = curve.pointAt((isFirst) ? 1f : 0f);
|
||||
HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST;
|
||||
hitResultList.add(new HitObjectResult(time, hitResult, p[0], p[1], color, type, null, expand));
|
||||
}
|
||||
boolean hideResult = (hitResult == HIT_300 || hitResult == HIT_300G || hitResult == HIT_300K) && !Options.isPerfectHitBurstEnabled();
|
||||
hitResultList.add(new HitObjectResult(time, hitResult, x, y, color, hitResultType, curve, expand, hideResult));
|
||||
|
||||
// sliders: add the other curve endpoint for the hit animation
|
||||
if (curve != null) {
|
||||
boolean isFirst = (hitResultType == HitObjectType.SLIDER_FIRST);
|
||||
Vec2f p = curve.pointAt((isFirst) ? 1f : 0f);
|
||||
HitObjectType type = (isFirst) ? HitObjectType.SLIDER_LAST : HitObjectType.SLIDER_FIRST;
|
||||
hitResultList.add(new HitObjectResult(time, hitResult, p.x, p.y, color, type, null, expand, hideResult));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1460,7 +1471,13 @@ public class GameData {
|
||||
* Returns whether or not this object is used for gameplay.
|
||||
* @return true if gameplay, false if score viewing
|
||||
*/
|
||||
public boolean isGameplay() { return gameplay; }
|
||||
public boolean isGameplay() { return isGameplay; }
|
||||
|
||||
/**
|
||||
* Sets whether or not this object is used for gameplay.
|
||||
* @param gameplay true if gameplay, false if score viewing
|
||||
*/
|
||||
public void setGameplay(boolean gameplay) { this.isGameplay = gameplay; }
|
||||
|
||||
/**
|
||||
* Adds the hit into the list of hit error information.
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -35,8 +37,8 @@ public enum GameImage {
|
||||
CURSOR ("cursor", "png"),
|
||||
CURSOR_MIDDLE ("cursormiddle", "png"),
|
||||
CURSOR_TRAIL ("cursortrail", "png"),
|
||||
CURSOR_OLD ("cursor2", "png", false, false),
|
||||
CURSOR_TRAIL_OLD ("cursortrail2", "png", false, false),
|
||||
CURSOR_OLD ("cursor2", "png", false, false), // custom
|
||||
CURSOR_TRAIL_OLD ("cursortrail2", "png", false, false), // custom
|
||||
|
||||
// Game
|
||||
SECTION_PASS ("section-pass", "png"),
|
||||
@@ -251,14 +253,14 @@ public enum GameImage {
|
||||
MENU_MUSICNOTE ("music-note", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
int r = (int) ((Utils.FONT_LARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 8) / getUIscale());
|
||||
int r = (int) ((Fonts.LARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() - 8) / getUIscale());
|
||||
return img.getScaledCopy(r, r);
|
||||
}
|
||||
},
|
||||
MENU_LOADER ("loader", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
int r = (int) ((Utils.FONT_LARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 8) / getUIscale());
|
||||
int r = (int) ((Fonts.LARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() - 8) / getUIscale());
|
||||
return img.getScaledCopy(r / 48f);
|
||||
}
|
||||
},
|
||||
@@ -290,12 +292,25 @@ public enum GameImage {
|
||||
MENU_BUTTON_MID ("button-middle", "png", false, false),
|
||||
MENU_BUTTON_LEFT ("button-left", "png", false, false),
|
||||
MENU_BUTTON_RIGHT ("button-right", "png", false, false),
|
||||
STAR ("star", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
return img.getScaledCopy((MENU_BUTTON_BG.getImage().getHeight() * 0.16f) / img.getHeight());
|
||||
}
|
||||
},
|
||||
STAR2 ("star2", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
return img.getScaledCopy((MENU_BUTTON_BG.getImage().getHeight() * 0.33f) / img.getHeight());
|
||||
}
|
||||
},
|
||||
|
||||
// Music Player Buttons
|
||||
MUSIC_PLAY ("music-play", "png", false, false),
|
||||
MUSIC_PAUSE ("music-pause", "png", false, false),
|
||||
MUSIC_NEXT ("music-next", "png", false, false),
|
||||
MUSIC_PREVIOUS ("music-previous", "png", false, false),
|
||||
|
||||
DOWNLOADS ("downloads", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
@@ -312,7 +327,7 @@ public enum GameImage {
|
||||
DELETE ("delete", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
int lineHeight = Utils.FONT_DEFAULT.getLineHeight();
|
||||
int lineHeight = Fonts.DEFAULT.getLineHeight();
|
||||
return img.getScaledCopy(lineHeight, lineHeight);
|
||||
}
|
||||
},
|
||||
@@ -328,10 +343,16 @@ public enum GameImage {
|
||||
return img.getScaledCopy((h / 17f) / img.getHeight());
|
||||
}
|
||||
},
|
||||
BANG ("bang", "png", false, false) {
|
||||
DOWNLOAD ("download", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
return REPOSITORY.process_sub(img, w, h);
|
||||
return img.getScaledCopy((h / 14f) / img.getHeight());
|
||||
}
|
||||
},
|
||||
UPDATE ("update", "png", false, false) {
|
||||
@Override
|
||||
protected Image process_sub(Image img, int w, int h) {
|
||||
return img.getScaledCopy((h / 14f) / img.getHeight());
|
||||
}
|
||||
},
|
||||
OPTIONS_BG ("options-background", "png|jpg", false, true) {
|
||||
@@ -341,6 +362,8 @@ public enum GameImage {
|
||||
return img.getScaledCopy(w, h);
|
||||
}
|
||||
},
|
||||
CHEVRON_DOWN ("chevron-down", "png", false, false),
|
||||
CHEVRON_RIGHT ("chevron-right", "png", false, false),
|
||||
|
||||
// TODO: ensure this image hasn't been modified (checksum?)
|
||||
ALPHA_MAP ("alpha", "png", false, false);
|
||||
@@ -351,22 +374,22 @@ public enum GameImage {
|
||||
IMG_JPG = 2;
|
||||
|
||||
/** The file name. */
|
||||
private String filename;
|
||||
private final String filename;
|
||||
|
||||
/** The formatted file name string (for loading multiple images). */
|
||||
private String filenameFormat;
|
||||
|
||||
/** Image file type. */
|
||||
private byte type;
|
||||
private final byte type;
|
||||
|
||||
/**
|
||||
* Whether or not the image is skinnable by a beatmap.
|
||||
* These images are typically related to gameplay.
|
||||
*/
|
||||
private boolean skinnable;
|
||||
private final boolean beatmapSkinnable;
|
||||
|
||||
/** Whether or not to preload the image when the program starts. */
|
||||
private boolean preload;
|
||||
private final boolean preload;
|
||||
|
||||
/** The default image. */
|
||||
private Image defaultImage;
|
||||
@@ -374,6 +397,9 @@ public enum GameImage {
|
||||
/** The default image array. */
|
||||
private Image[] defaultImages;
|
||||
|
||||
/** Whether the image is currently skinned by a game skin. */
|
||||
private boolean isSkinned = false;
|
||||
|
||||
/** The beatmap skin image (optional, temporary). */
|
||||
private Image skinImage;
|
||||
|
||||
@@ -421,6 +447,7 @@ public enum GameImage {
|
||||
for (GameImage img : GameImage.values()) {
|
||||
img.defaultImage = img.skinImage = null;
|
||||
img.defaultImages = img.skinImages = null;
|
||||
img.isSkinned = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,7 +515,7 @@ public enum GameImage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for game-related images (skinnable and preloaded).
|
||||
* Constructor for game-related images (beatmap-skinnable and preloaded).
|
||||
* @param filename the image file name
|
||||
* @param type the file types (separated by '|')
|
||||
*/
|
||||
@@ -497,7 +524,7 @@ public enum GameImage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for an array of game-related images (skinnable and preloaded).
|
||||
* Constructor for an array of game-related images (beatmap-skinnable and preloaded).
|
||||
* @param filename the image file name
|
||||
* @param filenameFormat the formatted file name string (for loading multiple images)
|
||||
* @param type the file types (separated by '|')
|
||||
@@ -511,21 +538,21 @@ public enum GameImage {
|
||||
* Constructor for general images.
|
||||
* @param filename the image file name
|
||||
* @param type the file types (separated by '|')
|
||||
* @param skinnable whether or not the image is skinnable
|
||||
* @param beatmapSkinnable whether or not the image is beatmap-skinnable
|
||||
* @param preload whether or not to preload the image
|
||||
*/
|
||||
GameImage(String filename, String type, boolean skinnable, boolean preload) {
|
||||
GameImage(String filename, String type, boolean beatmapSkinnable, boolean preload) {
|
||||
this.filename = filename;
|
||||
this.type = getType(type);
|
||||
this.skinnable = skinnable;
|
||||
this.beatmapSkinnable = beatmapSkinnable;
|
||||
this.preload = preload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the image is skinnable.
|
||||
* @return true if skinnable
|
||||
* Returns whether or not the image is beatmap-skinnable.
|
||||
* @return true if beatmap-skinnable
|
||||
*/
|
||||
public boolean isSkinnable() { return skinnable; }
|
||||
public boolean isBeatmapSkinnable() { return beatmapSkinnable; }
|
||||
|
||||
/**
|
||||
* Returns whether or not to preload the image when the program starts.
|
||||
@@ -535,7 +562,7 @@ public enum GameImage {
|
||||
|
||||
/**
|
||||
* Returns the image associated with this resource.
|
||||
* The skin image takes priority over the default image.
|
||||
* The beatmap skin image takes priority over the default image.
|
||||
*/
|
||||
public Image getImage() {
|
||||
setDefaultImage();
|
||||
@@ -556,7 +583,7 @@ public enum GameImage {
|
||||
|
||||
/**
|
||||
* Returns the image array associated with this resource.
|
||||
* The skin images takes priority over the default images.
|
||||
* The beatmap skin images takes priority over the default images.
|
||||
*/
|
||||
public Image[] getImages() {
|
||||
setDefaultImage();
|
||||
@@ -565,7 +592,7 @@ public enum GameImage {
|
||||
|
||||
/**
|
||||
* Sets the image associated with this resource to another image.
|
||||
* The skin image takes priority over the default image.
|
||||
* The beatmap skin image takes priority over the default image.
|
||||
* @param img the image to set
|
||||
*/
|
||||
public void setImage(Image img) {
|
||||
@@ -577,7 +604,7 @@ public enum GameImage {
|
||||
|
||||
/**
|
||||
* Sets an image associated with this resource to another image.
|
||||
* The skin image takes priority over the default image.
|
||||
* The beatmap skin image takes priority over the default image.
|
||||
* @param img the image to set
|
||||
* @param index the index in the image array
|
||||
*/
|
||||
@@ -602,16 +629,26 @@ public enum GameImage {
|
||||
// try to load multiple images
|
||||
File skinDir = Options.getSkin().getDirectory();
|
||||
if (filenameFormat != null) {
|
||||
if ((skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) ||
|
||||
((defaultImages = loadImageArray(null)) != null)) {
|
||||
if (skinDir != null && ((defaultImages = loadImageArray(skinDir)) != null)) {
|
||||
isSkinned = true;
|
||||
process();
|
||||
return;
|
||||
}
|
||||
if ((defaultImages = loadImageArray(null)) != null) {
|
||||
isSkinned = false;
|
||||
process();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// try to load a single image
|
||||
if ((skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) ||
|
||||
((defaultImage = loadImageSingle(null)) != null)) {
|
||||
if (skinDir != null && ((defaultImage = loadImageSingle(skinDir)) != null)) {
|
||||
isSkinned = true;
|
||||
process();
|
||||
return;
|
||||
}
|
||||
if ((defaultImage = loadImageSingle(null)) != null) {
|
||||
isSkinned = false;
|
||||
process();
|
||||
return;
|
||||
}
|
||||
@@ -620,17 +657,17 @@ public enum GameImage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the associated skin image.
|
||||
* Sets the associated beatmap skin image.
|
||||
* If the path does not contain the image, the default image is used.
|
||||
* @param dir the image directory to search
|
||||
* @return true if a new skin image is loaded, false otherwise
|
||||
*/
|
||||
public boolean setSkinImage(File dir) {
|
||||
public boolean setBeatmapSkinImage(File dir) {
|
||||
if (dir == null)
|
||||
return false;
|
||||
|
||||
// destroy the existing images, if any
|
||||
destroySkinImage();
|
||||
destroyBeatmapSkinImage();
|
||||
|
||||
// beatmap skins disabled
|
||||
if (Options.isBeatmapSkinIgnored())
|
||||
@@ -709,21 +746,27 @@ public enum GameImage {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a skin image is currently loaded.
|
||||
* @return true if skin image exists
|
||||
* Returns whether the default image loaded is part of a game skin.
|
||||
* @return true if a game skin image is loaded, false if the default image is loaded
|
||||
*/
|
||||
public boolean hasSkinImage() { return (skinImage != null && !skinImage.isDestroyed()); }
|
||||
public boolean hasGameSkinImage() { return isSkinned; }
|
||||
|
||||
/**
|
||||
* Returns whether skin images are currently loaded.
|
||||
* @return true if any skin image exists
|
||||
* Returns whether a beatmap skin image is currently loaded.
|
||||
* @return true if a beatmap skin image exists
|
||||
*/
|
||||
public boolean hasSkinImages() { return (skinImages != null); }
|
||||
public boolean hasBeatmapSkinImage() { return (skinImage != null && !skinImage.isDestroyed()); }
|
||||
|
||||
/**
|
||||
* Destroys the associated skin image(s), if any.
|
||||
* Returns whether beatmap skin images are currently loaded.
|
||||
* @return true if any beatmap skin image exists
|
||||
*/
|
||||
public void destroySkinImage() {
|
||||
public boolean hasBeatmapSkinImages() { return (skinImages != null); }
|
||||
|
||||
/**
|
||||
* Destroys the associated beatmap skin image(s), if any.
|
||||
*/
|
||||
public void destroyBeatmapSkinImage() {
|
||||
if (skinImage == null && skinImages == null)
|
||||
return;
|
||||
try {
|
||||
@@ -740,7 +783,7 @@ public enum GameImage {
|
||||
skinImages = null;
|
||||
}
|
||||
} catch (SlickException e) {
|
||||
ErrorHandler.error(String.format("Failed to destroy skin images for '%s'.", this.name()), e, true);
|
||||
ErrorHandler.error(String.format("Failed to destroy beatmap skin images for '%s'.", this.name()), e, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.MenuButton;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -47,7 +49,7 @@ public enum GameMod {
|
||||
"DoubleTime", "Zoooooooooom."),
|
||||
// NIGHTCORE (Category.HARD, 2, GameImage.MOD_NIGHTCORE, "NT", 64, Input.KEY_D, 1.12f,
|
||||
// "Nightcore", "uguuuuuuuu"),
|
||||
HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f, false,
|
||||
HIDDEN (Category.HARD, 3, GameImage.MOD_HIDDEN, "HD", 8, Input.KEY_F, 1.06f,
|
||||
"Hidden", "Play with no approach circles and fading notes for a slight score advantage."),
|
||||
FLASHLIGHT (Category.HARD, 4, GameImage.MOD_FLASHLIGHT, "FL", 1024, Input.KEY_G, 1.12f,
|
||||
"Flashlight", "Restricted view area."),
|
||||
@@ -55,9 +57,9 @@ public enum GameMod {
|
||||
"Relax", "You don't need to click.\nGive your clicking/tapping finger a break from the heat of things.\n**UNRANKED**"),
|
||||
AUTOPILOT (Category.SPECIAL, 1, GameImage.MOD_AUTOPILOT, "AP", 8192, Input.KEY_X, 0f,
|
||||
"Relax2", "Automatic cursor movement - just follow the rhythm.\n**UNRANKED**"),
|
||||
SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_V, 0.9f,
|
||||
SPUN_OUT (Category.SPECIAL, 2, GameImage.MOD_SPUN_OUT, "SO", 4096, Input.KEY_C, 0.9f,
|
||||
"SpunOut", "Spinners will be automatically completed."),
|
||||
AUTO (Category.SPECIAL, 3, GameImage.MOD_AUTO, "", 2048, Input.KEY_B, 1f,
|
||||
AUTO (Category.SPECIAL, 3, GameImage.MOD_AUTO, "", 2048, Input.KEY_V, 1f,
|
||||
"Autoplay", "Watch a perfect automated play through the song.");
|
||||
|
||||
/** Mod categories. */
|
||||
@@ -67,13 +69,13 @@ public enum GameMod {
|
||||
SPECIAL (2, "Special", Color.white);
|
||||
|
||||
/** Drawing index. */
|
||||
private int index;
|
||||
private final int index;
|
||||
|
||||
/** Category name. */
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
/** Text color. */
|
||||
private Color color;
|
||||
private final Color color;
|
||||
|
||||
/** The coordinates of the category. */
|
||||
private float x, y;
|
||||
@@ -96,10 +98,10 @@ public enum GameMod {
|
||||
* @param height the container height
|
||||
*/
|
||||
public void init(int width, int height) {
|
||||
float multY = Utils.FONT_LARGE.getLineHeight() * 2 + height * 0.06f;
|
||||
float multY = Fonts.LARGE.getLineHeight() * 2 + height * 0.06f;
|
||||
float offsetY = GameImage.MOD_EASY.getImage().getHeight() * 1.5f;
|
||||
this.x = width / 30f;
|
||||
this.y = multY + Utils.FONT_LARGE.getLineHeight() * 3f + offsetY * index;
|
||||
this.y = multY + Fonts.LARGE.getLineHeight() * 3f + offsetY * index;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,37 +126,34 @@ public enum GameMod {
|
||||
}
|
||||
|
||||
/** The category for the mod. */
|
||||
private Category category;
|
||||
private final Category category;
|
||||
|
||||
/** The index in the category (for positioning). */
|
||||
private int categoryIndex;
|
||||
private final int categoryIndex;
|
||||
|
||||
/** The file name of the mod image. */
|
||||
private GameImage image;
|
||||
private final GameImage image;
|
||||
|
||||
/** The abbreviation for the mod. */
|
||||
private String abbrev;
|
||||
private final String abbrev;
|
||||
|
||||
/**
|
||||
* Bit value associated with the mod.
|
||||
* See the osu! API: https://github.com/peppy/osu-api/wiki#mods
|
||||
*/
|
||||
private int bit;
|
||||
private final int bit;
|
||||
|
||||
/** The shortcut key associated with the mod. */
|
||||
private int key;
|
||||
private final int key;
|
||||
|
||||
/** The score multiplier. */
|
||||
private float multiplier;
|
||||
|
||||
/** Whether or not the mod is implemented. */
|
||||
private boolean implemented;
|
||||
private final float multiplier;
|
||||
|
||||
/** The name of the mod. */
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
/** The description of the mod. */
|
||||
private String description;
|
||||
private final String description;
|
||||
|
||||
/** Whether or not this mod is active. */
|
||||
private boolean active = false;
|
||||
@@ -192,13 +191,15 @@ public enum GameMod {
|
||||
c.init(width, height);
|
||||
|
||||
// create buttons
|
||||
float baseX = Category.EASY.getX() + Utils.FONT_LARGE.getWidth(Category.EASY.getName()) * 1.25f;
|
||||
float baseX = Category.EASY.getX() + Fonts.LARGE.getWidth(Category.EASY.getName()) * 1.25f;
|
||||
float offsetX = GameImage.MOD_EASY.getImage().getWidth() * 2.1f;
|
||||
for (GameMod mod : GameMod.values()) {
|
||||
Image img = mod.image.getImage();
|
||||
mod.button = new MenuButton(img,
|
||||
baseX + (offsetX * mod.categoryIndex) + img.getWidth() / 2f,
|
||||
mod.category.getY());
|
||||
mod.button.setHoverAnimationDuration(300);
|
||||
mod.button.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
|
||||
mod.button.setHoverExpand(1.2f);
|
||||
mod.button.setHoverRotate(10f);
|
||||
|
||||
@@ -309,24 +310,6 @@ public enum GameMod {
|
||||
*/
|
||||
GameMod(Category category, int categoryIndex, GameImage image, String abbrev,
|
||||
int bit, int key, float multiplier, String name, String description) {
|
||||
this(category, categoryIndex, image, abbrev, bit, key, multiplier, true, name, description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param category the category for the mod
|
||||
* @param categoryIndex the index in the category
|
||||
* @param image the GameImage
|
||||
* @param abbrev the two-letter abbreviation
|
||||
* @param bit the bit
|
||||
* @param key the shortcut key
|
||||
* @param multiplier the score multiplier
|
||||
* @param implemented whether the mod is implemented
|
||||
* @param name the name
|
||||
* @param description the description
|
||||
*/
|
||||
GameMod(Category category, int categoryIndex, GameImage image, String abbrev,
|
||||
int bit, int key, float multiplier, boolean implemented, String name, String description) {
|
||||
this.category = category;
|
||||
this.categoryIndex = categoryIndex;
|
||||
this.image = image;
|
||||
@@ -334,7 +317,6 @@ public enum GameMod {
|
||||
this.bit = bit;
|
||||
this.key = key;
|
||||
this.multiplier = multiplier;
|
||||
this.implemented = implemented;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
}
|
||||
@@ -376,20 +358,11 @@ public enum GameMod {
|
||||
*/
|
||||
public String getDescription() { return description; }
|
||||
|
||||
/**
|
||||
* Returns whether or not the mod is implemented.
|
||||
* @return true if implemented
|
||||
*/
|
||||
public boolean isImplemented() { return implemented; }
|
||||
|
||||
/**
|
||||
* Toggles the active status of the mod.
|
||||
* @param checkInverse if true, perform checks for mutual exclusivity
|
||||
*/
|
||||
public void toggle(boolean checkInverse) {
|
||||
if (!implemented)
|
||||
return;
|
||||
|
||||
active = !active;
|
||||
scoreMultiplier = speedMultiplier = difficultyMultiplier = -1f;
|
||||
|
||||
@@ -446,14 +419,7 @@ public enum GameMod {
|
||||
/**
|
||||
* Draws the game mod.
|
||||
*/
|
||||
public void draw() {
|
||||
if (!implemented) {
|
||||
button.getImage().setAlpha(0.2f);
|
||||
button.draw();
|
||||
button.getImage().setAlpha(1f);
|
||||
} else
|
||||
button.draw();
|
||||
}
|
||||
public void draw() { button.draw(); }
|
||||
|
||||
/**
|
||||
* Checks if the coordinates are within the image bounds.
|
||||
|
||||
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.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.state.transition.FadeInTransition;
|
||||
@@ -116,16 +120,43 @@ public class Opsu extends StateBasedGame {
|
||||
|
||||
// only allow a single instance
|
||||
try {
|
||||
SERVER_SOCKET = new ServerSocket(Options.getPort());
|
||||
SERVER_SOCKET = new ServerSocket(Options.getPort(), 1, InetAddress.getLocalHost());
|
||||
} catch (UnknownHostException e) {
|
||||
// shouldn't happen
|
||||
} catch (IOException e) {
|
||||
ErrorHandler.error(String.format("Another program is already running on port %d.", Options.getPort()), e, false);
|
||||
ErrorHandler.error(String.format(
|
||||
"opsu! could not be launched for one of these reasons:\n" +
|
||||
"- An instance of opsu! is already running.\n" +
|
||||
"- Another program is bound to port %d. " +
|
||||
"You can change the port opsu! uses by editing the \"Port\" field in the configuration file.",
|
||||
Options.getPort()), null, false);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
// set path for lwjgl natives - NOT NEEDED if using JarSplice
|
||||
File nativeDir = new File("./target/natives/");
|
||||
if (nativeDir.isDirectory())
|
||||
System.setProperty("org.lwjgl.librarypath", nativeDir.getAbsolutePath());
|
||||
File nativeDir;
|
||||
if (!Utils.isJarRunning() && (
|
||||
(nativeDir = new File("./target/natives/")).isDirectory() ||
|
||||
(nativeDir = new File("./build/natives/")).isDirectory()))
|
||||
;
|
||||
else {
|
||||
nativeDir = Options.NATIVE_DIR;
|
||||
try {
|
||||
new NativeLoader(nativeDir).loadNatives();
|
||||
} catch (IOException e) {
|
||||
Log.error("Error loading natives.", e);
|
||||
}
|
||||
}
|
||||
System.setProperty("org.lwjgl.librarypath", nativeDir.getAbsolutePath());
|
||||
System.setProperty("java.library.path", nativeDir.getAbsolutePath());
|
||||
try {
|
||||
// Workaround for "java.library.path" property being read-only.
|
||||
// http://stackoverflow.com/a/24988095
|
||||
Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths");
|
||||
fieldSysPath.setAccessible(true);
|
||||
fieldSysPath.set(null, null);
|
||||
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
|
||||
Log.warn("Failed to set 'sys_paths' field.", e);
|
||||
}
|
||||
|
||||
// set the resource paths
|
||||
ResourceLoader.addResourceLocation(new FileSystemLocation(new File("./res/")));
|
||||
@@ -142,16 +173,21 @@ public class Opsu extends StateBasedGame {
|
||||
Updater.get().setUpdateInfo(args[0], args[1]);
|
||||
|
||||
// check for updates
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Updater.get().checkForUpdates();
|
||||
} catch (IOException e) {
|
||||
Log.warn("Check for updates failed.", e);
|
||||
if (!Options.isUpdaterDisabled()) {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Updater.get().checkForUpdates();
|
||||
} catch (IOException e) {
|
||||
Log.warn("Check for updates failed.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}.start();
|
||||
}
|
||||
|
||||
// disable jinput
|
||||
Input.disableControllers();
|
||||
|
||||
// start the game
|
||||
try {
|
||||
@@ -190,30 +226,28 @@ public class Opsu extends StateBasedGame {
|
||||
SongMenu songMenu = (SongMenu) this.getState(Opsu.STATE_SONGMENU);
|
||||
if (id == STATE_GAMERANKING) {
|
||||
GameData data = ((GameRanking) this.getState(Opsu.STATE_GAMERANKING)).getGameData();
|
||||
if (data != null && data.isGameplay()) {
|
||||
songMenu.resetGameDataOnLoad();
|
||||
if (data != null && data.isGameplay())
|
||||
songMenu.resetTrackOnLoad();
|
||||
}
|
||||
} else {
|
||||
songMenu.resetGameDataOnLoad();
|
||||
if (id == STATE_GAME) {
|
||||
MusicController.pause();
|
||||
MusicController.resume();
|
||||
} else
|
||||
songMenu.resetTrackOnLoad();
|
||||
}
|
||||
if (UI.getCursor().isSkinned())
|
||||
if (UI.getCursor().isBeatmapSkinned())
|
||||
UI.getCursor().reset();
|
||||
songMenu.resetGameDataOnLoad();
|
||||
this.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
return false;
|
||||
}
|
||||
|
||||
// show confirmation dialog if any downloads are active
|
||||
if (DownloadList.get().hasActiveDownloads() &&
|
||||
UI.showExitConfirmation(DownloadList.EXIT_CONFIRMATION))
|
||||
UI.showExitConfirmation(DownloadList.EXIT_CONFIRMATION))
|
||||
return false;
|
||||
if (Updater.get().getStatus() == Updater.Status.UPDATE_DOWNLOADING &&
|
||||
UI.showExitConfirmation(Updater.EXIT_CONFIRMATION))
|
||||
UI.showExitConfirmation(Updater.EXIT_CONFIRMATION))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
@@ -248,7 +282,7 @@ public class Opsu extends StateBasedGame {
|
||||
// JARs will not run properly inside directories containing '!'
|
||||
// http://bugs.java.com/view_bug.do?bug_id=4523159
|
||||
if (Utils.isJarRunning() && Utils.getRunningDirectory() != null &&
|
||||
Utils.getRunningDirectory().getAbsolutePath().indexOf('!') != -1)
|
||||
Utils.getRunningDirectory().getAbsolutePath().indexOf('!') != -1)
|
||||
ErrorHandler.error("JARs cannot be run from some paths containing '!'. Please move or rename the file and try again.", null, false);
|
||||
else
|
||||
ErrorHandler.error(message, e, true);
|
||||
|
||||
@@ -22,6 +22,7 @@ import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||
import itdelatrisu.opsu.skins.Skin;
|
||||
import itdelatrisu.opsu.skins.SkinLoader;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@@ -37,6 +38,9 @@ import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.lwjgl.input.Keyboard;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
@@ -51,12 +55,18 @@ import org.newdawn.slick.util.ResourceLoader;
|
||||
* Handles all user options.
|
||||
*/
|
||||
public class Options {
|
||||
/** Whether to use XDG directories. */
|
||||
public static final boolean USE_XDG = checkXDGFlag();
|
||||
|
||||
/** The config directory. */
|
||||
private static final File CONFIG_DIR = getXDGBaseDir("XDG_CONFIG_HOME", ".config");
|
||||
|
||||
/** The data directory. */
|
||||
private static final File DATA_DIR = getXDGBaseDir("XDG_DATA_HOME", ".local/share");
|
||||
|
||||
/** The cache directory. */
|
||||
private static final File CACHE_DIR = getXDGBaseDir("XDG_CACHE_HOME", ".cache");
|
||||
|
||||
/** File for logging errors. */
|
||||
public static final File LOG_FILE = new File(CONFIG_DIR, ".opsu.log");
|
||||
|
||||
@@ -83,6 +93,9 @@ public class Options {
|
||||
/** Score database name. */
|
||||
public static final File SCORE_DB = new File(DATA_DIR, ".opsu_scores.db");
|
||||
|
||||
/** Directory where natives are unpacked. */
|
||||
public static final File NATIVE_DIR = new File(CACHE_DIR, "Natives/");
|
||||
|
||||
/** Font file name. */
|
||||
public static final String FONT_NAME = "DroidSansFallback.ttf";
|
||||
|
||||
@@ -119,15 +132,35 @@ public class Options {
|
||||
/** Port binding. */
|
||||
private static int port = 49250;
|
||||
|
||||
/**
|
||||
* Returns whether the XDG flag in the manifest (if any) is set to "true".
|
||||
* @return true if XDG directories are enabled, false otherwise
|
||||
*/
|
||||
private static boolean checkXDGFlag() {
|
||||
JarFile jarFile = Utils.getJarFile();
|
||||
if (jarFile == null)
|
||||
return false;
|
||||
try {
|
||||
Manifest manifest = jarFile.getManifest();
|
||||
if (manifest == null)
|
||||
return false;
|
||||
Attributes attributes = manifest.getMainAttributes();
|
||||
String value = attributes.getValue("Use-XDG");
|
||||
return (value != null && value.equalsIgnoreCase("true"));
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory based on the XDG base directory specification for
|
||||
* Unix-like operating systems, only if the system property "XDG" has been defined.
|
||||
* Unix-like operating systems, only if the "XDG" flag is enabled.
|
||||
* @param env the environment variable to check (XDG_*_*)
|
||||
* @param fallback the fallback directory relative to ~home
|
||||
* @return the XDG base directory, or the working directory if unavailable
|
||||
*/
|
||||
private static File getXDGBaseDir(String env, String fallback) {
|
||||
if (System.getProperty("XDG") == null)
|
||||
if (!USE_XDG)
|
||||
return new File("./");
|
||||
|
||||
String OS = System.getProperty("os.name").toLowerCase();
|
||||
@@ -140,8 +173,8 @@ public class Options {
|
||||
rootPath = String.format("%s/%s", home, fallback);
|
||||
}
|
||||
File dir = new File(rootPath, "opsu");
|
||||
if (!dir.isDirectory())
|
||||
dir.mkdir();
|
||||
if (!dir.isDirectory() && !dir.mkdir())
|
||||
ErrorHandler.error(String.format("Failed to create configuration folder at '%s/opsu'.", rootPath), null, false);
|
||||
return dir;
|
||||
} else
|
||||
return new File("./");
|
||||
@@ -212,7 +245,7 @@ public class Options {
|
||||
@Override
|
||||
public void read(String s) {
|
||||
int i = Integer.parseInt(s);
|
||||
if (i > 0 && i < 65535)
|
||||
if (i > 0 && i <= 65535)
|
||||
port = i;
|
||||
}
|
||||
},
|
||||
@@ -287,9 +320,9 @@ public class Options {
|
||||
super.click(container);
|
||||
if (bool) {
|
||||
try {
|
||||
Utils.FONT_LARGE.loadGlyphs();
|
||||
Utils.FONT_MEDIUM.loadGlyphs();
|
||||
Utils.FONT_DEFAULT.loadGlyphs();
|
||||
Fonts.LARGE.loadGlyphs();
|
||||
Fonts.MEDIUM.loadGlyphs();
|
||||
Fonts.DEFAULT.loadGlyphs();
|
||||
} catch (SlickException e) {
|
||||
Log.warn("Failed to load glyphs.", e);
|
||||
}
|
||||
@@ -357,7 +390,7 @@ public class Options {
|
||||
public String getValueString() { return String.format("%dms", val); }
|
||||
},
|
||||
DISABLE_SOUNDS ("Disable All Sound Effects", "DisableSound", "May resolve Linux sound driver issues. Requires a restart.",
|
||||
(System.getProperty("os.name").toLowerCase().indexOf("linux") > -1)),
|
||||
(System.getProperty("os.name").toLowerCase().contains("linux"))),
|
||||
KEY_LEFT ("Left Game Key", "keyOsuLeft", "Select this option to input a key.") {
|
||||
@Override
|
||||
public String getValueString() { return Keyboard.getKeyName(getGameKeyLeft()); }
|
||||
@@ -380,6 +413,7 @@ public class Options {
|
||||
},
|
||||
DISABLE_MOUSE_WHEEL ("Disable mouse wheel in play mode", "MouseDisableWheel", "During play, you can use the mouse wheel to adjust the volume and pause the game.\nThis will disable that functionality.", false),
|
||||
DISABLE_MOUSE_BUTTONS ("Disable mouse buttons in play mode", "MouseDisableButtons", "This option will disable all mouse buttons.\nSpecifically for people who use their keyboard to click.", false),
|
||||
DISABLE_CURSOR ("Disable Cursor", "DisableCursor", "Hide the cursor sprite.", false),
|
||||
BACKGROUND_DIM ("Background Dim", "DimLevel", "Percentage to dim the background image during gameplay.", 50, 0, 100),
|
||||
FORCE_DEFAULT_PLAYFIELD ("Force Default Playfield", "ForceDefaultPlayfield", "Override the song background with the default playfield background.", false),
|
||||
IGNORE_BEATMAP_SKINS ("Ignore All Beatmap Skins", "IgnoreBeatmapSkins", "Never use skin element overrides provided by beatmaps.", false),
|
||||
@@ -453,16 +487,19 @@ public class Options {
|
||||
val - TimeUnit.MINUTES.toSeconds(TimeUnit.SECONDS.toMinutes(val)));
|
||||
}
|
||||
},
|
||||
ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true);
|
||||
ENABLE_THEME_SONG ("Enable Theme Song", "MenuMusic", "Whether to play the theme song upon starting opsu!", true),
|
||||
REPLAY_SEEKING ("Replay Seeking", "ReplaySeeking", "Enable a seeking bar on the left side of the screen during replays.", false),
|
||||
DISABLE_UPDATER ("Disable Automatic Updates", "DisableUpdater", "Disable automatic checking for updates upon starting opsu!.", false),
|
||||
ENABLE_WATCH_SERVICE ("Enable Watch Service", "WatchService", "Watch the beatmap directory for changes. Requires a restart.", false);
|
||||
|
||||
/** Option name. */
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
/** Option name, as displayed in the configuration file. */
|
||||
private String displayName;
|
||||
private final String displayName;
|
||||
|
||||
/** Option description. */
|
||||
private String description;
|
||||
private final String description;
|
||||
|
||||
/** The boolean value for the option (if applicable). */
|
||||
protected boolean bool;
|
||||
@@ -484,7 +521,7 @@ public class Options {
|
||||
* @param displayName the option name, as displayed in the configuration file
|
||||
*/
|
||||
GameOption(String displayName) {
|
||||
this.displayName = displayName;
|
||||
this(null, displayName, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -604,7 +641,7 @@ public class Options {
|
||||
*/
|
||||
public void drag(GameContainer container, int d) {
|
||||
if (type == OptionType.NUMERIC)
|
||||
val = Utils.getBoundedValue(val, d, min, max);
|
||||
val = Utils.clamp(val + d, min, max);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -958,6 +995,24 @@ public class Options {
|
||||
*/
|
||||
public static boolean isThemeSongEnabled() { return GameOption.ENABLE_THEME_SONG.getBooleanValue(); }
|
||||
|
||||
/**
|
||||
* Returns whether or not replay seeking is enabled.
|
||||
* @return true if enabled
|
||||
*/
|
||||
public static boolean isReplaySeekingEnabled() { return GameOption.REPLAY_SEEKING.getBooleanValue(); }
|
||||
|
||||
/**
|
||||
* Returns whether or not automatic checking for updates is disabled.
|
||||
* @return true if disabled
|
||||
*/
|
||||
public static boolean isUpdaterDisabled() { return GameOption.DISABLE_UPDATER.getBooleanValue(); }
|
||||
|
||||
/**
|
||||
* Returns whether or not the beatmap watch service is enabled.
|
||||
* @return true if enabled
|
||||
*/
|
||||
public static boolean isWatchServiceEnabled() { return GameOption.ENABLE_WATCH_SERVICE.getBooleanValue(); }
|
||||
|
||||
/**
|
||||
* Sets the track checkpoint time, if within bounds.
|
||||
* @param time the track position (in ms)
|
||||
@@ -1005,6 +1060,12 @@ public class Options {
|
||||
"Mouse buttons are disabled." : "Mouse buttons are enabled.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the cursor sprite should be hidden.
|
||||
* @return true if disabled
|
||||
*/
|
||||
public static boolean isCursorDisabled() { return GameOption.DISABLE_CURSOR.getBooleanValue(); }
|
||||
|
||||
/**
|
||||
* Returns the left game key.
|
||||
* @return the left key code
|
||||
@@ -1080,7 +1141,10 @@ public class Options {
|
||||
if (beatmapDir.isDirectory())
|
||||
return beatmapDir;
|
||||
}
|
||||
beatmapDir.mkdir(); // none found, create new directory
|
||||
|
||||
// none found, create new directory
|
||||
if (!beatmapDir.mkdir())
|
||||
ErrorHandler.error(String.format("Failed to create beatmap directory at '%s'.", beatmapDir.getAbsolutePath()), null, false);
|
||||
return beatmapDir;
|
||||
}
|
||||
|
||||
@@ -1094,7 +1158,8 @@ public class Options {
|
||||
return oszDir;
|
||||
|
||||
oszDir = new File(DATA_DIR, "SongPacks/");
|
||||
oszDir.mkdir();
|
||||
if (!oszDir.isDirectory() && !oszDir.mkdir())
|
||||
ErrorHandler.error(String.format("Failed to create song packs directory at '%s'.", oszDir.getAbsolutePath()), null, false);
|
||||
return oszDir;
|
||||
}
|
||||
|
||||
@@ -1108,7 +1173,8 @@ public class Options {
|
||||
return replayImportDir;
|
||||
|
||||
replayImportDir = new File(DATA_DIR, "ReplayImport/");
|
||||
replayImportDir.mkdir();
|
||||
if (!replayImportDir.isDirectory() && !replayImportDir.mkdir())
|
||||
ErrorHandler.error(String.format("Failed to create replay import directory at '%s'.", replayImportDir.getAbsolutePath()), null, false);
|
||||
return replayImportDir;
|
||||
}
|
||||
|
||||
@@ -1153,7 +1219,10 @@ public class Options {
|
||||
if (skinRootDir.isDirectory())
|
||||
return skinRootDir;
|
||||
}
|
||||
skinRootDir.mkdir(); // none found, create new directory
|
||||
|
||||
// none found, create new directory
|
||||
if (!skinRootDir.mkdir())
|
||||
ErrorHandler.error(String.format("Failed to create skins directory at '%s'.", skinRootDir.getAbsolutePath()), null, false);
|
||||
return skinRootDir;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ package itdelatrisu.opsu;
|
||||
|
||||
import itdelatrisu.opsu.GameData.Grade;
|
||||
import itdelatrisu.opsu.states.SongMenu;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
@@ -84,11 +87,6 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||
/** Drawing values. */
|
||||
private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset, buttonAreaHeight;
|
||||
|
||||
/** Button background colors. */
|
||||
private static final Color
|
||||
BG_NORMAL = new Color(0, 0, 0, 0.25f),
|
||||
BG_FOCUS = new Color(0, 0, 0, 0.75f);
|
||||
|
||||
/**
|
||||
* Initializes the base coordinates for drawing.
|
||||
* @param containerWidth the container width
|
||||
@@ -99,7 +97,7 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||
baseY = topY;
|
||||
buttonWidth = containerWidth * 0.4f;
|
||||
float gradeHeight = GameImage.MENU_BUTTON_BG.getImage().getHeight() * 0.45f;
|
||||
buttonHeight = Math.max(gradeHeight, Utils.FONT_DEFAULT.getLineHeight() * 3.03f);
|
||||
buttonHeight = Math.max(gradeHeight, Fonts.DEFAULT.getLineHeight() * 3.03f);
|
||||
buttonOffset = buttonHeight + gradeHeight / 10f;
|
||||
buttonAreaHeight = (SongMenu.MAX_SCORE_BUTTONS - 1) * buttonOffset + buttonHeight;
|
||||
}
|
||||
@@ -155,14 +153,14 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty constructor.
|
||||
* Creates an empty score data object.
|
||||
*/
|
||||
public ScoreData() {}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param rs the ResultSet to read from (at the current cursor position)
|
||||
* @throws SQLException
|
||||
* Builds a score data object from a database result set.
|
||||
* @param rs the {@link ResultSet} to read from (at the current cursor position)
|
||||
* @throws SQLException if a database access error occurs or the result set is closed
|
||||
*/
|
||||
public ScoreData(ResultSet rs) throws SQLException {
|
||||
this.timestamp = rs.getLong(1);
|
||||
@@ -244,46 +242,52 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||
* @param rank the score rank
|
||||
* @param prevScore the previous (lower) score, or -1 if none
|
||||
* @param focus whether the button is focused
|
||||
* @param t the animation progress [0,1]
|
||||
*/
|
||||
public void draw(Graphics g, float position, int rank, long prevScore, boolean focus) {
|
||||
Image img = getGrade().getMenuImage();
|
||||
float textX = baseX + buttonWidth * 0.24f;
|
||||
float edgeX = baseX + buttonWidth * 0.98f;
|
||||
public void draw(Graphics g, float position, int rank, long prevScore, boolean focus, float t) {
|
||||
float x = baseX - buttonWidth * (1 - AnimationEquation.OUT_BACK.calc(t)) / 2.5f;
|
||||
float textX = x + buttonWidth * 0.24f;
|
||||
float edgeX = x + buttonWidth * 0.98f;
|
||||
float y = baseY + position;
|
||||
float midY = y + buttonHeight / 2f;
|
||||
float marginY = Utils.FONT_DEFAULT.getLineHeight() * 0.01f;
|
||||
float marginY = Fonts.DEFAULT.getLineHeight() * 0.01f;
|
||||
Color c = Colors.WHITE_FADE;
|
||||
float alpha = t;
|
||||
float oldAlpha = c.a;
|
||||
c.a = alpha;
|
||||
|
||||
// rectangle outline
|
||||
g.setColor((focus) ? BG_FOCUS : BG_NORMAL);
|
||||
g.fillRect(baseX, y, buttonWidth, buttonHeight);
|
||||
Color rectColor = (focus) ? Colors.BLACK_BG_FOCUS : Colors.BLACK_BG_NORMAL;
|
||||
float oldRectAlpha = rectColor.a;
|
||||
rectColor.a *= AnimationEquation.IN_QUAD.calc(alpha);
|
||||
g.setColor(rectColor);
|
||||
g.fillRect(x, y, buttonWidth, buttonHeight);
|
||||
rectColor.a = oldRectAlpha;
|
||||
|
||||
// rank
|
||||
if (focus) {
|
||||
Utils.FONT_LARGE.drawString(
|
||||
baseX + buttonWidth * 0.04f,
|
||||
y + (buttonHeight - Utils.FONT_LARGE.getLineHeight()) / 2f,
|
||||
Integer.toString(rank + 1), Color.white
|
||||
Fonts.LARGE.drawString(
|
||||
x + buttonWidth * 0.04f,
|
||||
y + (buttonHeight - Fonts.LARGE.getLineHeight()) / 2f,
|
||||
Integer.toString(rank + 1), c
|
||||
);
|
||||
}
|
||||
|
||||
// grade image
|
||||
img.drawCentered(baseX + buttonWidth * 0.15f, midY);
|
||||
Image img = getGrade().getMenuImage();
|
||||
img.setAlpha(alpha);
|
||||
img.drawCentered(x + buttonWidth * 0.15f, midY);
|
||||
img.setAlpha(1f);
|
||||
|
||||
// score
|
||||
float textOffset = (buttonHeight - Utils.FONT_MEDIUM.getLineHeight() - Utils.FONT_SMALL.getLineHeight()) / 2f;
|
||||
Utils.FONT_MEDIUM.drawString(
|
||||
textX, y + textOffset,
|
||||
String.format("Score: %s (%dx)", NumberFormat.getNumberInstance().format(score), combo),
|
||||
Color.white
|
||||
);
|
||||
float textOffset = (buttonHeight - Fonts.MEDIUM.getLineHeight() - Fonts.SMALL.getLineHeight()) / 2f;
|
||||
Fonts.MEDIUM.drawString(textX, y + textOffset,
|
||||
String.format("Score: %s (%dx)", NumberFormat.getNumberInstance().format(score), combo), c);
|
||||
|
||||
// hit counts (custom: osu! shows user instead, above score)
|
||||
String player = (playerName == null) ? "" : String.format(" (%s)", playerName);
|
||||
Utils.FONT_SMALL.drawString(
|
||||
textX, y + textOffset + Utils.FONT_MEDIUM.getLineHeight(),
|
||||
String.format("300:%d 100:%d 50:%d Miss:%d%s", hit300, hit100, hit50, miss, player),
|
||||
Color.white
|
||||
);
|
||||
Fonts.SMALL.drawString(textX, y + textOffset + Fonts.MEDIUM.getLineHeight(),
|
||||
String.format("300:%d 100:%d 50:%d Miss:%d%s", hit300, hit100, hit50, miss, player), c);
|
||||
|
||||
// mods
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -296,39 +300,30 @@ public class ScoreData implements Comparable<ScoreData> {
|
||||
if (sb.length() > 0) {
|
||||
sb.setLength(sb.length() - 1);
|
||||
String modString = sb.toString();
|
||||
Utils.FONT_DEFAULT.drawString(
|
||||
edgeX - Utils.FONT_DEFAULT.getWidth(modString),
|
||||
y + marginY, modString, Color.white
|
||||
);
|
||||
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(modString), y + marginY, modString, c);
|
||||
}
|
||||
|
||||
// accuracy
|
||||
String accuracy = String.format("%.2f%%", getScorePercent());
|
||||
Utils.FONT_DEFAULT.drawString(
|
||||
edgeX - Utils.FONT_DEFAULT.getWidth(accuracy),
|
||||
y + marginY + Utils.FONT_DEFAULT.getLineHeight(),
|
||||
accuracy, Color.white
|
||||
);
|
||||
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(accuracy), y + marginY + Fonts.DEFAULT.getLineHeight(), accuracy, c);
|
||||
|
||||
// score difference
|
||||
String diff = (prevScore < 0 || score < prevScore) ?
|
||||
"-" : String.format("+%s", NumberFormat.getNumberInstance().format(score - prevScore));
|
||||
Utils.FONT_DEFAULT.drawString(
|
||||
edgeX - Utils.FONT_DEFAULT.getWidth(diff),
|
||||
y + marginY + Utils.FONT_DEFAULT.getLineHeight() * 2,
|
||||
diff, Color.white
|
||||
);
|
||||
Fonts.DEFAULT.drawString(edgeX - Fonts.DEFAULT.getWidth(diff), y + marginY + Fonts.DEFAULT.getLineHeight() * 2, diff, c);
|
||||
|
||||
// time since
|
||||
if (getTimeSince() != null) {
|
||||
Image clock = GameImage.HISTORY.getImage();
|
||||
clock.drawCentered(baseX + buttonWidth * 1.02f + clock.getWidth() / 2f, midY);
|
||||
Utils.FONT_DEFAULT.drawString(
|
||||
baseX + buttonWidth * 1.03f + clock.getWidth(),
|
||||
midY - Utils.FONT_DEFAULT.getLineHeight() / 2f,
|
||||
getTimeSince(), Color.white
|
||||
clock.drawCentered(x + buttonWidth * 1.02f + clock.getWidth() / 2f, midY);
|
||||
Fonts.DEFAULT.drawString(
|
||||
x + buttonWidth * 1.03f + clock.getWidth(),
|
||||
midY - Fonts.DEFAULT.getLineHeight() / 2f,
|
||||
getTimeSince(), c
|
||||
);
|
||||
}
|
||||
|
||||
c.a = oldAlpha;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,14 +24,15 @@ import itdelatrisu.opsu.beatmap.HitObject;
|
||||
import itdelatrisu.opsu.downloads.Download;
|
||||
import itdelatrisu.opsu.downloads.DownloadNode;
|
||||
import itdelatrisu.opsu.replay.PlaybackSpeed;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
|
||||
import java.awt.Font;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
@@ -43,13 +44,10 @@ import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
@@ -60,16 +58,10 @@ import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.Display;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.newdawn.slick.Animation;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.UnicodeFont;
|
||||
import org.newdawn.slick.font.effects.ColorEffect;
|
||||
import org.newdawn.slick.font.effects.Effect;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
import org.newdawn.slick.util.Log;
|
||||
import org.newdawn.slick.util.ResourceLoader;
|
||||
|
||||
import com.sun.jna.platform.FileUtils;
|
||||
|
||||
@@ -77,34 +69,6 @@ import com.sun.jna.platform.FileUtils;
|
||||
* Contains miscellaneous utilities.
|
||||
*/
|
||||
public class Utils {
|
||||
/** Game colors. */
|
||||
public static final Color
|
||||
COLOR_BLACK_ALPHA = new Color(0, 0, 0, 0.5f),
|
||||
COLOR_WHITE_ALPHA = new Color(255, 255, 255, 0.5f),
|
||||
COLOR_BLUE_DIVIDER = new Color(49, 94, 237),
|
||||
COLOR_BLUE_BACKGROUND = new Color(74, 130, 255),
|
||||
COLOR_BLUE_BUTTON = new Color(40, 129, 237),
|
||||
COLOR_ORANGE_BUTTON = new Color(200, 90, 3),
|
||||
COLOR_YELLOW_ALPHA = new Color(255, 255, 0, 0.4f),
|
||||
COLOR_WHITE_FADE = new Color(255, 255, 255, 1f),
|
||||
COLOR_RED_HOVER = new Color(255, 112, 112),
|
||||
COLOR_GREEN = new Color(137, 201, 79),
|
||||
COLOR_LIGHT_ORANGE = new Color(255,192,128),
|
||||
COLOR_LIGHT_GREEN = new Color(128,255,128),
|
||||
COLOR_LIGHT_BLUE = new Color(128,128,255),
|
||||
COLOR_GREEN_SEARCH = new Color(173, 255, 47),
|
||||
COLOR_DARK_GRAY = new Color(0.3f, 0.3f, 0.3f, 1f),
|
||||
COLOR_RED_HIGHLIGHT = new Color(246, 154, 161),
|
||||
COLOR_BLUE_HIGHLIGHT = new Color(173, 216, 230);
|
||||
|
||||
/** Game fonts. */
|
||||
public static UnicodeFont
|
||||
FONT_DEFAULT, FONT_BOLD,
|
||||
FONT_XLARGE, FONT_LARGE, FONT_MEDIUM, FONT_SMALL;
|
||||
|
||||
/** Set of all Unicode strings already loaded per font. */
|
||||
private static HashMap<UnicodeFont, HashSet<String>> loadedGlyphs = new HashMap<UnicodeFont, HashSet<String>>();
|
||||
|
||||
/**
|
||||
* List of illegal filename characters.
|
||||
* @see #cleanFileName(String, char)
|
||||
@@ -115,7 +79,7 @@ public class Utils {
|
||||
24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47
|
||||
};
|
||||
static {
|
||||
Arrays.sort(illegalChars);
|
||||
Arrays.sort(illegalChars);
|
||||
}
|
||||
|
||||
// game-related variables
|
||||
@@ -128,10 +92,8 @@ public class Utils {
|
||||
* Initializes game settings and class data.
|
||||
* @param container the game container
|
||||
* @param game the game object
|
||||
* @throws SlickException
|
||||
*/
|
||||
public static void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
public static void init(GameContainer container, StateBasedGame game) {
|
||||
input = container.getInput();
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
@@ -149,23 +111,8 @@ public class Utils {
|
||||
GameImage.init(width, height);
|
||||
|
||||
// create fonts
|
||||
float fontBase = 12f * GameImage.getUIscale();
|
||||
try {
|
||||
Font javaFont = Font.createFont(Font.TRUETYPE_FONT, ResourceLoader.getResourceAsStream(Options.FONT_NAME));
|
||||
Font font = javaFont.deriveFont(Font.PLAIN, (int) (fontBase * 4 / 3));
|
||||
FONT_DEFAULT = new UnicodeFont(font);
|
||||
FONT_BOLD = new UnicodeFont(font.deriveFont(Font.BOLD));
|
||||
FONT_XLARGE = new UnicodeFont(font.deriveFont(fontBase * 3));
|
||||
FONT_LARGE = new UnicodeFont(font.deriveFont(fontBase * 2));
|
||||
FONT_MEDIUM = new UnicodeFont(font.deriveFont(fontBase * 3 / 2));
|
||||
FONT_SMALL = new UnicodeFont(font.deriveFont(fontBase));
|
||||
ColorEffect colorEffect = new ColorEffect();
|
||||
loadFont(FONT_DEFAULT, colorEffect);
|
||||
loadFont(FONT_BOLD, colorEffect);
|
||||
loadFont(FONT_XLARGE, colorEffect);
|
||||
loadFont(FONT_LARGE, colorEffect);
|
||||
loadFont(FONT_MEDIUM, colorEffect);
|
||||
loadFont(FONT_SMALL, colorEffect);
|
||||
Fonts.init();
|
||||
} catch (Exception e) {
|
||||
ErrorHandler.error("Failed to load fonts.", e, true);
|
||||
}
|
||||
@@ -206,36 +153,18 @@ public class Utils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bounded value for a base value and displacement.
|
||||
* @param base the initial value
|
||||
* @param diff the value change
|
||||
* @param min the minimum value
|
||||
* @param max the maximum value
|
||||
* @return the bounded value
|
||||
* Clamps a value between a lower and upper bound.
|
||||
* @param val the value to clamp
|
||||
* @param low the lower bound
|
||||
* @param high the upper bound
|
||||
* @return the clamped value
|
||||
* @author fluddokt
|
||||
*/
|
||||
public static int getBoundedValue(int base, int diff, int min, int max) {
|
||||
int val = base + diff;
|
||||
if (val < min)
|
||||
val = min;
|
||||
else if (val > max)
|
||||
val = max;
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a bounded value for a base value and displacement.
|
||||
* @param base the initial value
|
||||
* @param diff the value change
|
||||
* @param min the minimum value
|
||||
* @param max the maximum value
|
||||
* @return the bounded value
|
||||
*/
|
||||
public static float getBoundedValue(float base, float diff, float min, float max) {
|
||||
float val = base + diff;
|
||||
if (val < min)
|
||||
val = min;
|
||||
else if (val > max)
|
||||
val = max;
|
||||
public static int clamp(int val, int low, int high) {
|
||||
if (val < low)
|
||||
return low;
|
||||
if (val > high)
|
||||
return high;
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -269,6 +198,13 @@ public class Utils {
|
||||
return (float) Math.sqrt((v1 * v1) + (v2 * v2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Linear interpolation of a and b at t.
|
||||
*/
|
||||
public static float lerp(float a, float b, float t) {
|
||||
return a * (1 - t) + b * t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a game input key is pressed (mouse/keyboard left/right).
|
||||
* @return true if pressed
|
||||
@@ -289,11 +225,9 @@ public class Utils {
|
||||
public static void takeScreenShot() {
|
||||
// create the screenshot directory
|
||||
File dir = Options.getScreenshotDir();
|
||||
if (!dir.isDirectory()) {
|
||||
if (!dir.mkdir()) {
|
||||
ErrorHandler.error("Failed to create screenshot directory.", null, false);
|
||||
return;
|
||||
}
|
||||
if (!dir.isDirectory() && !dir.mkdir()) {
|
||||
ErrorHandler.error(String.format("Failed to create screenshot directory at '%s'.", dir.getAbsolutePath()), null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// create file name
|
||||
@@ -333,54 +267,6 @@ public class Utils {
|
||||
}.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a Unicode font.
|
||||
* @param font the font to load
|
||||
* @param effect the font effect
|
||||
* @throws SlickException
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void loadFont(UnicodeFont font, Effect effect) throws SlickException {
|
||||
font.addAsciiGlyphs();
|
||||
font.getEffects().add(effect);
|
||||
font.loadGlyphs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds and loads glyphs for a beatmap's Unicode title and artist strings.
|
||||
* @param font the font to add the glyphs to
|
||||
* @param title the title string
|
||||
* @param artist the artist string
|
||||
*/
|
||||
public static void loadGlyphs(UnicodeFont font, String title, String artist) {
|
||||
// get set of added strings
|
||||
HashSet<String> set = loadedGlyphs.get(font);
|
||||
if (set == null) {
|
||||
set = new HashSet<String>();
|
||||
loadedGlyphs.put(font, set);
|
||||
}
|
||||
|
||||
// add glyphs if not in set
|
||||
boolean glyphsAdded = false;
|
||||
if (title != null && !title.isEmpty() && !set.contains(title)) {
|
||||
font.addGlyphs(title);
|
||||
set.add(title);
|
||||
glyphsAdded = true;
|
||||
}
|
||||
if (artist != null && !artist.isEmpty() && !set.contains(artist)) {
|
||||
font.addGlyphs(artist);
|
||||
set.add(artist);
|
||||
glyphsAdded = true;
|
||||
}
|
||||
if (glyphsAdded) {
|
||||
try {
|
||||
font.loadGlyphs();
|
||||
} catch (SlickException e) {
|
||||
Log.warn("Failed to load glyphs.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable representation of a given number of bytes.
|
||||
* @param bytes the number of bytes
|
||||
@@ -407,15 +293,15 @@ public class Utils {
|
||||
return null;
|
||||
|
||||
boolean doReplace = (replace > 0 && Arrays.binarySearch(illegalChars, replace) < 0);
|
||||
StringBuilder cleanName = new StringBuilder();
|
||||
for (int i = 0, n = badFileName.length(); i < n; i++) {
|
||||
int c = badFileName.charAt(i);
|
||||
if (Arrays.binarySearch(illegalChars, c) < 0)
|
||||
cleanName.append((char) c);
|
||||
else if (doReplace)
|
||||
cleanName.append(replace);
|
||||
}
|
||||
return cleanName.toString();
|
||||
StringBuilder cleanName = new StringBuilder();
|
||||
for (int i = 0, n = badFileName.length(); i < n; i++) {
|
||||
int c = badFileName.charAt(i);
|
||||
if (Arrays.binarySearch(illegalChars, c) < 0)
|
||||
cleanName.append((char) c);
|
||||
else if (doReplace)
|
||||
cleanName.append(replace);
|
||||
}
|
||||
return cleanName.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -474,50 +360,12 @@ public class Utils {
|
||||
dir.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the given string into a list of split lines based on the width.
|
||||
* @param text the text to split
|
||||
* @param font the font used to draw the string
|
||||
* @param width the maximum width of a line
|
||||
* @return the list of split strings
|
||||
* @author davedes (http://slick.ninjacave.com/forum/viewtopic.php?t=3778)
|
||||
*/
|
||||
public static List<String> wrap(String text, org.newdawn.slick.Font font, int width) {
|
||||
List<String> list = new ArrayList<String>();
|
||||
String str = text;
|
||||
String line = "";
|
||||
int i = 0;
|
||||
int lastSpace = -1;
|
||||
while (i < str.length()) {
|
||||
char c = str.charAt(i);
|
||||
if (Character.isWhitespace(c))
|
||||
lastSpace = i;
|
||||
String append = line + c;
|
||||
if (font.getWidth(append) > width) {
|
||||
int split = (lastSpace != -1) ? lastSpace : i;
|
||||
int splitTrimmed = split;
|
||||
if (lastSpace != -1 && split < str.length() - 1)
|
||||
splitTrimmed++;
|
||||
list.add(str.substring(0, split));
|
||||
str = str.substring(splitTrimmed);
|
||||
line = "";
|
||||
i = 0;
|
||||
lastSpace = -1;
|
||||
} else {
|
||||
line = append;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (str.length() != 0)
|
||||
list.add(str);
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a the contents of a URL as a string.
|
||||
* @param url the remote URL
|
||||
* @return the contents as a string, or null if any error occurred
|
||||
* @author Roland Illig (http://stackoverflow.com/a/4308662)
|
||||
* @throws IOException if an I/O exception occurs
|
||||
*/
|
||||
public static String readDataFromUrl(URL url) throws IOException {
|
||||
// open connection
|
||||
@@ -553,6 +401,7 @@ public class Utils {
|
||||
* Returns a JSON object from a URL.
|
||||
* @param url the remote URL
|
||||
* @return the JSON object, or null if an error occurred
|
||||
* @throws IOException if an I/O exception occurs
|
||||
*/
|
||||
public static JSONObject readJsonObjectFromUrl(URL url) throws IOException {
|
||||
String s = Utils.readDataFromUrl(url);
|
||||
@@ -571,6 +420,7 @@ public class Utils {
|
||||
* Returns a JSON array from a URL.
|
||||
* @param url the remote URL
|
||||
* @return the JSON array, or null if an error occurred
|
||||
* @throws IOException if an I/O exception occurs
|
||||
*/
|
||||
public static JSONArray readJsonArrayFromUrl(URL url) throws IOException {
|
||||
String s = Utils.readDataFromUrl(url);
|
||||
@@ -640,32 +490,6 @@ public class Utils {
|
||||
return String.format("%02d:%02d:%02d", seconds / 3600, (seconds / 60) % 60, seconds % 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cubic ease out function.
|
||||
* @param t the current time
|
||||
* @param a the starting position
|
||||
* @param b the finishing position
|
||||
* @param d the duration
|
||||
* @return the eased float
|
||||
*/
|
||||
public static float easeOut(float t, float a, float b, float d) {
|
||||
return b * ((t = t / d - 1f) * t * t + 1f) + a;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake bounce ease function.
|
||||
* @param t the current time
|
||||
* @param a the starting position
|
||||
* @param b the finishing position
|
||||
* @param d the duration
|
||||
* @return the eased float
|
||||
*/
|
||||
public static float easeBounce(float t, float a, float b, float d) {
|
||||
if (t < d / 2)
|
||||
return easeOut(t, a, b, d);
|
||||
return easeOut(d - t, a, b, d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the application is running within a JAR.
|
||||
* @return true if JAR, false if file
|
||||
@@ -674,8 +498,25 @@ public class Utils {
|
||||
return Opsu.class.getResource(String.format("%s.class", Opsu.class.getSimpleName())).toString().startsWith("jar:");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JarFile for the application.
|
||||
* @return the JAR file, or null if it could not be determined
|
||||
*/
|
||||
public static JarFile getJarFile() {
|
||||
if (!isJarRunning())
|
||||
return null;
|
||||
|
||||
try {
|
||||
return new JarFile(new File(Opsu.class.getProtectionDomain().getCodeSource().getLocation().toURI()), false);
|
||||
} catch (URISyntaxException | IOException e) {
|
||||
Log.error("Could not determine the JAR file.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory where the application is being run.
|
||||
* @return the directory, or null if it could not be determined
|
||||
*/
|
||||
public static File getRunningDirectory() {
|
||||
try {
|
||||
@@ -695,4 +536,29 @@ public class Utils {
|
||||
public static boolean parseBoolean(String s) {
|
||||
return (Integer.parseInt(s) == 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the git hash of the remote-tracking branch 'origin/master' from the
|
||||
* most recent update to the working directory (e.g. fetch or successful push).
|
||||
* @return the 40-character SHA-1 hash, or null if it could not be determined
|
||||
*/
|
||||
public static String getGitHash() {
|
||||
if (isJarRunning())
|
||||
return null;
|
||||
File f = new File(".git/refs/remotes/origin/master");
|
||||
if (!f.isFile())
|
||||
return null;
|
||||
try (BufferedReader in = new BufferedReader(new FileReader(f))) {
|
||||
char[] sha = new char[40];
|
||||
if (in.read(sha, 0, sha.length) < sha.length)
|
||||
return null;
|
||||
for (int i = 0; i < sha.length; i++) {
|
||||
if (Character.digit(sha[i], 16) == -1)
|
||||
return null;
|
||||
}
|
||||
return String.valueOf(sha);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,10 +40,10 @@ public enum HitSound implements SoundController.SoundComponent {
|
||||
// TAIKO ("taiko", 4);
|
||||
|
||||
/** The sample set name. */
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
/** The sample set index. */
|
||||
private int index;
|
||||
private final int index;
|
||||
|
||||
/** Total number of sample sets. */
|
||||
public static final int SIZE = values().length;
|
||||
@@ -77,7 +77,7 @@ public enum HitSound implements SoundController.SoundComponent {
|
||||
private static SampleSet currentDefaultSampleSet = SampleSet.NORMAL;
|
||||
|
||||
/** The file name. */
|
||||
private String filename;
|
||||
private final String filename;
|
||||
|
||||
/** The Clip associated with the hit sound. */
|
||||
private HashMap<SampleSet, MultiClip> clips;
|
||||
|
||||
@@ -49,12 +49,15 @@ public class MultiClip {
|
||||
private byte[] audioData;
|
||||
|
||||
/** The name given to this clip. */
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param name the clip name
|
||||
* @param audioIn the associated AudioInputStream
|
||||
* @throws IOException if an input or output error occurs
|
||||
* @throws LineUnavailableException if a clip object is not available or
|
||||
* if the line cannot be opened due to resource restrictions
|
||||
*/
|
||||
public MultiClip(String name, AudioInputStream audioIn) throws IOException, LineUnavailableException {
|
||||
this.name = name;
|
||||
@@ -105,6 +108,8 @@ public class MultiClip {
|
||||
* Plays the clip with the specified volume.
|
||||
* @param volume the volume the play at
|
||||
* @param listener the line listener
|
||||
* @throws LineUnavailableException if a clip object is not available or
|
||||
* if the line cannot be opened due to resource restrictions
|
||||
*/
|
||||
public void start(float volume, LineListener listener) throws LineUnavailableException {
|
||||
Clip clip = getClip();
|
||||
@@ -130,6 +135,8 @@ public class MultiClip {
|
||||
* If no clip is available, then a new one is created if under MAX_CLIPS.
|
||||
* Otherwise, an existing clip will be returned.
|
||||
* @return the Clip to play
|
||||
* @throws LineUnavailableException if a clip object is not available or
|
||||
* if the line cannot be opened due to resource restrictions
|
||||
*/
|
||||
private Clip getClip() throws LineUnavailableException {
|
||||
// TODO:
|
||||
|
||||
@@ -22,6 +22,7 @@ import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -41,6 +42,7 @@ import org.newdawn.slick.MusicListener;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.openal.Audio;
|
||||
import org.newdawn.slick.openal.SoundStore;
|
||||
import org.newdawn.slick.util.ResourceLoader;
|
||||
import org.tritonus.share.sampled.file.TAudioFileFormat;
|
||||
|
||||
/**
|
||||
@@ -87,6 +89,13 @@ public class MusicController {
|
||||
public static void play(final Beatmap beatmap, final boolean loop, final boolean preview) {
|
||||
// new track: load and play
|
||||
if (lastBeatmap == null || !beatmap.audioFilename.equals(lastBeatmap.audioFilename)) {
|
||||
final File audioFile = beatmap.audioFilename;
|
||||
if (!audioFile.isFile() && !ResourceLoader.resourceExists(audioFile.getPath())) {
|
||||
UI.sendBarNotification(String.format("Could not find track '%s'.", audioFile.getName()));
|
||||
System.out.println(beatmap);
|
||||
return;
|
||||
}
|
||||
|
||||
reset();
|
||||
System.gc();
|
||||
|
||||
@@ -96,7 +105,7 @@ public class MusicController {
|
||||
trackLoader = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
loadTrack(beatmap.audioFilename, (preview) ? beatmap.previewTime : 0, loop);
|
||||
loadTrack(audioFile, (preview) ? beatmap.previewTime : 0, loop);
|
||||
}
|
||||
};
|
||||
trackLoader.start();
|
||||
@@ -221,6 +230,7 @@ public class MusicController {
|
||||
|
||||
/**
|
||||
* Fades out the track.
|
||||
* @param duration the fade time (in ms)
|
||||
*/
|
||||
public static void fadeOut(int duration) {
|
||||
if (isPlaying())
|
||||
|
||||
@@ -59,6 +59,9 @@ public class SoundController {
|
||||
/** Sample volume multiplier, from timing points [0, 1]. */
|
||||
private static float sampleVolumeMultiplier = 1f;
|
||||
|
||||
/** Whether all sounds are muted. */
|
||||
private static boolean isMuted;
|
||||
|
||||
/** The name of the current sound file being loaded. */
|
||||
private static String currentFileName;
|
||||
|
||||
@@ -261,7 +264,7 @@ public class SoundController {
|
||||
if (clip == null) // clip failed to load properly
|
||||
return;
|
||||
|
||||
if (volume > 0f) {
|
||||
if (volume > 0f && !isMuted) {
|
||||
try {
|
||||
clip.start(volume, listener);
|
||||
} catch (LineUnavailableException e) {
|
||||
@@ -317,6 +320,12 @@ public class SoundController {
|
||||
playClip(s.getClip(), Options.getHitSoundVolume() * sampleVolumeMultiplier * Options.getMasterVolume(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes or unmutes all sounds (hit sounds and sound effects).
|
||||
* @param mute true to mute, false to unmute
|
||||
*/
|
||||
public static void mute(boolean mute) { isMuted = mute; }
|
||||
|
||||
/**
|
||||
* Returns the name of the current file being loaded, or null if none.
|
||||
*/
|
||||
|
||||
@@ -42,7 +42,7 @@ public enum SoundEffect implements SoundController.SoundComponent {
|
||||
SPINNERSPIN ("spinnerspin");
|
||||
|
||||
/** The file name. */
|
||||
private String filename;
|
||||
private final String filename;
|
||||
|
||||
/** The Clip associated with the sound effect. */
|
||||
private MultiClip clip;
|
||||
|
||||
@@ -23,6 +23,7 @@ import itdelatrisu.opsu.Options;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Map;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.Image;
|
||||
@@ -36,16 +37,37 @@ public class Beatmap implements Comparable<Beatmap> {
|
||||
public static final byte MODE_OSU = 0, MODE_TAIKO = 1, MODE_CTB = 2, MODE_MANIA = 3;
|
||||
|
||||
/** Background image cache. */
|
||||
private static final BeatmapImageCache bgImageCache = new BeatmapImageCache();
|
||||
@SuppressWarnings("serial")
|
||||
private static final LRUCache<File, ImageLoader> bgImageCache = new LRUCache<File, ImageLoader>(10) {
|
||||
@Override
|
||||
public void eldestRemoved(Map.Entry<File, ImageLoader> eldest) {
|
||||
if (eldest.getKey() == lastBG)
|
||||
lastBG = null;
|
||||
ImageLoader imageLoader = eldest.getValue();
|
||||
imageLoader.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
/** The last background image loaded. */
|
||||
private static File lastBG;
|
||||
|
||||
/**
|
||||
* Returns the background image cache.
|
||||
* Clears the background image cache.
|
||||
* <p>
|
||||
* NOTE: This does NOT destroy the images in the cache, and will cause
|
||||
* memory leaks if all images have not been destroyed.
|
||||
*/
|
||||
public static BeatmapImageCache getBackgroundImageCache() { return bgImageCache; }
|
||||
public static void clearBackgroundImageCache() { bgImageCache.clear(); }
|
||||
|
||||
/** The OSU File object associated with this beatmap. */
|
||||
private File file;
|
||||
|
||||
/** MD5 hash of this file. */
|
||||
public String md5Hash;
|
||||
|
||||
/** The star rating. */
|
||||
public double starRating = -1;
|
||||
|
||||
/**
|
||||
* [General]
|
||||
*/
|
||||
@@ -138,7 +160,7 @@ public class Beatmap implements Comparable<Beatmap> {
|
||||
public float HPDrainRate = 5f;
|
||||
|
||||
/** CS: Size of circles and sliders (0:large ~ 10:small). */
|
||||
public float circleSize = 4f;
|
||||
public float circleSize = 5f;
|
||||
|
||||
/** OD: Affects timing window, spinners, and approach speed (0:easy ~ 10:hard). */
|
||||
public float overallDifficulty = 5f;
|
||||
@@ -147,7 +169,7 @@ public class Beatmap implements Comparable<Beatmap> {
|
||||
public float approachRate = -1f;
|
||||
|
||||
/** Slider movement speed multiplier. */
|
||||
public float sliderMultiplier = 1f;
|
||||
public float sliderMultiplier = 1.4f;
|
||||
|
||||
/** Rate at which slider ticks are placed (x per beat). */
|
||||
public float sliderTickRate = 1f;
|
||||
@@ -185,9 +207,6 @@ public class Beatmap implements Comparable<Beatmap> {
|
||||
/** Slider border color. If null, the skin value is used. */
|
||||
public Color sliderBorder;
|
||||
|
||||
/** MD5 hash of this file. */
|
||||
public String md5Hash;
|
||||
|
||||
/**
|
||||
* [HitObjects]
|
||||
*/
|
||||
@@ -255,47 +274,74 @@ public class Beatmap implements Comparable<Beatmap> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the beatmap background.
|
||||
* Loads the beatmap background image.
|
||||
*/
|
||||
public void loadBackground() {
|
||||
if (bg == null || bgImageCache.containsKey(bg) || !bg.isFile())
|
||||
return;
|
||||
|
||||
if (lastBG != null) {
|
||||
ImageLoader lastImageLoader = bgImageCache.get(lastBG);
|
||||
if (lastImageLoader != null && lastImageLoader.isLoading()) {
|
||||
lastImageLoader.interrupt(); // only allow loading one image at a time
|
||||
bgImageCache.remove(lastBG);
|
||||
}
|
||||
}
|
||||
ImageLoader imageLoader = new ImageLoader(bg);
|
||||
bgImageCache.put(bg, imageLoader);
|
||||
imageLoader.load(true);
|
||||
lastBG = bg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the beatmap background image is currently loading.
|
||||
* @return true if loading
|
||||
*/
|
||||
public boolean isBackgroundLoading() {
|
||||
if (bg == null)
|
||||
return false;
|
||||
ImageLoader imageLoader = bgImageCache.get(bg);
|
||||
return (imageLoader != null && imageLoader.isLoading());
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the beatmap background image.
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
* @param alpha the alpha value
|
||||
* @param stretch if true, stretch to screen dimensions; otherwise, maintain aspect ratio
|
||||
* @return true if successful, false if any errors were produced
|
||||
*/
|
||||
public boolean drawBG(int width, int height, float alpha, boolean stretch) {
|
||||
public boolean drawBackground(int width, int height, float alpha, boolean stretch) {
|
||||
if (bg == null)
|
||||
return false;
|
||||
try {
|
||||
Image bgImage = bgImageCache.get(this);
|
||||
if (bgImage == null) {
|
||||
bgImage = new Image(bg.getAbsolutePath());
|
||||
bgImageCache.put(this, bgImage);
|
||||
}
|
||||
|
||||
int swidth = width;
|
||||
int sheight = height;
|
||||
if (!stretch) {
|
||||
// fit image to screen
|
||||
if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
|
||||
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
||||
else
|
||||
swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
|
||||
} else {
|
||||
// fill screen while maintaining aspect ratio
|
||||
if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
|
||||
swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
|
||||
else
|
||||
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
||||
}
|
||||
bgImage = bgImage.getScaledCopy(swidth, sheight);
|
||||
|
||||
bgImage.setAlpha(alpha);
|
||||
bgImage.drawCentered(width / 2, height / 2);
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to get background image '%s'.", bg), e);
|
||||
bg = null; // don't try to load the file again until a restart
|
||||
ImageLoader imageLoader = bgImageCache.get(bg);
|
||||
if (imageLoader == null)
|
||||
return false;
|
||||
|
||||
Image bgImage = imageLoader.getImage();
|
||||
if (bgImage == null)
|
||||
return true;
|
||||
|
||||
int swidth = width;
|
||||
int sheight = height;
|
||||
if (!stretch) {
|
||||
// fit image to screen
|
||||
if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
|
||||
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
||||
else
|
||||
swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
|
||||
} else {
|
||||
// fill screen while maintaining aspect ratio
|
||||
if (bgImage.getWidth() / (float) bgImage.getHeight() > width / (float) height) // x > y
|
||||
swidth = (int) (height * bgImage.getWidth() / (float) bgImage.getHeight());
|
||||
else
|
||||
sheight = (int) (width * bgImage.getHeight() / (float) bgImage.getWidth());
|
||||
}
|
||||
bgImage = bgImage.getScaledCopy(swidth, sheight);
|
||||
bgImage.setAlpha(alpha);
|
||||
bgImage.drawCentered(width / 2, height / 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
|
||||
import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.db.BeatmapDB;
|
||||
import itdelatrisu.opsu.io.MD5InputStreamWrapper;
|
||||
@@ -83,6 +84,10 @@ public class BeatmapParser {
|
||||
// create a new BeatmapSetList
|
||||
BeatmapSetList.create();
|
||||
|
||||
// create a new watch service
|
||||
if (Options.isWatchServiceEnabled())
|
||||
BeatmapWatchService.create();
|
||||
|
||||
// parse all directories
|
||||
parseDirectories(root.listFiles());
|
||||
}
|
||||
@@ -110,6 +115,9 @@ public class BeatmapParser {
|
||||
List<Beatmap> cachedBeatmaps = new LinkedList<Beatmap>(); // loaded from database
|
||||
List<Beatmap> parsedBeatmaps = new LinkedList<Beatmap>(); // loaded from parser
|
||||
|
||||
// watch service
|
||||
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
|
||||
|
||||
// parse directories
|
||||
BeatmapSetNode lastNode = null;
|
||||
for (File dir : dirs) {
|
||||
@@ -134,17 +142,19 @@ public class BeatmapParser {
|
||||
|
||||
// check if beatmap is cached
|
||||
String path = String.format("%s/%s", dir.getName(), file.getName());
|
||||
if (map.containsKey(path)) {
|
||||
// check last modified times
|
||||
long lastModified = map.get(path);
|
||||
if (lastModified == file.lastModified()) {
|
||||
// add to cached beatmap list
|
||||
Beatmap beatmap = new Beatmap(file);
|
||||
beatmaps.add(beatmap);
|
||||
cachedBeatmaps.add(beatmap);
|
||||
continue;
|
||||
} else
|
||||
BeatmapDB.delete(dir.getName(), file.getName());
|
||||
if (map != null) {
|
||||
Long lastModified = map.get(path);
|
||||
if (lastModified != null) {
|
||||
// check last modified times
|
||||
if (lastModified == file.lastModified()) {
|
||||
// add to cached beatmap list
|
||||
Beatmap beatmap = new Beatmap(file);
|
||||
beatmaps.add(beatmap);
|
||||
cachedBeatmaps.add(beatmap);
|
||||
continue;
|
||||
} else
|
||||
BeatmapDB.delete(dir.getName(), file.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// Parse hit objects only when needed to save time/memory.
|
||||
@@ -162,6 +172,8 @@ public class BeatmapParser {
|
||||
if (!beatmaps.isEmpty()) {
|
||||
beatmaps.trimToSize();
|
||||
allBeatmaps.add(beatmaps);
|
||||
if (ws != null)
|
||||
ws.registerAll(dir.toPath());
|
||||
}
|
||||
|
||||
// stop parsing files (interrupted)
|
||||
@@ -676,10 +688,15 @@ public class BeatmapParser {
|
||||
|
||||
beatmap.objects[objectIndex++] = hitObject;
|
||||
} catch (Exception e) {
|
||||
Log.warn(String.format("Failed to read hit object '%s' for Beatmap '%s'.",
|
||||
Log.warn(String.format("Failed to read hit object '%s' for beatmap '%s'.",
|
||||
line, beatmap.toString()), e);
|
||||
}
|
||||
}
|
||||
|
||||
// check that all objects were parsed
|
||||
if (objectIndex != beatmap.objects.length)
|
||||
ErrorHandler.error(String.format("Parsed %d objects for beatmap '%s', %d objects expected.",
|
||||
objectIndex, beatmap.toString(), beatmap.objects.length), null, true);
|
||||
} catch (IOException e) {
|
||||
ErrorHandler.error(String.format("Failed to read file '%s'.", beatmap.getFile().getAbsolutePath()), e, false);
|
||||
}
|
||||
@@ -711,6 +728,7 @@ public class BeatmapParser {
|
||||
|
||||
/**
|
||||
* Returns the file extension of a file.
|
||||
* @param file the file name
|
||||
*/
|
||||
public static String getExtension(String file) {
|
||||
int i = file.lastIndexOf('.');
|
||||
|
||||
@@ -21,14 +21,15 @@ package itdelatrisu.opsu.beatmap;
|
||||
import itdelatrisu.opsu.GameMod;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Data type containing all beatmaps in a beatmap set.
|
||||
*/
|
||||
public class BeatmapSet {
|
||||
public class BeatmapSet implements Iterable<Beatmap> {
|
||||
/** List of associated beatmaps. */
|
||||
private ArrayList<Beatmap> beatmaps;
|
||||
private final ArrayList<Beatmap> beatmaps;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -46,7 +47,7 @@ public class BeatmapSet {
|
||||
/**
|
||||
* Returns the beatmap at the given index.
|
||||
* @param index the beatmap index
|
||||
* @throws IndexOutOfBoundsException
|
||||
* @throws IndexOutOfBoundsException if the index is out of range
|
||||
*/
|
||||
public Beatmap get(int index) { return beatmaps.get(index); }
|
||||
|
||||
@@ -54,10 +55,13 @@ public class BeatmapSet {
|
||||
* Removes the beatmap at the given index.
|
||||
* @param index the beatmap index
|
||||
* @return the removed beatmap
|
||||
* @throws IndexOutOfBoundsException
|
||||
* @throws IndexOutOfBoundsException if the index is out of range
|
||||
*/
|
||||
public Beatmap remove(int index) { return beatmaps.remove(index); }
|
||||
|
||||
@Override
|
||||
public Iterator<Beatmap> iterator() { return beatmaps.iterator(); }
|
||||
|
||||
/**
|
||||
* Returns an array of strings containing beatmap information.
|
||||
* <ul>
|
||||
@@ -65,10 +69,10 @@ public class BeatmapSet {
|
||||
* <li>1: Mapped by {Creator}
|
||||
* <li>2: Length: {} BPM: {} Objects: {}
|
||||
* <li>3: Circles: {} Sliders: {} Spinners: {}
|
||||
* <li>4: CS:{} HP:{} AR:{} OD:{}
|
||||
* <li>4: CS:{} HP:{} AR:{} OD:{} Stars:{}
|
||||
* </ul>
|
||||
* @param index the beatmap index
|
||||
* @throws IndexOutOfBoundsException
|
||||
* @throws IndexOutOfBoundsException if the index is out of range
|
||||
*/
|
||||
public String[] getInfo(int index) {
|
||||
Beatmap beatmap = beatmaps.get(index);
|
||||
@@ -88,11 +92,12 @@ public class BeatmapSet {
|
||||
(beatmap.hitObjectCircle + beatmap.hitObjectSlider + beatmap.hitObjectSpinner));
|
||||
info[3] = String.format("Circles: %d Sliders: %d Spinners: %d",
|
||||
beatmap.hitObjectCircle, beatmap.hitObjectSlider, beatmap.hitObjectSpinner);
|
||||
info[4] = String.format("CS:%.1f HP:%.1f AR:%.1f OD:%.1f",
|
||||
info[4] = String.format("CS:%.1f HP:%.1f AR:%.1f OD:%.1f%s",
|
||||
Math.min(beatmap.circleSize * multiplier, 10f),
|
||||
Math.min(beatmap.HPDrainRate * multiplier, 10f),
|
||||
Math.min(beatmap.approachRate * multiplier, 10f),
|
||||
Math.min(beatmap.overallDifficulty * multiplier, 10f));
|
||||
Math.min(beatmap.overallDifficulty * multiplier, 10f),
|
||||
(beatmap.starRating >= 0) ? String.format(" Stars:%.2f", beatmap.starRating) : "");
|
||||
return info;
|
||||
}
|
||||
|
||||
@@ -126,7 +131,7 @@ public class BeatmapSet {
|
||||
return true;
|
||||
|
||||
// search: version, tags (remaining beatmaps)
|
||||
for (int i = 1; i < beatmaps.size(); i++) {
|
||||
for (int i = 1, n = beatmaps.size(); i < n; i++) {
|
||||
beatmap = beatmaps.get(i);
|
||||
if (beatmap.version.toLowerCase().contains(query) ||
|
||||
beatmap.tags.contains(query))
|
||||
@@ -138,7 +143,7 @@ public class BeatmapSet {
|
||||
|
||||
/**
|
||||
* Checks whether the beatmap set matches a given condition.
|
||||
* @param type the condition type (ar, cs, od, hp, bpm, length)
|
||||
* @param type the condition type (ar, cs, od, hp, bpm, length, star/stars)
|
||||
* @param operator the operator {@literal (=/==, >, >=, <, <=)}
|
||||
* @param value the value
|
||||
* @return true if the condition is met
|
||||
@@ -154,6 +159,8 @@ public class BeatmapSet {
|
||||
case "hp": v = beatmap.HPDrainRate; break;
|
||||
case "bpm": v = beatmap.bpmMax; break;
|
||||
case "length": v = beatmap.endTime / 1000; break;
|
||||
case "star":
|
||||
case "stars": v = Math.round(beatmap.starRating * 100) / 100f; break;
|
||||
default: return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package itdelatrisu.opsu.beatmap;
|
||||
|
||||
import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.db.BeatmapDB;
|
||||
@@ -44,7 +45,7 @@ public class BeatmapSetList {
|
||||
|
||||
/** Search pattern for conditional expressions. */
|
||||
private static final Pattern SEARCH_CONDITION_PATTERN = Pattern.compile(
|
||||
"(ar|cs|od|hp|bpm|length)(=|==|>|>=|<|<=)((\\d*\\.)?\\d+)"
|
||||
"(ar|cs|od|hp|bpm|length|stars?)(==?|>=?|<=?)((\\d*\\.)?\\d+)"
|
||||
);
|
||||
|
||||
/** List containing all parsed nodes. */
|
||||
@@ -163,12 +164,17 @@ public class BeatmapSetList {
|
||||
}
|
||||
|
||||
// remove all node references
|
||||
Beatmap beatmap = node.getBeatmapSet().get(0);
|
||||
BeatmapSet beatmapSet = node.getBeatmapSet();
|
||||
Beatmap beatmap = beatmapSet.get(0);
|
||||
nodes.remove(index);
|
||||
parsedNodes.remove(eCur);
|
||||
mapCount -= node.getBeatmapSet().size();
|
||||
mapCount -= beatmapSet.size();
|
||||
if (beatmap.beatmapSetID > 0)
|
||||
MSIDdb.remove(beatmap.beatmapSetID);
|
||||
for (Beatmap bm : beatmapSet) {
|
||||
if (bm.md5Hash != null)
|
||||
this.beatmapHashDB.remove(bm.md5Hash);
|
||||
}
|
||||
|
||||
// reset indices
|
||||
for (int i = index, size = size(); i < size; i++)
|
||||
@@ -199,11 +205,16 @@ public class BeatmapSetList {
|
||||
BeatmapDB.delete(dir.getName());
|
||||
|
||||
// delete the associated directory
|
||||
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
|
||||
if (ws != null)
|
||||
ws.pause();
|
||||
try {
|
||||
Utils.deleteToTrash(dir);
|
||||
} catch (IOException e) {
|
||||
ErrorHandler.error("Could not delete song group.", e, true);
|
||||
}
|
||||
if (ws != null)
|
||||
ws.resume();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -235,6 +246,8 @@ public class BeatmapSetList {
|
||||
// remove song reference
|
||||
Beatmap beatmap = node.getBeatmapSet().remove(node.beatmapIndex);
|
||||
mapCount--;
|
||||
if (beatmap.md5Hash != null)
|
||||
beatmapHashDB.remove(beatmap.md5Hash);
|
||||
|
||||
// re-link nodes
|
||||
if (node.prev != null)
|
||||
@@ -247,11 +260,16 @@ public class BeatmapSetList {
|
||||
BeatmapDB.delete(file.getParentFile().getName(), file.getName());
|
||||
|
||||
// delete the associated file
|
||||
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
|
||||
if (ws != null)
|
||||
ws.pause();
|
||||
try {
|
||||
Utils.deleteToTrash(file);
|
||||
} catch (IOException e) {
|
||||
ErrorHandler.error("Could not delete song.", e, true);
|
||||
}
|
||||
if (ws != null)
|
||||
ws.resume();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -312,6 +330,7 @@ public class BeatmapSetList {
|
||||
/**
|
||||
* Expands the node at an index by inserting a new node for each Beatmap
|
||||
* in that node and hiding the group node.
|
||||
* @param index the node index
|
||||
* @return the first of the newly-inserted nodes
|
||||
*/
|
||||
public BeatmapSetNode expand(int index) {
|
||||
|
||||
@@ -21,7 +21,8 @@ package itdelatrisu.opsu.beatmap;
|
||||
import itdelatrisu.opsu.GameData.Grade;
|
||||
import itdelatrisu.opsu.GameImage;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.Image;
|
||||
@@ -31,7 +32,7 @@ import org.newdawn.slick.Image;
|
||||
*/
|
||||
public class BeatmapSetNode {
|
||||
/** The associated beatmap set. */
|
||||
private BeatmapSet beatmapSet;
|
||||
private final BeatmapSet beatmapSet;
|
||||
|
||||
/** Index of the selected beatmap (-1 if not focused). */
|
||||
public int beatmapIndex = -1;
|
||||
@@ -56,6 +57,14 @@ public class BeatmapSetNode {
|
||||
*/
|
||||
public BeatmapSet getBeatmapSet() { return beatmapSet; }
|
||||
|
||||
/**
|
||||
* Returns the selected beatmap (based on {@link #beatmapIndex}).
|
||||
* @return the beatmap, or null if the index is invalid
|
||||
*/
|
||||
public Beatmap getSelectedBeatmap() {
|
||||
return (beatmapIndex < 0 || beatmapIndex >= beatmapSet.size()) ? null : beatmapSet.get(beatmapIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the button.
|
||||
* @param x the x coordinate
|
||||
@@ -78,16 +87,16 @@ public class BeatmapSetNode {
|
||||
bgColor = Color.white;
|
||||
textColor = Options.getSkin().getSongSelectActiveTextColor();
|
||||
} else
|
||||
bgColor = Utils.COLOR_BLUE_BUTTON;
|
||||
bgColor = Colors.BLUE_BUTTON;
|
||||
beatmap = beatmapSet.get(beatmapIndex);
|
||||
} else {
|
||||
bgColor = Utils.COLOR_ORANGE_BUTTON;
|
||||
bgColor = Colors.ORANGE_BUTTON;
|
||||
beatmap = beatmapSet.get(0);
|
||||
}
|
||||
bg.draw(x, y, bgColor);
|
||||
|
||||
float cx = x + (bg.getWidth() * 0.043f);
|
||||
float cy = y + (bg.getHeight() * 0.2f) - 3;
|
||||
float cy = y + (bg.getHeight() * 0.18f) - 3;
|
||||
|
||||
// draw grade
|
||||
if (grade != Grade.NULL) {
|
||||
@@ -98,15 +107,58 @@ public class BeatmapSetNode {
|
||||
|
||||
// draw text
|
||||
if (Options.useUnicodeMetadata()) { // load glyphs
|
||||
Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, null);
|
||||
Utils.loadGlyphs(Utils.FONT_DEFAULT, null, beatmap.artistUnicode);
|
||||
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.titleUnicode);
|
||||
Fonts.loadGlyphs(Fonts.DEFAULT, beatmap.artistUnicode);
|
||||
}
|
||||
Utils.FONT_MEDIUM.drawString(cx, cy, beatmap.getTitle(), textColor);
|
||||
Utils.FONT_DEFAULT.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() - 2,
|
||||
Fonts.MEDIUM.drawString(cx, cy, beatmap.getTitle(), textColor);
|
||||
Fonts.DEFAULT.drawString(cx, cy + Fonts.MEDIUM.getLineHeight() - 3,
|
||||
String.format("%s // %s", beatmap.getArtist(), beatmap.creator), textColor);
|
||||
if (expanded || beatmapSet.size() == 1)
|
||||
Utils.FONT_BOLD.drawString(cx, cy + Utils.FONT_MEDIUM.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() - 4,
|
||||
Fonts.BOLD.drawString(cx, cy + Fonts.MEDIUM.getLineHeight() + Fonts.DEFAULT.getLineHeight() - 6,
|
||||
beatmap.version, textColor);
|
||||
|
||||
// draw stars
|
||||
// (note: in osu!, stars are also drawn for beatmap sets of size 1)
|
||||
if (expanded) {
|
||||
if (beatmap.starRating >= 0) {
|
||||
Image star = GameImage.STAR.getImage();
|
||||
float starOffset = star.getWidth() * 1.7f;
|
||||
float starX = cx + starOffset * 0.04f;
|
||||
float starY = cy + Fonts.MEDIUM.getLineHeight() + Fonts.DEFAULT.getLineHeight() * 2 - 8f * GameImage.getUIscale();
|
||||
float starCenterY = starY + star.getHeight() / 2f;
|
||||
final float baseAlpha = focus ? 1f : 0.8f;
|
||||
final float smallStarScale = 0.4f;
|
||||
star.setAlpha(baseAlpha);
|
||||
int i = 1;
|
||||
for (; i < beatmap.starRating && i <= 5; i++) {
|
||||
if (focus)
|
||||
star.drawFlash(starX + (i - 1) * starOffset, starY, star.getWidth(), star.getHeight(), textColor);
|
||||
else
|
||||
star.draw(starX + (i - 1) * starOffset, starY);
|
||||
}
|
||||
if (i <= 5) {
|
||||
float partialStarScale = smallStarScale + (float) (beatmap.starRating - i + 1) * (1f - smallStarScale);
|
||||
Image partialStar = star.getScaledCopy(partialStarScale);
|
||||
partialStar.setAlpha(baseAlpha);
|
||||
float partialStarY = starCenterY - partialStar.getHeight() / 2f;
|
||||
if (focus)
|
||||
partialStar.drawFlash(starX + (i - 1) * starOffset, partialStarY, partialStar.getWidth(), partialStar.getHeight(), textColor);
|
||||
else
|
||||
partialStar.draw(starX + (i - 1) * starOffset, partialStarY);
|
||||
}
|
||||
if (++i <= 5) {
|
||||
Image smallStar = star.getScaledCopy(smallStarScale);
|
||||
smallStar.setAlpha(0.5f);
|
||||
float smallStarY = starCenterY - smallStar.getHeight() / 2f;
|
||||
for (; i <= 5; i++) {
|
||||
if (focus)
|
||||
smallStar.drawFlash(starX + (i - 1) * starOffset, smallStarY, smallStar.getWidth(), smallStar.getHeight(), textColor);
|
||||
else
|
||||
smallStar.draw(starX + (i - 1) * starOffset, smallStarY);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,13 +39,13 @@ public enum BeatmapSortOrder {
|
||||
LENGTH (4, "Length", new LengthOrder());
|
||||
|
||||
/** The ID of the sort (used for tab positioning). */
|
||||
private int id;
|
||||
private final int id;
|
||||
|
||||
/** The name of the sort. */
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
/** The comparator for the sort. */
|
||||
private Comparator<BeatmapSetNode> comparator;
|
||||
private final Comparator<BeatmapSetNode> comparator;
|
||||
|
||||
/** The tab associated with the sort (displayed in Song Menu screen). */
|
||||
private MenuButton tab;
|
||||
@@ -123,13 +123,11 @@ public enum BeatmapSortOrder {
|
||||
@Override
|
||||
public int compare(BeatmapSetNode v, BeatmapSetNode w) {
|
||||
int vMax = 0, wMax = 0;
|
||||
for (int i = 0, size = v.getBeatmapSet().size(); i < size; i++) {
|
||||
Beatmap beatmap = v.getBeatmapSet().get(i);
|
||||
for (Beatmap beatmap : v.getBeatmapSet()) {
|
||||
if (beatmap.endTime > vMax)
|
||||
vMax = beatmap.endTime;
|
||||
}
|
||||
for (int i = 0, size = w.getBeatmapSet().size(); i < size; i++) {
|
||||
Beatmap beatmap = w.getBeatmapSet().get(i);
|
||||
for (Beatmap beatmap : w.getBeatmapSet()) {
|
||||
if (beatmap.endTime > wMax)
|
||||
wMax = beatmap.endTime;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
import itdelatrisu.opsu.GameMod;
|
||||
import itdelatrisu.opsu.objects.curves.CatmullCurve;
|
||||
import itdelatrisu.opsu.objects.curves.CircumscribedCircle;
|
||||
import itdelatrisu.opsu.objects.curves.Curve;
|
||||
import itdelatrisu.opsu.objects.curves.LinearBezier;
|
||||
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
@@ -34,13 +39,6 @@ public class HitObject {
|
||||
TYPE_NEWCOMBO = 4, // not an object
|
||||
TYPE_SPINNER = 8;
|
||||
|
||||
/** Hit object type names. */
|
||||
private static final String
|
||||
CIRCLE = "circle",
|
||||
SLIDER = "slider",
|
||||
SPINNER = "spinner",
|
||||
UNKNOWN = "unknown object";
|
||||
|
||||
/** Hit sound types (bits). */
|
||||
public static final byte
|
||||
SOUND_NORMAL = 0,
|
||||
@@ -101,9 +99,18 @@ public class HitObject {
|
||||
/** Hit sound type (SOUND_* bitmask). */
|
||||
private byte hitSound;
|
||||
|
||||
/** Hit sound addition (sampleSet, AdditionSampleSet, ?, ...). */
|
||||
/** Hit sound addition (sampleSet, AdditionSampleSet). */
|
||||
private byte[] addition;
|
||||
|
||||
/** Addition custom sample index. */
|
||||
private byte additionCustomSampleIndex;
|
||||
|
||||
/** Addition hit sound volume. */
|
||||
private int additionHitSoundVolume;
|
||||
|
||||
/** Addition hit sound file. */
|
||||
private String additionHitSound;
|
||||
|
||||
/** Slider curve type (SLIDER_* constant). */
|
||||
private char sliderType;
|
||||
|
||||
@@ -250,9 +257,17 @@ public class HitObject {
|
||||
// addition
|
||||
if (tokens.length > additionIndex) {
|
||||
String[] additionTokens = tokens[additionIndex].split(":");
|
||||
this.addition = new byte[additionTokens.length];
|
||||
for (int j = 0; j < additionTokens.length; j++)
|
||||
this.addition[j] = Byte.parseByte(additionTokens[j]);
|
||||
if (additionTokens.length > 1) {
|
||||
this.addition = new byte[2];
|
||||
addition[0] = Byte.parseByte(additionTokens[0]);
|
||||
addition[1] = Byte.parseByte(additionTokens[1]);
|
||||
}
|
||||
if (additionTokens.length > 2)
|
||||
this.additionCustomSampleIndex = Byte.parseByte(additionTokens[2]);
|
||||
if (additionTokens.length > 3)
|
||||
this.additionHitSoundVolume = Integer.parseInt(additionTokens[3]);
|
||||
if (additionTokens.length > 4)
|
||||
this.additionHitSound = additionTokens[4];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,13 +313,13 @@ public class HitObject {
|
||||
*/
|
||||
public String getTypeName() {
|
||||
if (isCircle())
|
||||
return CIRCLE;
|
||||
return "circle";
|
||||
else if (isSlider())
|
||||
return SLIDER;
|
||||
return "slider";
|
||||
else if (isSpinner())
|
||||
return SPINNER;
|
||||
return "spinner";
|
||||
else
|
||||
return UNKNOWN;
|
||||
return "unknown object type";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -386,6 +401,35 @@ public class HitObject {
|
||||
*/
|
||||
public float getPixelLength() { return pixelLength; }
|
||||
|
||||
/**
|
||||
* Returns the time duration of the slider (excluding repeats), in milliseconds.
|
||||
* @param sliderMultiplier the beatmap's slider movement speed multiplier
|
||||
* @param beatLength the beat length
|
||||
* @return the slider segment length
|
||||
*/
|
||||
public float getSliderTime(float sliderMultiplier, float beatLength) {
|
||||
return beatLength * (pixelLength / sliderMultiplier) / 100f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the slider curve.
|
||||
* @param scaled whether to use scaled coordinates
|
||||
* @return a new Curve instance
|
||||
*/
|
||||
public Curve getSliderCurve(boolean scaled) {
|
||||
if (sliderType == SLIDER_PASSTHROUGH && sliderX.length == 2) {
|
||||
Vec2f nora = new Vec2f(sliderX[0] - x, sliderY[0] - y).nor();
|
||||
Vec2f norb = new Vec2f(sliderX[0] - sliderX[1], sliderY[0] - sliderY[1]).nor();
|
||||
if (Math.abs(norb.x * nora.y - norb.y * nora.x) < 0.00001f)
|
||||
return new LinearBezier(this, false, scaled); // vectors parallel, use linear bezier instead
|
||||
else
|
||||
return new CircumscribedCircle(this, scaled);
|
||||
} else if (sliderType == SLIDER_CATMULL)
|
||||
return new CatmullCurve(this, scaled);
|
||||
else
|
||||
return new LinearBezier(this, sliderType == SLIDER_LINEAR, scaled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the spinner end time.
|
||||
* @return the end time (in ms)
|
||||
@@ -471,6 +515,21 @@ public class HitObject {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the custom sample index (addition).
|
||||
*/
|
||||
public byte getCustomSampleIndex() { return additionCustomSampleIndex; }
|
||||
|
||||
/**
|
||||
* Returns the hit sound volume (addition).
|
||||
*/
|
||||
public int getHitSoundVolume() { return additionHitSoundVolume; }
|
||||
|
||||
/**
|
||||
* Returns the hit sound file (addition).
|
||||
*/
|
||||
public String getHitSoundFile() { return additionHitSound; }
|
||||
|
||||
/**
|
||||
* Sets the hit object index in the current stack.
|
||||
* @param stack index in the stack
|
||||
@@ -529,9 +588,12 @@ public class HitObject {
|
||||
// addition
|
||||
if (addition != null) {
|
||||
for (int i = 0; i < addition.length; i++) {
|
||||
sb.append(addition[i]);
|
||||
sb.append(':');
|
||||
sb.append(addition[i]); sb.append(':');
|
||||
}
|
||||
sb.append(additionCustomSampleIndex); sb.append(':');
|
||||
sb.append(additionHitSoundVolume); sb.append(':');
|
||||
if (additionHitSound != null)
|
||||
sb.append(additionHitSound);
|
||||
} else
|
||||
sb.setLength(sb.length() - 1);
|
||||
|
||||
|
||||
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/>.
|
||||
*/
|
||||
|
||||
package itdelatrisu.opsu;
|
||||
package itdelatrisu.opsu.beatmap;
|
||||
|
||||
import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.Options;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
@@ -62,6 +65,9 @@ public class OszUnpacker {
|
||||
}
|
||||
|
||||
// unpack OSZs
|
||||
BeatmapWatchService ws = (Options.isWatchServiceEnabled()) ? BeatmapWatchService.get() : null;
|
||||
if (ws != null)
|
||||
ws.pause();
|
||||
for (File file : files) {
|
||||
fileIndex++;
|
||||
String dirName = file.getName().substring(0, file.getName().lastIndexOf('.'));
|
||||
@@ -73,6 +79,8 @@ public class OszUnpacker {
|
||||
dirs.add(songDir);
|
||||
}
|
||||
}
|
||||
if (ws != null)
|
||||
ws.resume();
|
||||
|
||||
fileIndex = -1;
|
||||
files = null;
|
||||
@@ -43,7 +43,7 @@ public class BeatmapDB {
|
||||
* Current database version.
|
||||
* This value should be changed whenever the database format changes.
|
||||
*/
|
||||
private static final String DATABASE_VERSION = "2015-06-11";
|
||||
private static final String DATABASE_VERSION = "2015-09-02";
|
||||
|
||||
/** Minimum batch size ratio ({@code batchSize/cacheSize}) to invoke batch loading. */
|
||||
private static final float LOAD_BATCH_MIN_RATIO = 0.2f;
|
||||
@@ -58,7 +58,7 @@ public class BeatmapDB {
|
||||
private static Connection connection;
|
||||
|
||||
/** Query statements. */
|
||||
private static PreparedStatement insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt, updateSizeStmt;
|
||||
private static PreparedStatement insertStmt, selectStmt, deleteMapStmt, deleteGroupStmt, setStarsStmt, updateSizeStmt;
|
||||
|
||||
/** Current size of beatmap cache table. */
|
||||
private static int cacheSize = -1;
|
||||
@@ -95,12 +95,13 @@ public class BeatmapDB {
|
||||
try {
|
||||
insertStmt = connection.prepareStatement(
|
||||
"INSERT INTO beatmaps VALUES (" +
|
||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " +
|
||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?," +
|
||||
"?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
selectStmt = connection.prepareStatement("SELECT * FROM beatmaps WHERE dir = ? AND file = ?");
|
||||
deleteMapStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ? AND file = ?");
|
||||
deleteGroupStmt = connection.prepareStatement("DELETE FROM beatmaps WHERE dir = ?");
|
||||
setStarsStmt = connection.prepareStatement("UPDATE beatmaps SET stars = ? WHERE dir = ? AND file = ?");
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to prepare beatmap statements.", e, true);
|
||||
}
|
||||
@@ -123,7 +124,7 @@ public class BeatmapDB {
|
||||
"audioFile TEXT, audioLeadIn INTEGER, previewTime INTEGER, countdown INTEGER, sampleSet TEXT, stackLeniency REAL, " +
|
||||
"mode INTEGER, letterboxInBreaks BOOLEAN, widescreenStoryboard BOOLEAN, epilepsyWarning BOOLEAN, " +
|
||||
"bg TEXT, sliderBorder TEXT, timingPoints TEXT, breaks TEXT, combo TEXT, " +
|
||||
"md5hash TEXT" +
|
||||
"md5hash TEXT, stars REAL" +
|
||||
"); " +
|
||||
"CREATE TABLE IF NOT EXISTS info (" +
|
||||
"key TEXT NOT NULL UNIQUE, value TEXT" +
|
||||
@@ -342,6 +343,7 @@ public class BeatmapDB {
|
||||
stmt.setString(39, beatmap.breaksToString());
|
||||
stmt.setString(40, beatmap.comboToString());
|
||||
stmt.setString(41, beatmap.md5Hash);
|
||||
stmt.setDouble(42, beatmap.starRating);
|
||||
} catch (SQLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -484,6 +486,7 @@ public class BeatmapDB {
|
||||
beatmap.bg = new File(dir, BeatmapParser.getDBString(bg));
|
||||
beatmap.sliderBorderFromString(rs.getString(37));
|
||||
beatmap.md5Hash = rs.getString(41);
|
||||
beatmap.starRating = rs.getDouble(42);
|
||||
} catch (SQLException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -571,6 +574,25 @@ public class BeatmapDB {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the star rating for a beatmap in the database.
|
||||
* @param beatmap the beatmap
|
||||
*/
|
||||
public static void setStars(Beatmap beatmap) {
|
||||
if (connection == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
setStarsStmt.setDouble(1, beatmap.starRating);
|
||||
setStarsStmt.setString(2, beatmap.getFile().getParentFile().getName());
|
||||
setStarsStmt.setString(3, beatmap.getFile().getName());
|
||||
setStarsStmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error(String.format("Failed to save star rating '%.4f' for beatmap '%s' in database.",
|
||||
beatmap.starRating, beatmap.toString()), e, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the connection to the database.
|
||||
*/
|
||||
|
||||
@@ -119,7 +119,9 @@ public class ScoreDB {
|
||||
"timestamp = ? AND MID = ? AND MSID = ? AND title = ? AND artist = ? AND " +
|
||||
"creator = ? AND version = ? AND hit300 = ? AND hit100 = ? AND hit50 = ? AND " +
|
||||
"geki = ? AND katu = ? AND miss = ? AND score = ? AND combo = ? AND perfect = ? AND mods = ? AND " +
|
||||
"replay = ? AND playerName = ?"
|
||||
"(replay = ? OR (replay IS NULL AND ? IS NULL)) AND " +
|
||||
"(playerName = ? OR (playerName IS NULL AND ? IS NULL))"
|
||||
// TODO: extra playerName checks not needed if name is guaranteed not null
|
||||
);
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to prepare score statements.", e, true);
|
||||
@@ -222,6 +224,7 @@ public class ScoreDB {
|
||||
try {
|
||||
setStatementFields(insertStmt, data);
|
||||
insertStmt.setString(18, data.replayString);
|
||||
insertStmt.setString(19, data.playerName);
|
||||
insertStmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to save score to database.", e, true);
|
||||
@@ -238,6 +241,10 @@ public class ScoreDB {
|
||||
|
||||
try {
|
||||
setStatementFields(deleteScoreStmt, data);
|
||||
deleteScoreStmt.setString(18, data.replayString);
|
||||
deleteScoreStmt.setString(19, data.replayString);
|
||||
deleteScoreStmt.setString(20, data.playerName);
|
||||
deleteScoreStmt.setString(21, data.playerName);
|
||||
deleteScoreStmt.executeUpdate();
|
||||
} catch (SQLException e) {
|
||||
ErrorHandler.error("Failed to delete score from database.", e, true);
|
||||
@@ -289,8 +296,6 @@ public class ScoreDB {
|
||||
stmt.setInt(15, data.combo);
|
||||
stmt.setBoolean(16, data.perfect);
|
||||
stmt.setInt(17, data.mods);
|
||||
stmt.setString(18, data.replayString);
|
||||
stmt.setString(19, data.playerName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,9 @@ public class Download {
|
||||
/** Read timeout, in ms. */
|
||||
public static final int READ_TIMEOUT = 10000;
|
||||
|
||||
/** Maximum number of HTTP/HTTPS redirects to follow. */
|
||||
public static final int MAX_REDIRECTS = 3;
|
||||
|
||||
/** Time between download speed and ETA updates, in ms. */
|
||||
private static final int UPDATE_INTERVAL = 1000;
|
||||
|
||||
@@ -58,7 +61,7 @@ public class Download {
|
||||
ERROR ("Error");
|
||||
|
||||
/** The status name. */
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -172,13 +175,57 @@ public class Download {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// open connection, get content length
|
||||
// open connection
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
||||
conn.setReadTimeout(READ_TIMEOUT);
|
||||
conn.setUseCaches(false);
|
||||
URL downloadURL = url;
|
||||
int redirectCount = 0;
|
||||
boolean isRedirect = false;
|
||||
do {
|
||||
isRedirect = false;
|
||||
|
||||
conn = (HttpURLConnection) downloadURL.openConnection();
|
||||
conn.setConnectTimeout(CONNECTION_TIMEOUT);
|
||||
conn.setReadTimeout(READ_TIMEOUT);
|
||||
conn.setUseCaches(false);
|
||||
|
||||
// allow HTTP <--> HTTPS redirects
|
||||
// http://download.java.net/jdk7u2/docs/technotes/guides/deployment/deployment-guide/upgrade-guide/article-17.html
|
||||
conn.setInstanceFollowRedirects(false);
|
||||
conn.setRequestProperty("User-Agent", "Mozilla/5.0...");
|
||||
|
||||
// check for redirect
|
||||
int status = conn.getResponseCode();
|
||||
if (status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM ||
|
||||
status == HttpURLConnection.HTTP_SEE_OTHER || status == HttpURLConnection.HTTP_USE_PROXY) {
|
||||
URL base = conn.getURL();
|
||||
String location = conn.getHeaderField("Location");
|
||||
URL target = null;
|
||||
if (location != null)
|
||||
target = new URL(base, location);
|
||||
conn.disconnect();
|
||||
|
||||
// check for problems
|
||||
String error = null;
|
||||
if (location == null)
|
||||
error = String.format("Download for URL '%s' is attempting to redirect without a 'location' header.", base.toString());
|
||||
else if (!target.getProtocol().equals("http") && !target.getProtocol().equals("https"))
|
||||
error = String.format("Download for URL '%s' is attempting to redirect to a non-HTTP/HTTPS protocol '%s'.", base.toString(), target.getProtocol());
|
||||
else if (redirectCount > MAX_REDIRECTS)
|
||||
error = String.format("Download for URL '%s' is attempting too many redirects (over %d).", base.toString(), MAX_REDIRECTS);
|
||||
if (error != null) {
|
||||
ErrorHandler.error(error, null, false);
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
// follow redirect
|
||||
downloadURL = target;
|
||||
redirectCount++;
|
||||
isRedirect = true;
|
||||
}
|
||||
} while (isRedirect);
|
||||
|
||||
// store content length
|
||||
contentLength = conn.getContentLength();
|
||||
} catch (IOException e) {
|
||||
status = Status.ERROR;
|
||||
@@ -198,9 +245,18 @@ public class Download {
|
||||
fos = fileOutputStream;
|
||||
status = Status.DOWNLOADING;
|
||||
updateReadSoFar();
|
||||
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||
long bytesRead = fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||
if (status == Status.DOWNLOADING) { // not interrupted
|
||||
// TODO: if connection is lost before a download finishes, it's still marked as "complete"
|
||||
// check if the entire file was received
|
||||
if (bytesRead < contentLength) {
|
||||
status = Status.ERROR;
|
||||
Log.warn(String.format("Download '%s' failed: %d bytes expected, %d bytes received.", url.toString(), contentLength, bytesRead));
|
||||
if (listener != null)
|
||||
listener.error();
|
||||
return;
|
||||
}
|
||||
|
||||
// mark download as complete
|
||||
status = Status.COMPLETE;
|
||||
rbc.close();
|
||||
fos.close();
|
||||
@@ -273,7 +329,7 @@ public class Download {
|
||||
public long readSoFar() {
|
||||
switch (status) {
|
||||
case COMPLETE:
|
||||
return contentLength;
|
||||
return (rbc != null) ? rbc.getReadSoFar() : contentLength;
|
||||
case DOWNLOADING:
|
||||
if (rbc != null)
|
||||
return rbc.getReadSoFar();
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||
import itdelatrisu.opsu.downloads.Download.DownloadListener;
|
||||
import itdelatrisu.opsu.downloads.Download.Status;
|
||||
import itdelatrisu.opsu.downloads.servers.DownloadServer;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
|
||||
import java.io.File;
|
||||
@@ -42,19 +44,19 @@ public class DownloadNode {
|
||||
private Download download;
|
||||
|
||||
/** Beatmap set ID. */
|
||||
private int beatmapSetID;
|
||||
private final int beatmapSetID;
|
||||
|
||||
/** Last updated date string. */
|
||||
private String date;
|
||||
private final String date;
|
||||
|
||||
/** Song title. */
|
||||
private String title, titleUnicode;
|
||||
private final String title, titleUnicode;
|
||||
|
||||
/** Song artist. */
|
||||
private String artist, artistUnicode;
|
||||
private final String artist, artistUnicode;
|
||||
|
||||
/** Beatmap creator. */
|
||||
private String creator;
|
||||
private final String creator;
|
||||
|
||||
/** Button drawing values. */
|
||||
private static float buttonBaseX, buttonBaseY, buttonWidth, buttonHeight, buttonOffset;
|
||||
@@ -68,12 +70,6 @@ public class DownloadNode {
|
||||
/** Container width. */
|
||||
private static int containerWidth;
|
||||
|
||||
/** Button background colors. */
|
||||
public static final Color
|
||||
BG_NORMAL = new Color(0, 0, 0, 0.25f),
|
||||
BG_HOVER = new Color(0, 0, 0, 0.5f),
|
||||
BG_FOCUS = new Color(0, 0, 0, 0.75f);
|
||||
|
||||
/**
|
||||
* Initializes the base coordinates for drawing.
|
||||
* @param width the container width
|
||||
@@ -86,16 +82,16 @@ public class DownloadNode {
|
||||
buttonBaseX = width * 0.024f;
|
||||
buttonBaseY = height * 0.2f;
|
||||
buttonWidth = width * 0.7f;
|
||||
buttonHeight = Utils.FONT_MEDIUM.getLineHeight() * 2.1f;
|
||||
buttonHeight = Fonts.MEDIUM.getLineHeight() * 2.1f;
|
||||
buttonOffset = buttonHeight * 1.1f;
|
||||
|
||||
// download info
|
||||
infoBaseX = width * 0.75f;
|
||||
infoBaseY = height * 0.07f + Utils.FONT_LARGE.getLineHeight() * 2f;
|
||||
infoBaseY = height * 0.07f + Fonts.LARGE.getLineHeight() * 2f;
|
||||
infoWidth = width * 0.25f;
|
||||
infoHeight = Utils.FONT_DEFAULT.getLineHeight() * 2.4f;
|
||||
infoHeight = Fonts.DEFAULT.getLineHeight() * 2.4f;
|
||||
|
||||
float searchY = (height * 0.05f) + Utils.FONT_LARGE.getLineHeight();
|
||||
float searchY = (height * 0.05f) + Fonts.LARGE.getLineHeight();
|
||||
float buttonHeight = height * 0.038f;
|
||||
maxResultsShown = (int) ((height - buttonBaseY - searchY) / buttonOffset);
|
||||
maxDownloadsShown = (int) ((height - infoBaseY - searchY - buttonHeight) / infoHeight);
|
||||
@@ -228,10 +224,9 @@ public class DownloadNode {
|
||||
* @param total the total number of buttons
|
||||
*/
|
||||
public static void drawResultScrollbar(Graphics g, float position, float total) {
|
||||
UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset,
|
||||
buttonBaseX, buttonBaseY,
|
||||
UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset, buttonBaseX, buttonBaseY,
|
||||
buttonWidth * 1.01f, (maxResultsShown-1) * buttonOffset + buttonHeight,
|
||||
BG_NORMAL, Color.white, true);
|
||||
Colors.BLACK_BG_NORMAL, Color.white, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -242,11 +237,18 @@ public class DownloadNode {
|
||||
*/
|
||||
public static void drawDownloadScrollbar(Graphics g, float index, float total) {
|
||||
UI.drawScrollbar(g, index, total, maxDownloadsShown * infoHeight, infoBaseX, infoBaseY,
|
||||
infoWidth, maxDownloadsShown * infoHeight, BG_NORMAL, Color.white, true);
|
||||
infoWidth, maxDownloadsShown * infoHeight, Colors.BLACK_BG_NORMAL, Color.white, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param beatmapSetID the beatmap set ID
|
||||
* @param date the last modified date string
|
||||
* @param title the song title
|
||||
* @param titleUnicode the Unicode song title (or {@code null} if none)
|
||||
* @param artist the song artist
|
||||
* @param artistUnicode the Unicode song artist (or {@code null} if none)
|
||||
* @param creator the beatmap creator
|
||||
*/
|
||||
public DownloadNode(int beatmapSetID, String date, String title,
|
||||
String titleUnicode, String artist, String artistUnicode, String creator) {
|
||||
@@ -273,7 +275,7 @@ public class DownloadNode {
|
||||
return;
|
||||
String path = String.format("%s%c%d", Options.getOSZDir(), File.separatorChar, beatmapSetID);
|
||||
String rename = String.format("%d %s - %s.osz", beatmapSetID, artist, title);
|
||||
this.download = new Download(url, path, rename);
|
||||
Download download = new Download(url, path, rename);
|
||||
download.setListener(new DownloadListener() {
|
||||
@Override
|
||||
public void completed() {
|
||||
@@ -285,8 +287,9 @@ public class DownloadNode {
|
||||
UI.sendBarNotification("Download failed due to a connection error.");
|
||||
}
|
||||
});
|
||||
this.download = download;
|
||||
if (Options.useUnicodeMetadata()) // load glyphs
|
||||
Utils.loadGlyphs(Utils.FONT_LARGE, getTitle(), null);
|
||||
Fonts.loadGlyphs(Fonts.LARGE, getTitle());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,12 +351,12 @@ public class DownloadNode {
|
||||
Download dl = DownloadList.get().getDownload(beatmapSetID);
|
||||
|
||||
// rectangle outline
|
||||
g.setColor((focus) ? BG_FOCUS : (hover) ? BG_HOVER : BG_NORMAL);
|
||||
g.setColor((focus) ? Colors.BLACK_BG_FOCUS : (hover) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
|
||||
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
|
||||
|
||||
// map is already loaded
|
||||
if (BeatmapSetList.get().containsBeatmapSetID(beatmapSetID)) {
|
||||
g.setColor(Utils.COLOR_BLUE_BUTTON);
|
||||
g.setColor(Colors.BLUE_BUTTON);
|
||||
g.fillRect(buttonBaseX, y, buttonWidth, buttonHeight);
|
||||
}
|
||||
|
||||
@@ -361,7 +364,7 @@ public class DownloadNode {
|
||||
if (dl != null) {
|
||||
float progress = dl.getProgress();
|
||||
if (progress > 0f) {
|
||||
g.setColor(Utils.COLOR_GREEN);
|
||||
g.setColor(Colors.GREEN);
|
||||
g.fillRect(buttonBaseX, y, buttonWidth * progress / 100f, buttonHeight);
|
||||
}
|
||||
}
|
||||
@@ -373,21 +376,22 @@ public class DownloadNode {
|
||||
|
||||
// text
|
||||
// TODO: if the title/artist line is too long, shorten it (e.g. add "...") instead of just clipping
|
||||
if (Options.useUnicodeMetadata()) // load glyphs
|
||||
Utils.loadGlyphs(Utils.FONT_BOLD, getTitle(), getArtist());
|
||||
|
||||
if (Options.useUnicodeMetadata()) { // load glyphs
|
||||
Fonts.loadGlyphs(Fonts.BOLD, getTitle());
|
||||
Fonts.loadGlyphs(Fonts.BOLD, getArtist());
|
||||
}
|
||||
// TODO can't set clip again or else old clip will be cleared
|
||||
//g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Utils.FONT_DEFAULT.getWidth(creator)), Utils.FONT_BOLD.getLineHeight());
|
||||
Utils.FONT_BOLD.drawString(
|
||||
//g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Fonts.DEFAULT.getWidth(creator)), Fonts.BOLD.getLineHeight());
|
||||
Fonts.BOLD.drawString(
|
||||
textX, y + marginY,
|
||||
String.format("%s - %s%s", getArtist(), getTitle(),
|
||||
(dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white);
|
||||
//g.clearClip();
|
||||
Utils.FONT_DEFAULT.drawString(
|
||||
textX, y + marginY + Utils.FONT_BOLD.getLineHeight(),
|
||||
Fonts.DEFAULT.drawString(
|
||||
textX, y + marginY + Fonts.BOLD.getLineHeight(),
|
||||
String.format("Last updated: %s", date), Color.white);
|
||||
Utils.FONT_DEFAULT.drawString(
|
||||
edgeX - Utils.FONT_DEFAULT.getWidth(creator), y + marginY,
|
||||
Fonts.DEFAULT.drawString(
|
||||
edgeX - Fonts.DEFAULT.getWidth(creator), y + marginY,
|
||||
creator, Color.white);
|
||||
}
|
||||
|
||||
@@ -399,6 +403,7 @@ public class DownloadNode {
|
||||
* @param hover true if the mouse is hovering over this button
|
||||
*/
|
||||
public void drawDownload(Graphics g, float position, int id, boolean hover) {
|
||||
Download download = this.download; // in case clearDownload() is called asynchronously
|
||||
if (download == null) {
|
||||
ErrorHandler.error("Trying to draw download information for button without Download object.", null, false);
|
||||
return;
|
||||
@@ -410,7 +415,7 @@ public class DownloadNode {
|
||||
float marginY = infoHeight * 0.04f;
|
||||
|
||||
// rectangle outline
|
||||
g.setColor((id % 2 == 0) ? BG_HOVER : BG_NORMAL);
|
||||
g.setColor((id % 2 == 0) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
|
||||
g.fillRect(infoBaseX, y, infoWidth, infoHeight);
|
||||
|
||||
// text
|
||||
@@ -428,8 +433,8 @@ public class DownloadNode {
|
||||
info = String.format("%s: %.1f%% (%s/%s)", status.getName(), progress,
|
||||
Utils.bytesToString(download.readSoFar()), Utils.bytesToString(download.contentLength()));
|
||||
}
|
||||
Utils.FONT_BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
|
||||
Utils.FONT_DEFAULT.drawString(textX, y + marginY + Utils.FONT_BOLD.getLineHeight(), info, Color.white);
|
||||
Fonts.BOLD.drawString(textX, y + marginY, getTitle(), Color.white);
|
||||
Fonts.DEFAULT.drawString(textX, y + marginY + Fonts.BOLD.getLineHeight(), info, Color.white);
|
||||
|
||||
// 'x' button
|
||||
if (hover) {
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.nio.channels.ReadableByteChannel;
|
||||
*/
|
||||
public class ReadableByteChannelWrapper implements ReadableByteChannel {
|
||||
/** The wrapped ReadableByteChannel. */
|
||||
private ReadableByteChannel rbc;
|
||||
private final ReadableByteChannel rbc;
|
||||
|
||||
/** The number of bytes read. */
|
||||
private long bytesRead;
|
||||
|
||||
@@ -77,7 +77,7 @@ public class Updater {
|
||||
UPDATE_FINAL ("Update queued.");
|
||||
|
||||
/** The status description. */
|
||||
private String description;
|
||||
private final String description;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -194,9 +194,10 @@ public class Updater {
|
||||
|
||||
/**
|
||||
* Checks the program version against the version file on the update server.
|
||||
* @throws IOException if an I/O exception occurs
|
||||
*/
|
||||
public void checkForUpdates() throws IOException {
|
||||
if (status != Status.INITIAL || System.getProperty("XDG") != null)
|
||||
if (status != Status.INITIAL || Options.USE_XDG)
|
||||
return;
|
||||
|
||||
status = Status.CHECKING;
|
||||
|
||||
@@ -75,4 +75,7 @@ public abstract class DownloadServer {
|
||||
public String getPreviewURL(int beatmapSetID) {
|
||||
return String.format(PREVIEW_URL, beatmapSetID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() { return getName(); }
|
||||
}
|
||||
|
||||
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/
|
||||
* <p>
|
||||
* <i>This server went offline in August 2015.</i>
|
||||
*/
|
||||
public class OsuMirrorServer extends DownloadServer {
|
||||
/** 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.
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public void close() throws IOException { reader.close(); }
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ public class OsuWriter {
|
||||
|
||||
/**
|
||||
* Closes the output stream.
|
||||
* @throws IOException
|
||||
* @throws IOException if an I/O error occurs
|
||||
*/
|
||||
public void close() throws IOException { writer.close(); }
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ import itdelatrisu.opsu.GameMod;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||
import itdelatrisu.opsu.states.Game;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
@@ -35,9 +37,6 @@ import org.newdawn.slick.Graphics;
|
||||
* Data type representing a circle object.
|
||||
*/
|
||||
public class Circle implements GameObject {
|
||||
/** The amount of time, in milliseconds, to fade in the circle. */
|
||||
private static final int FADE_IN_TIME = 375;
|
||||
|
||||
/** The diameter of hit circles. */
|
||||
private static float diameter;
|
||||
|
||||
@@ -62,11 +61,10 @@ public class Circle implements GameObject {
|
||||
/**
|
||||
* Initializes the Circle data type with map modifiers, images, and dimensions.
|
||||
* @param container the game container
|
||||
* @param circleSize the map's circleSize value
|
||||
* @param circleDiameter the circle diameter
|
||||
*/
|
||||
public static void init(GameContainer container, float circleSize) {
|
||||
diameter = (104 - (circleSize * 8));
|
||||
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||
public static void init(GameContainer container, float circleDiameter) {
|
||||
diameter = circleDiameter * HitObject.getXMultiplier(); // convert from Osupixels (640x480)
|
||||
int diameterInt = (int) diameter;
|
||||
GameImage.HITCIRCLE.setImage(GameImage.HITCIRCLE.getImage().getScaledCopy(diameterInt, diameterInt));
|
||||
GameImage.HITCIRCLE_OVERLAY.setImage(GameImage.HITCIRCLE_OVERLAY.getImage().getScaledCopy(diameterInt, diameterInt));
|
||||
@@ -93,26 +91,37 @@ public class Circle implements GameObject {
|
||||
@Override
|
||||
public void draw(Graphics g, int trackPosition) {
|
||||
int timeDiff = hitObject.getTime() - trackPosition;
|
||||
float scale = timeDiff / (float) game.getApproachTime();
|
||||
float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
|
||||
final int approachTime = game.getApproachTime();
|
||||
final int fadeInTime = game.getFadeInTime();
|
||||
float scale = timeDiff / (float) approachTime;
|
||||
float approachScale = 1 + scale * 3;
|
||||
float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime;
|
||||
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
|
||||
|
||||
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
|
||||
Utils.COLOR_WHITE_FADE.a = color.a = alpha;
|
||||
if (GameMod.HIDDEN.isActive()) {
|
||||
final int hiddenDecayTime = game.getHiddenDecayTime();
|
||||
final int hiddenTimeDiff = game.getHiddenTimeDiff();
|
||||
if (fadeinScale <= 0f && timeDiff < hiddenTimeDiff + hiddenDecayTime) {
|
||||
float hiddenAlpha = (timeDiff < hiddenTimeDiff) ? 0f : (timeDiff - hiddenTimeDiff) / (float) hiddenDecayTime;
|
||||
alpha = Math.min(alpha, hiddenAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
if (timeDiff >= 0)
|
||||
float oldAlpha = Colors.WHITE_FADE.a;
|
||||
Colors.WHITE_FADE.a = color.a = alpha;
|
||||
|
||||
if (timeDiff >= 0 && !GameMod.HIDDEN.isActive())
|
||||
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
|
||||
GameImage.HITCIRCLE.getImage().drawCentered(x, y, color);
|
||||
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
|
||||
if (!overlayAboveNumber)
|
||||
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
|
||||
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Colors.WHITE_FADE);
|
||||
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
|
||||
GameImage.HITCIRCLE.getImage().getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
|
||||
if (overlayAboveNumber)
|
||||
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Utils.COLOR_WHITE_FADE);
|
||||
GameImage.HITCIRCLE_OVERLAY.getImage().drawCentered(x, y, Colors.WHITE_FADE);
|
||||
|
||||
Utils.COLOR_WHITE_FADE.a = oldAlpha;
|
||||
Colors.WHITE_FADE.a = oldAlpha;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +195,7 @@ public class Circle implements GameObject {
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
|
||||
public Vec2f getPointAt(int trackPosition) { return new Vec2f(x, y); }
|
||||
|
||||
@Override
|
||||
public int getEndTime() { return hitObject.getTime(); }
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package itdelatrisu.opsu.objects;
|
||||
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||
|
||||
import org.newdawn.slick.Graphics;
|
||||
|
||||
@@ -53,7 +54,7 @@ public class DummyObject implements GameObject {
|
||||
public boolean mousePressed(int x, int y, int trackPosition) { return false; }
|
||||
|
||||
@Override
|
||||
public float[] getPointAt(int trackPosition) { return new float[] { x, y }; }
|
||||
public Vec2f getPointAt(int trackPosition) { return new Vec2f(x, y); }
|
||||
|
||||
@Override
|
||||
public int getEndTime() { return hitObject.getTime(); }
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
package itdelatrisu.opsu.objects;
|
||||
|
||||
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||
|
||||
import org.newdawn.slick.Graphics;
|
||||
|
||||
/**
|
||||
@@ -55,9 +57,9 @@ public interface GameObject {
|
||||
/**
|
||||
* Returns the coordinates of the hit object at a given track position.
|
||||
* @param trackPosition the track position
|
||||
* @return the [x,y] coordinates
|
||||
* @return the position vector
|
||||
*/
|
||||
public float[] getPointAt(int trackPosition);
|
||||
public Vec2f getPointAt(int trackPosition);
|
||||
|
||||
/**
|
||||
* Returns the end time of the hit object.
|
||||
|
||||
@@ -26,11 +26,10 @@ import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
import itdelatrisu.opsu.objects.curves.CatmullCurve;
|
||||
import itdelatrisu.opsu.objects.curves.CircumscribedCircle;
|
||||
import itdelatrisu.opsu.objects.curves.Curve;
|
||||
import itdelatrisu.opsu.objects.curves.LinearBezier;
|
||||
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||
import itdelatrisu.opsu.states.Game;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
@@ -56,9 +55,6 @@ public class Slider implements GameObject {
|
||||
/** The diameter of hit circles. */
|
||||
private static float diameter;
|
||||
|
||||
/** The amount of time, in milliseconds, to fade in the slider. */
|
||||
private static final int FADE_IN_TIME = 375;
|
||||
|
||||
/** The associated HitObject. */
|
||||
private HitObject hitObject;
|
||||
|
||||
@@ -113,22 +109,21 @@ public class Slider implements GameObject {
|
||||
/**
|
||||
* Initializes the Slider data type with images and dimensions.
|
||||
* @param container the game container
|
||||
* @param circleSize the map's circleSize value
|
||||
* @param circleDiameter the circle diameter
|
||||
* @param beatmap the associated beatmap
|
||||
*/
|
||||
public static void init(GameContainer container, float circleSize, Beatmap beatmap) {
|
||||
public static void init(GameContainer container, float circleDiameter, Beatmap beatmap) {
|
||||
containerWidth = container.getWidth();
|
||||
containerHeight = container.getHeight();
|
||||
|
||||
diameter = (104 - (circleSize * 8));
|
||||
diameter = (diameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||
diameter = circleDiameter * HitObject.getXMultiplier(); // convert from Osupixels (640x480)
|
||||
int diameterInt = (int) diameter;
|
||||
|
||||
followRadius = diameter / 2 * 3f;
|
||||
|
||||
// slider ball
|
||||
if (GameImage.SLIDER_BALL.hasSkinImages() ||
|
||||
(!GameImage.SLIDER_BALL.hasSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
|
||||
if (GameImage.SLIDER_BALL.hasBeatmapSkinImages() ||
|
||||
(!GameImage.SLIDER_BALL.hasBeatmapSkinImage() && GameImage.SLIDER_BALL.getImages() != null))
|
||||
sliderBallImages = GameImage.SLIDER_BALL.getImages();
|
||||
else
|
||||
sliderBallImages = new Image[]{ GameImage.SLIDER_BALL.getImage() };
|
||||
@@ -160,7 +155,7 @@ public class Slider implements GameObject {
|
||||
updatePosition();
|
||||
|
||||
// slider time calculations
|
||||
this.sliderTime = game.getBeatLength() * (hitObject.getPixelLength() / sliderMultiplier) / 100f;
|
||||
this.sliderTime = hitObject.getSliderTime(sliderMultiplier, game.getBeatLength());
|
||||
this.sliderTimeTotal = sliderTime * hitObject.getRepeatCount();
|
||||
|
||||
// ticks
|
||||
@@ -178,36 +173,46 @@ public class Slider implements GameObject {
|
||||
@Override
|
||||
public void draw(Graphics g, int trackPosition) {
|
||||
int timeDiff = hitObject.getTime() - trackPosition;
|
||||
float scale = timeDiff / (float) game.getApproachTime();
|
||||
float fadeinScale = (timeDiff - game.getApproachTime() + FADE_IN_TIME) / (float) FADE_IN_TIME;
|
||||
final int approachTime = game.getApproachTime();
|
||||
final int fadeInTime = game.getFadeInTime();
|
||||
float scale = timeDiff / (float) approachTime;
|
||||
float approachScale = 1 + scale * 3;
|
||||
float fadeinScale = (timeDiff - approachTime + fadeInTime) / (float) fadeInTime;
|
||||
float alpha = Utils.clamp(1 - fadeinScale, 0, 1);
|
||||
boolean overlayAboveNumber = Options.getSkin().isHitCircleOverlayAboveNumber();
|
||||
|
||||
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
|
||||
Utils.COLOR_WHITE_FADE.a = color.a = alpha;
|
||||
float oldAlpha = Colors.WHITE_FADE.a;
|
||||
Colors.WHITE_FADE.a = color.a = alpha;
|
||||
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
|
||||
Image hitCircle = GameImage.HITCIRCLE.getImage();
|
||||
float[] endPos = curve.pointAt(1);
|
||||
Vec2f endPos = curve.pointAt(1);
|
||||
|
||||
curve.draw(color);
|
||||
color.a = alpha;
|
||||
|
||||
// end circle
|
||||
hitCircle.drawCentered(endPos[0], endPos[1], color);
|
||||
hitCircleOverlay.drawCentered(endPos[0], endPos[1], Utils.COLOR_WHITE_FADE);
|
||||
hitCircle.drawCentered(endPos.x, endPos.y, color);
|
||||
hitCircleOverlay.drawCentered(endPos.x, endPos.y, Colors.WHITE_FADE);
|
||||
|
||||
// start circle
|
||||
hitCircle.drawCentered(x, y, color);
|
||||
if (!overlayAboveNumber)
|
||||
hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
|
||||
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
|
||||
|
||||
// ticks
|
||||
if (ticksT != null) {
|
||||
Image tick = GameImage.SLIDER_TICK.getImage();
|
||||
for (int i = 0; i < ticksT.length; i++) {
|
||||
float[] c = curve.pointAt(ticksT[i]);
|
||||
tick.drawCentered(c[0], c[1], Utils.COLOR_WHITE_FADE);
|
||||
Vec2f c = curve.pointAt(ticksT[i]);
|
||||
tick.drawCentered(c.x, c.y, Colors.WHITE_FADE);
|
||||
}
|
||||
}
|
||||
if (GameMod.HIDDEN.isActive()) {
|
||||
final int hiddenDecayTime = game.getHiddenDecayTime();
|
||||
final int hiddenTimeDiff = game.getHiddenTimeDiff();
|
||||
if (fadeinScale <= 0f && timeDiff < hiddenTimeDiff + hiddenDecayTime) {
|
||||
float hiddenAlpha = (timeDiff < hiddenTimeDiff) ? 0f : (timeDiff - hiddenTimeDiff) / (float) hiddenDecayTime;
|
||||
alpha = Math.min(alpha, hiddenAlpha);
|
||||
}
|
||||
}
|
||||
if (sliderClickedInitial)
|
||||
@@ -216,7 +221,7 @@ public class Slider implements GameObject {
|
||||
data.drawSymbolNumber(hitObject.getComboNumber(), x, y,
|
||||
hitCircle.getWidth() * 0.40f / data.getDefaultSymbolImage(0).getHeight(), alpha);
|
||||
if (overlayAboveNumber)
|
||||
hitCircleOverlay.drawCentered(x, y, Utils.COLOR_WHITE_FADE);
|
||||
hitCircleOverlay.drawCentered(x, y, Colors.WHITE_FADE);
|
||||
|
||||
// repeats
|
||||
for (int tcurRepeat = currentRepeats; tcurRepeat <= currentRepeats + 1; tcurRepeat++) {
|
||||
@@ -232,7 +237,7 @@ public class Slider implements GameObject {
|
||||
if (tcurRepeat % 2 == 0) {
|
||||
// last circle
|
||||
arrow.setRotation(curve.getEndAngle());
|
||||
arrow.drawCentered(endPos[0], endPos[1]);
|
||||
arrow.drawCentered(endPos.x, endPos.y);
|
||||
} else {
|
||||
// first circle
|
||||
arrow.setRotation(curve.getStartAngle());
|
||||
@@ -243,40 +248,41 @@ public class Slider implements GameObject {
|
||||
|
||||
if (timeDiff >= 0) {
|
||||
// approach circle
|
||||
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
|
||||
if (!GameMod.HIDDEN.isActive())
|
||||
GameImage.APPROACHCIRCLE.getImage().getScaledCopy(approachScale).drawCentered(x, y, color);
|
||||
} else {
|
||||
// Since update() might not have run before drawing during a replay, the
|
||||
// slider time may not have been calculated, which causes NAN numbers and flicker.
|
||||
if (sliderTime == 0)
|
||||
return;
|
||||
|
||||
float[] c = curve.pointAt(getT(trackPosition, false));
|
||||
float[] c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
|
||||
Vec2f c = curve.pointAt(getT(trackPosition, false));
|
||||
Vec2f c2 = curve.pointAt(getT(trackPosition, false) + 0.01f);
|
||||
|
||||
float t = getT(trackPosition, false);
|
||||
// float dis = hitObject.getPixelLength() * HitObject.getXMultiplier() * (t - (int) t);
|
||||
// Image sliderBallFrame = sliderBallImages[(int) (dis / (diameter * Math.PI) * 30) % sliderBallImages.length];
|
||||
Image sliderBallFrame = sliderBallImages[(int) (t * sliderTime * 60 / 1000) % sliderBallImages.length];
|
||||
float angle = (float) (Math.atan2(c2[1] - c[1], c2[0] - c[0]) * 180 / Math.PI);
|
||||
float angle = (float) (Math.atan2(c2.y - c.y, c2.x - c.x) * 180 / Math.PI);
|
||||
sliderBallFrame.setRotation(angle);
|
||||
sliderBallFrame.drawCentered(c[0], c[1]);
|
||||
sliderBallFrame.drawCentered(c.x, c.y);
|
||||
|
||||
// follow circle
|
||||
if (followCircleActive) {
|
||||
GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c[0], c[1]);
|
||||
GameImage.SLIDER_FOLLOWCIRCLE.getImage().drawCentered(c.x, c.y);
|
||||
|
||||
// "flashlight" mod: dim the screen
|
||||
if (GameMod.FLASHLIGHT.isActive()) {
|
||||
float oldAlphaBlack = Utils.COLOR_BLACK_ALPHA.a;
|
||||
Utils.COLOR_BLACK_ALPHA.a = 0.75f;
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
float oldAlphaBlack = Colors.BLACK_ALPHA.a;
|
||||
Colors.BLACK_ALPHA.a = 0.75f;
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(0, 0, containerWidth, containerHeight);
|
||||
Utils.COLOR_BLACK_ALPHA.a = oldAlphaBlack;
|
||||
Colors.BLACK_ALPHA.a = oldAlphaBlack;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Utils.COLOR_WHITE_FADE.a = oldAlpha;
|
||||
Colors.WHITE_FADE.a = oldAlpha;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,9 +352,9 @@ public class Slider implements GameObject {
|
||||
float cx, cy;
|
||||
HitObjectType type;
|
||||
if (currentRepeats % 2 == 0) { // last circle
|
||||
float[] lastPos = curve.pointAt(1);
|
||||
cx = lastPos[0];
|
||||
cy = lastPos[1];
|
||||
Vec2f lastPos = curve.pointAt(1);
|
||||
cx = lastPos.x;
|
||||
cy = lastPos.y;
|
||||
type = HitObjectType.SLIDER_LAST;
|
||||
} else { // first circle
|
||||
cx = x;
|
||||
@@ -429,8 +435,8 @@ public class Slider implements GameObject {
|
||||
|
||||
// check if cursor pressed and within end circle
|
||||
if (keyPressed || GameMod.RELAX.isActive()) {
|
||||
float[] c = curve.pointAt(getT(trackPosition, false));
|
||||
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
||||
Vec2f c = curve.pointAt(getT(trackPosition, false));
|
||||
double distance = Math.hypot(c.x - mouseX, c.y - mouseY);
|
||||
if (distance < followRadius)
|
||||
sliderHeldToEnd = true;
|
||||
}
|
||||
@@ -473,12 +479,11 @@ public class Slider implements GameObject {
|
||||
}
|
||||
|
||||
// holding slider...
|
||||
float[] c = curve.pointAt(getT(trackPosition, false));
|
||||
double distance = Math.hypot(c[0] - mouseX, c[1] - mouseY);
|
||||
Vec2f c = curve.pointAt(getT(trackPosition, false));
|
||||
double distance = Math.hypot(c.x - mouseX, c.y - mouseY);
|
||||
if (((keyPressed || GameMod.RELAX.isActive()) && distance < followRadius) || isAutoMod) {
|
||||
// mouse pressed and within follow circle
|
||||
followCircleActive = true;
|
||||
data.changeHealth(delta * GameData.HP_DRAIN_MULTIPLIER);
|
||||
|
||||
// held during new repeat
|
||||
if (isNewRepeat) {
|
||||
@@ -489,14 +494,14 @@ public class Slider implements GameObject {
|
||||
curve.getX(lastIndex), curve.getY(lastIndex), hitObject, currentRepeats);
|
||||
} else // first circle
|
||||
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER30,
|
||||
c[0], c[1], hitObject, currentRepeats);
|
||||
c.x, c.y, hitObject, currentRepeats);
|
||||
}
|
||||
|
||||
// held during new tick
|
||||
if (isNewTick) {
|
||||
ticksHit++;
|
||||
data.sliderTickResult(trackPosition, GameData.HIT_SLIDER10,
|
||||
c[0], c[1], hitObject, currentRepeats);
|
||||
c.x, c.y, hitObject, currentRepeats);
|
||||
}
|
||||
|
||||
// held near end of slider
|
||||
@@ -518,22 +523,16 @@ public class Slider implements GameObject {
|
||||
public void updatePosition() {
|
||||
this.x = hitObject.getScaledX();
|
||||
this.y = hitObject.getScaledY();
|
||||
|
||||
if (hitObject.getSliderType() == HitObject.SLIDER_PASSTHROUGH && hitObject.getSliderX().length == 2)
|
||||
this.curve = new CircumscribedCircle(hitObject, color);
|
||||
else if (hitObject.getSliderType() == HitObject.SLIDER_CATMULL)
|
||||
this.curve = new CatmullCurve(hitObject, color);
|
||||
else
|
||||
this.curve = new LinearBezier(hitObject, color, hitObject.getSliderType() == HitObject.SLIDER_LINEAR);
|
||||
this.curve = hitObject.getSliderCurve(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] getPointAt(int trackPosition) {
|
||||
public Vec2f getPointAt(int trackPosition) {
|
||||
if (trackPosition <= hitObject.getTime())
|
||||
return new float[] { x, y };
|
||||
return new Vec2f(x, y);
|
||||
else if (trackPosition >= hitObject.getTime() + sliderTimeTotal) {
|
||||
if (hitObject.getRepeatCount() % 2 == 0)
|
||||
return new float[] { x, y };
|
||||
return new Vec2f(x, y);
|
||||
else
|
||||
return curve.pointAt(1);
|
||||
} else
|
||||
|
||||
@@ -27,7 +27,9 @@ import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.audio.SoundEffect;
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||
import itdelatrisu.opsu.states.Game;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
@@ -50,9 +52,6 @@ public class Spinner implements GameObject {
|
||||
/** The amount of time, in milliseconds, before another velocity is stored. */
|
||||
private static final float DELTA_UPDATE_TIME = 1000 / 60f;
|
||||
|
||||
/** The amount of time, in milliseconds, to fade in the spinner. */
|
||||
private static final int FADE_IN_TIME = 500;
|
||||
|
||||
/** Angle mod multipliers: "auto" (477rpm), "spun out" (287rpm) */
|
||||
private static final float
|
||||
AUTO_MULTIPLIER = 1 / 20f, // angle = 477/60f * delta/1000f * TWO_PI;
|
||||
@@ -69,6 +68,9 @@ public class Spinner implements GameObject {
|
||||
/** The associated HitObject. */
|
||||
private HitObject hitObject;
|
||||
|
||||
/** The associated Game object. */
|
||||
private Game game;
|
||||
|
||||
/** The associated GameData object. */
|
||||
private GameData data;
|
||||
|
||||
@@ -124,6 +126,7 @@ public class Spinner implements GameObject {
|
||||
*/
|
||||
public Spinner(HitObject hitObject, Game game, GameData data) {
|
||||
this.hitObject = hitObject;
|
||||
this.game = game;
|
||||
this.data = data;
|
||||
|
||||
/*
|
||||
@@ -162,7 +165,7 @@ public class Spinner implements GameObject {
|
||||
final int maxVel = 48;
|
||||
final int minTime = 2000;
|
||||
final int maxTime = 5000;
|
||||
maxStoredDeltaAngles = (int) Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime)
|
||||
maxStoredDeltaAngles = Utils.clamp((hitObject.getEndTime() - hitObject.getTime() - minTime)
|
||||
* (maxVel - minVel) / (maxTime - minTime) + minVel, minVel, maxVel);
|
||||
storedDeltaAngle = new float[maxStoredDeltaAngles];
|
||||
|
||||
@@ -175,20 +178,21 @@ public class Spinner implements GameObject {
|
||||
public void draw(Graphics g, int trackPosition) {
|
||||
// only draw spinners shortly before start time
|
||||
int timeDiff = hitObject.getTime() - trackPosition;
|
||||
if (timeDiff - FADE_IN_TIME > 0)
|
||||
final int fadeInTime = game.getFadeInTime();
|
||||
if (timeDiff - fadeInTime > 0)
|
||||
return;
|
||||
|
||||
boolean spinnerComplete = (rotations >= rotationsNeeded);
|
||||
float alpha = Utils.clamp(1 - (float) timeDiff / FADE_IN_TIME, 0f, 1f);
|
||||
float alpha = Utils.clamp(1 - (float) timeDiff / fadeInTime, 0f, 1f);
|
||||
|
||||
// darken screen
|
||||
if (Options.getSkin().isSpinnerFadePlayfield()) {
|
||||
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
|
||||
float oldAlpha = Colors.BLACK_ALPHA.a;
|
||||
if (timeDiff > 0)
|
||||
Utils.COLOR_BLACK_ALPHA.a *= alpha;
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
Colors.BLACK_ALPHA.a *= alpha;
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, height);
|
||||
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
|
||||
Colors.BLACK_ALPHA.a = oldAlpha;
|
||||
}
|
||||
|
||||
// rpm
|
||||
@@ -210,13 +214,15 @@ public class Spinner implements GameObject {
|
||||
spinnerMetreSub.draw(0, height - spinnerMetreSub.getHeight());
|
||||
|
||||
// main spinner elements
|
||||
float approachScale = 1 - Utils.clamp(((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime())), 0f, 1f);
|
||||
GameImage.SPINNER_CIRCLE.getImage().setAlpha(alpha);
|
||||
GameImage.SPINNER_CIRCLE.getImage().setRotation(drawRotation * 360f);
|
||||
GameImage.SPINNER_CIRCLE.getImage().drawCentered(width / 2, height / 2);
|
||||
Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale);
|
||||
approachCircleScaled.setAlpha(alpha);
|
||||
approachCircleScaled.drawCentered(width / 2, height / 2);
|
||||
if (!GameMod.HIDDEN.isActive()) {
|
||||
float approachScale = 1 - Utils.clamp(((float) timeDiff / (hitObject.getTime() - hitObject.getEndTime())), 0f, 1f);
|
||||
Image approachCircleScaled = GameImage.SPINNER_APPROACHCIRCLE.getImage().getScaledCopy(approachScale);
|
||||
approachCircleScaled.setAlpha(alpha);
|
||||
approachCircleScaled.drawCentered(width / 2, height / 2);
|
||||
}
|
||||
GameImage.SPINNER_SPIN.getImage().setAlpha(alpha);
|
||||
GameImage.SPINNER_SPIN.getImage().drawCentered(width / 2, height * 3 / 4);
|
||||
|
||||
@@ -342,7 +348,7 @@ public class Spinner implements GameObject {
|
||||
public void updatePosition() {}
|
||||
|
||||
@Override
|
||||
public float[] getPointAt(int trackPosition) {
|
||||
public Vec2f getPointAt(int trackPosition) {
|
||||
// get spinner time
|
||||
int timeDiff;
|
||||
float x = hitObject.getScaledX(), y = hitObject.getScaledY();
|
||||
@@ -357,10 +363,7 @@ public class Spinner implements GameObject {
|
||||
float multiplier = (GameMod.AUTO.isActive()) ? AUTO_MULTIPLIER : SPUN_OUT_MULTIPLIER;
|
||||
float angle = (timeDiff * multiplier) - HALF_PI;
|
||||
final float r = height / 10f;
|
||||
return new float[] {
|
||||
(float) (x + r * Math.cos(angle)),
|
||||
(float) (y + r * Math.sin(angle))
|
||||
};
|
||||
return new Vec2f((float) (x + r * Math.cos(angle)), (float) (y + r * Math.sin(angle)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,14 +18,10 @@
|
||||
|
||||
package itdelatrisu.opsu.objects.curves;
|
||||
|
||||
import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.SlickException;
|
||||
|
||||
/**
|
||||
* Representation of Catmull Curve with equidistant points.
|
||||
*
|
||||
@@ -35,10 +31,18 @@ public class CatmullCurve extends EqualDistanceMultiCurve {
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated HitObject
|
||||
* @param color the color of this curve
|
||||
*/
|
||||
public CatmullCurve(HitObject hitObject, Color color) {
|
||||
super(hitObject, color);
|
||||
public CatmullCurve(HitObject hitObject) {
|
||||
this(hitObject, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated HitObject
|
||||
* @param scaled whether to use scaled coordinates
|
||||
*/
|
||||
public CatmullCurve(HitObject hitObject, boolean scaled) {
|
||||
super(hitObject, scaled);
|
||||
LinkedList<CurveType> catmulls = new LinkedList<CurveType>();
|
||||
int ncontrolPoints = hitObject.getSliderX().length + 1;
|
||||
LinkedList<Vec2f> points = new LinkedList<Vec2f>(); // temporary list of points to separate different curves
|
||||
@@ -53,24 +57,15 @@ public class CatmullCurve extends EqualDistanceMultiCurve {
|
||||
for (int i = 0; i < ncontrolPoints; i++) {
|
||||
points.addLast(new Vec2f(getX(i), getY(i)));
|
||||
if (points.size() >= 4) {
|
||||
try {
|
||||
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
|
||||
} catch (SlickException e) {
|
||||
ErrorHandler.error(null, e, true);
|
||||
}
|
||||
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
|
||||
points.removeFirst();
|
||||
}
|
||||
}
|
||||
if (getX(ncontrolPoints - 1) != getX(ncontrolPoints - 2)
|
||||
||getY(ncontrolPoints - 1) != getY(ncontrolPoints - 2))
|
||||
points.addLast(new Vec2f(getX(ncontrolPoints - 1), getY(ncontrolPoints - 1)));
|
||||
if (points.size() >= 4) {
|
||||
try {
|
||||
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
|
||||
} catch (SlickException e) {
|
||||
ErrorHandler.error(null, e, true);
|
||||
}
|
||||
}
|
||||
if (getX(ncontrolPoints - 1) != getX(ncontrolPoints - 2) ||
|
||||
getY(ncontrolPoints - 1) != getY(ncontrolPoints - 2))
|
||||
points.addLast(new Vec2f(getX(ncontrolPoints - 1), getY(ncontrolPoints - 1)));
|
||||
if (points.size() >= 4)
|
||||
catmulls.add(new CentripetalCatmullRom(points.toArray(new Vec2f[0])));
|
||||
|
||||
init(catmulls);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
|
||||
package itdelatrisu.opsu.objects.curves;
|
||||
|
||||
import org.newdawn.slick.SlickException;
|
||||
|
||||
/**
|
||||
* Representation of a Centripetal Catmull–Rom spline.
|
||||
* (Currently not technically Centripetal Catmull–Rom.)
|
||||
@@ -37,11 +35,10 @@ public class CentripetalCatmullRom extends CurveType {
|
||||
/**
|
||||
* Constructor.
|
||||
* @param points the control points of the curve
|
||||
* @throws SlickException
|
||||
*/
|
||||
protected CentripetalCatmullRom(Vec2f[] points) throws SlickException {
|
||||
protected CentripetalCatmullRom(Vec2f[] points) {
|
||||
if (points.length != 4)
|
||||
throw new SlickException(String.format("Need exactly 4 points to initialize CentripetalCatmullRom, %d provided.", points.length));
|
||||
throw new RuntimeException(String.format("Need exactly 4 points to initialize CentripetalCatmullRom, %d provided.", points.length));
|
||||
|
||||
this.points = points;
|
||||
time = new float[4];
|
||||
|
||||
@@ -18,11 +18,9 @@
|
||||
|
||||
package itdelatrisu.opsu.objects.curves;
|
||||
|
||||
import itdelatrisu.opsu.ErrorHandler;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
|
||||
/**
|
||||
* Representation of a curve along a Circumscribed Circle of three points.
|
||||
* http://en.wikipedia.org/wiki/Circumscribed_circle
|
||||
@@ -53,10 +51,18 @@ public class CircumscribedCircle extends Curve {
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated HitObject
|
||||
* @param color the color of this curve
|
||||
*/
|
||||
public CircumscribedCircle(HitObject hitObject, Color color) {
|
||||
super(hitObject, color);
|
||||
public CircumscribedCircle(HitObject hitObject) {
|
||||
this(hitObject, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated HitObject
|
||||
* @param scaled whether to use scaled coordinates
|
||||
*/
|
||||
public CircumscribedCircle(HitObject hitObject, boolean scaled) {
|
||||
super(hitObject, scaled);
|
||||
|
||||
// construct the three points
|
||||
this.start = new Vec2f(getX(0), getY(0));
|
||||
@@ -70,8 +76,6 @@ public class CircumscribedCircle extends Curve {
|
||||
Vec2f norb = mid.cpy().sub(end).nor();
|
||||
|
||||
this.circleCenter = intersect(mida, nora, midb, norb);
|
||||
if (circleCenter == null)
|
||||
return;
|
||||
|
||||
// find the angles relative to the circle center
|
||||
Vec2f startAngPoint = start.cpy().sub(circleCenter);
|
||||
@@ -92,13 +96,8 @@ public class CircumscribedCircle extends Curve {
|
||||
startAng -= TWO_PI;
|
||||
else if (Math.abs(startAng - (endAng - TWO_PI)) < TWO_PI && isIn(startAng, midAng, endAng - (TWO_PI)))
|
||||
endAng -= TWO_PI;
|
||||
else {
|
||||
ErrorHandler.error(
|
||||
String.format("Cannot find angles between midAng (%.3f %.3f %.3f).",
|
||||
startAng, midAng, endAng), null, true
|
||||
);
|
||||
return;
|
||||
}
|
||||
else
|
||||
throw new RuntimeException(String.format("Cannot find angles between midAng (%.3f %.3f %.3f).", startAng, midAng, endAng));
|
||||
}
|
||||
|
||||
// find an angle with an arc length of pixelLength along this circle
|
||||
@@ -116,10 +115,8 @@ public class CircumscribedCircle extends Curve {
|
||||
// calculate points
|
||||
float step = hitObject.getPixelLength() / CURVE_POINTS_SEPERATION;
|
||||
curve = new Vec2f[(int) step + 1];
|
||||
for (int i = 0; i < curve.length; i++) {
|
||||
float[] xy = pointAt(i / step);
|
||||
curve[i] = new Vec2f(xy[0], xy[1]);
|
||||
}
|
||||
for (int i = 0; i < curve.length; i++)
|
||||
curve[i] = pointAt(i / step);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,21 +148,19 @@ public class CircumscribedCircle extends Curve {
|
||||
//u = ((b.y-a.y)ta.x +(a.x-b.x)ta.y) / (tb.x*ta.y - tb.y*ta.x);
|
||||
|
||||
float des = tb.x * ta.y - tb.y * ta.x;
|
||||
if (Math.abs(des) < 0.00001f) {
|
||||
ErrorHandler.error("Vectors are parallel.", null, true);
|
||||
return null;
|
||||
}
|
||||
if (Math.abs(des) < 0.00001f)
|
||||
throw new RuntimeException("Vectors are parallel.");
|
||||
float u = ((b.y - a.y) * ta.x + (a.x - b.x) * ta.y) / des;
|
||||
return b.cpy().add(tb.x * u, tb.y * u);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] pointAt(float t) {
|
||||
float ang = lerp(startAng, endAng, t);
|
||||
return new float[] {
|
||||
public Vec2f pointAt(float t) {
|
||||
float ang = Utils.lerp(startAng, endAng, t);
|
||||
return new Vec2f(
|
||||
(float) (Math.cos(ang) * radius + circleCenter.x),
|
||||
(float) (Math.sin(ang) * radius + circleCenter.y)
|
||||
};
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -20,10 +20,10 @@ package itdelatrisu.opsu.objects.curves;
|
||||
|
||||
import itdelatrisu.opsu.GameImage;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
import itdelatrisu.opsu.render.CurveRenderState;
|
||||
import itdelatrisu.opsu.skins.Skin;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
|
||||
import org.lwjgl.opengl.ContextCapabilities;
|
||||
import org.lwjgl.opengl.GLContext;
|
||||
@@ -64,14 +64,21 @@ public abstract class Curve {
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated HitObject
|
||||
* @param color the color of this curve
|
||||
* @param scaled whether to use scaled coordinates
|
||||
*/
|
||||
protected Curve(HitObject hitObject, Color color) {
|
||||
protected Curve(HitObject hitObject, boolean scaled) {
|
||||
this.hitObject = hitObject;
|
||||
this.x = hitObject.getScaledX();
|
||||
this.y = hitObject.getScaledY();
|
||||
this.sliderX = hitObject.getScaledSliderX();
|
||||
this.sliderY = hitObject.getScaledSliderY();
|
||||
if (scaled) {
|
||||
this.x = hitObject.getScaledX();
|
||||
this.y = hitObject.getScaledY();
|
||||
this.sliderX = hitObject.getScaledSliderX();
|
||||
this.sliderY = hitObject.getScaledSliderY();
|
||||
} else {
|
||||
this.x = hitObject.getX();
|
||||
this.y = hitObject.getY();
|
||||
this.sliderX = hitObject.getSliderX();
|
||||
this.sliderY = hitObject.getSliderY();
|
||||
}
|
||||
this.renderState = null;
|
||||
}
|
||||
|
||||
@@ -80,28 +87,28 @@ public abstract class Curve {
|
||||
* Should be called before any curves are drawn.
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
* @param circleSize the circle size
|
||||
* @param circleDiameter the circle diameter
|
||||
* @param borderColor the curve border color
|
||||
*/
|
||||
public static void init(int width, int height, float circleSize, Color borderColor) {
|
||||
public static void init(int width, int height, float circleDiameter, Color borderColor) {
|
||||
Curve.borderColor = borderColor;
|
||||
|
||||
ContextCapabilities capabilities = GLContext.getCapabilities();
|
||||
mmsliderSupported = capabilities.GL_EXT_framebuffer_object && capabilities.OpenGL32;
|
||||
mmsliderSupported = capabilities.GL_EXT_framebuffer_object;
|
||||
if (mmsliderSupported)
|
||||
CurveRenderState.init(width, height, circleSize);
|
||||
CurveRenderState.init(width, height, circleDiameter);
|
||||
else {
|
||||
if (Options.getSkin().getSliderStyle() != Skin.STYLE_PEPPYSLIDER)
|
||||
Log.warn("New slider style requires FBO support and OpenGL 3.2.");
|
||||
Log.warn("New slider style requires FBO support.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the point on the curve at a value t.
|
||||
* @param t the t value [0, 1]
|
||||
* @return the point [x, y]
|
||||
* @return the position vector
|
||||
*/
|
||||
public abstract float[] pointAt(float t);
|
||||
public abstract Vec2f pointAt(float t);
|
||||
|
||||
/**
|
||||
* Draws the full curve to the graphics context.
|
||||
@@ -116,7 +123,7 @@ public abstract class Curve {
|
||||
Image hitCircle = GameImage.HITCIRCLE.getImage();
|
||||
Image hitCircleOverlay = GameImage.HITCIRCLE_OVERLAY.getImage();
|
||||
for (int i = 0; i < curve.length; i++)
|
||||
hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Utils.COLOR_WHITE_FADE);
|
||||
hitCircleOverlay.drawCentered(curve[i].x, curve[i].y, Colors.WHITE_FADE);
|
||||
for (int i = 0; i < curve.length; i++)
|
||||
hitCircle.drawCentered(curve[i].x, curve[i].y, color);
|
||||
}
|
||||
@@ -151,13 +158,6 @@ public abstract class Curve {
|
||||
*/
|
||||
public float getY(int i) { return (i == 0) ? y : sliderY[i - 1]; }
|
||||
|
||||
/**
|
||||
* Linear interpolation of a and b at t.
|
||||
*/
|
||||
protected float lerp(float a, float b, float t) {
|
||||
return a * (1 - t) + b * t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the slider cache (only used for mmsliders).
|
||||
*/
|
||||
|
||||
@@ -18,13 +18,12 @@
|
||||
|
||||
package itdelatrisu.opsu.objects.curves;
|
||||
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
|
||||
/**
|
||||
* Representation of multiple curve with equidistant points.
|
||||
* http://pomax.github.io/bezierinfo/#tracing
|
||||
@@ -41,10 +40,18 @@ public abstract class EqualDistanceMultiCurve extends Curve {
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated HitObject
|
||||
* @param color the color of this curve
|
||||
*/
|
||||
public EqualDistanceMultiCurve(HitObject hitObject, Color color) {
|
||||
super(hitObject, color);
|
||||
public EqualDistanceMultiCurve(HitObject hitObject) {
|
||||
this(hitObject, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated HitObject
|
||||
* @param scaled whether to use scaled coordinates
|
||||
*/
|
||||
public EqualDistanceMultiCurve(HitObject hitObject, boolean scaled) {
|
||||
super(hitObject, scaled);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +101,7 @@ public abstract class EqualDistanceMultiCurve extends Curve {
|
||||
// interpolate the point between the two closest distances
|
||||
if (distanceAt - lastDistanceAt > 1) {
|
||||
float t = (prefDistance - lastDistanceAt) / (distanceAt - lastDistanceAt);
|
||||
curve[i] = new Vec2f(lerp(lastCurve.x, thisCurve.x, t), lerp(lastCurve.y, thisCurve.y, t));
|
||||
curve[i] = new Vec2f(Utils.lerp(lastCurve.x, thisCurve.x, t), Utils.lerp(lastCurve.y, thisCurve.y, t));
|
||||
} else
|
||||
curve[i] = thisCurve;
|
||||
}
|
||||
@@ -117,20 +124,19 @@ public abstract class EqualDistanceMultiCurve extends Curve {
|
||||
}
|
||||
|
||||
@Override
|
||||
public float[] pointAt(float t) {
|
||||
public Vec2f pointAt(float t) {
|
||||
float indexF = t * ncurve;
|
||||
int index = (int) indexF;
|
||||
if (index >= ncurve) {
|
||||
Vec2f poi = curve[ncurve];
|
||||
return new float[] { poi.x, poi.y };
|
||||
} else {
|
||||
if (index >= ncurve)
|
||||
return curve[ncurve].cpy();
|
||||
else {
|
||||
Vec2f poi = curve[index];
|
||||
Vec2f poi2 = curve[index + 1];
|
||||
float t2 = indexF - index;
|
||||
return new float[] {
|
||||
lerp(poi.x, poi2.x, t2),
|
||||
lerp(poi.y, poi2.y, t2)
|
||||
};
|
||||
return new Vec2f(
|
||||
Utils.lerp(poi.x, poi2.x, t2),
|
||||
Utils.lerp(poi.y, poi2.y, t2)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ import itdelatrisu.opsu.beatmap.HitObject;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
|
||||
/**
|
||||
* Representation of Bezier curve with equidistant points.
|
||||
* http://pomax.github.io/bezierinfo/#tracing
|
||||
@@ -34,11 +32,20 @@ public class LinearBezier extends EqualDistanceMultiCurve {
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated HitObject
|
||||
* @param color the color of this curve
|
||||
* @param line whether a new curve should be generated for each sequential pair
|
||||
*/
|
||||
public LinearBezier(HitObject hitObject, Color color, boolean line) {
|
||||
super(hitObject, color);
|
||||
public LinearBezier(HitObject hitObject, boolean line) {
|
||||
this(hitObject, line, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param hitObject the associated HitObject
|
||||
* @param line whether a new curve should be generated for each sequential pair
|
||||
* @param scaled whether to use scaled coordinates
|
||||
*/
|
||||
public LinearBezier(HitObject hitObject, boolean line, boolean scaled) {
|
||||
super(hitObject, scaled);
|
||||
|
||||
LinkedList<CurveType> beziers = new LinkedList<CurveType>();
|
||||
|
||||
|
||||
@@ -40,6 +40,16 @@ public class Vec2f {
|
||||
*/
|
||||
public Vec2f() {}
|
||||
|
||||
/**
|
||||
* Sets the x and y components of this vector.
|
||||
* @return itself (for chaining)
|
||||
*/
|
||||
public Vec2f set(float nx, float ny) {
|
||||
x = nx;
|
||||
y = ny;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the midpoint between this vector and another vector.
|
||||
* @param o the other vector
|
||||
@@ -93,6 +103,17 @@ public class Vec2f {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns this vector into a unit vector.
|
||||
* @return itself (for chaining)
|
||||
*/
|
||||
public Vec2f normalize() {
|
||||
float len = len();
|
||||
x /= len;
|
||||
y /= len;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this vector.
|
||||
*/
|
||||
|
||||
@@ -18,20 +18,21 @@
|
||||
package itdelatrisu.opsu.render;
|
||||
|
||||
import itdelatrisu.opsu.GameImage;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.beatmap.HitObject;
|
||||
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
import org.lwjgl.BufferUtils;
|
||||
import org.lwjgl.opengl.EXTFramebufferObject;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL13;
|
||||
import org.lwjgl.opengl.GL14;
|
||||
import org.lwjgl.opengl.GL15;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.util.Log;
|
||||
@@ -63,15 +64,14 @@ public class CurveRenderState {
|
||||
* Should be called before any curves are drawn.
|
||||
* @param width the container width
|
||||
* @param height the container height
|
||||
* @param circleSize the circle size
|
||||
* @param circleDiameter the circle diameter
|
||||
*/
|
||||
public static void init(int width, int height, float circleSize) {
|
||||
public static void init(int width, int height, float circleDiameter) {
|
||||
containerWidth = width;
|
||||
containerHeight = height;
|
||||
|
||||
// equivalent to what happens in Slider.init()
|
||||
scale = (int) (104 - (circleSize * 8));
|
||||
scale = (int) (scale * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||
scale = (int) (circleDiameter * HitObject.getXMultiplier()); // convert from Osupixels (640x480)
|
||||
//scale = scale * 118 / 128; //for curves exactly as big as the sliderball
|
||||
FrameBufferCache.init(width, height);
|
||||
}
|
||||
@@ -114,20 +114,25 @@ public class CurveRenderState {
|
||||
mapping = cache.insert(hitObject);
|
||||
fbo = mapping;
|
||||
|
||||
int old_fb = GL11.glGetInteger(GL30.GL_FRAMEBUFFER_BINDING);
|
||||
int old_tex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
|
||||
int oldFb = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT);
|
||||
int oldTex = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
|
||||
|
||||
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fbo.getID());
|
||||
//glGetInteger requires a buffer of size 16, even though just 4
|
||||
//values are returned in this specific case
|
||||
IntBuffer oldViewport = BufferUtils.createIntBuffer(16);
|
||||
GL11.glGetInteger(GL11.GL_VIEWPORT, oldViewport);
|
||||
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fbo.getID());
|
||||
GL11.glViewport(0, 0, fbo.width, fbo.height);
|
||||
GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
Utils.COLOR_WHITE_FADE.a = 1.0f;
|
||||
Colors.WHITE_FADE.a = 1.0f;
|
||||
this.draw_curve(color, borderColor, curve);
|
||||
color.a = 1f;
|
||||
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_tex);
|
||||
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_fb);
|
||||
Utils.COLOR_WHITE_FADE.a = alpha;
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, oldTex);
|
||||
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, oldFb);
|
||||
GL11.glViewport(oldViewport.get(0), oldViewport.get(1), oldViewport.get(2), oldViewport.get(3));
|
||||
Colors.WHITE_FADE.a = alpha;
|
||||
}
|
||||
|
||||
// draw a fullscreen quad with the texture that contains the curve
|
||||
@@ -389,7 +394,7 @@ public class CurveRenderState {
|
||||
buff.flip();
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_1D, gradientTexture);
|
||||
GL11.glTexImage1D(GL11.GL_TEXTURE_1D, 0, GL11.GL_RGBA, slider.getWidth(), 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buff);
|
||||
GL30.glGenerateMipmap(GL11.GL_TEXTURE_1D);
|
||||
EXTFramebufferObject.glGenerateMipmapEXT(GL11.GL_TEXTURE_1D);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,12 +407,12 @@ public class CurveRenderState {
|
||||
program = GL20.glCreateProgram();
|
||||
int vtxShdr = GL20.glCreateShader(GL20.GL_VERTEX_SHADER);
|
||||
int frgShdr = GL20.glCreateShader(GL20.GL_FRAGMENT_SHADER);
|
||||
GL20.glShaderSource(vtxShdr, "#version 330\n"
|
||||
GL20.glShaderSource(vtxShdr, "#version 110\n"
|
||||
+ "\n"
|
||||
+ "layout(location = 0) in vec4 in_position;\n"
|
||||
+ "layout(location = 1) in vec2 in_tex_coord;\n"
|
||||
+ "attribute vec4 in_position;\n"
|
||||
+ "attribute vec2 in_tex_coord;\n"
|
||||
+ "\n"
|
||||
+ "out vec2 tex_coord;\n"
|
||||
+ "varying vec2 tex_coord;\n"
|
||||
+ "void main()\n"
|
||||
+ "{\n"
|
||||
+ " gl_Position = in_position;\n"
|
||||
@@ -419,22 +424,21 @@ public class CurveRenderState {
|
||||
String error = GL20.glGetShaderInfoLog(vtxShdr, 1024);
|
||||
Log.error("Vertex Shader compilation failed.", new Exception(error));
|
||||
}
|
||||
GL20.glShaderSource(frgShdr, "#version 330\n"
|
||||
GL20.glShaderSource(frgShdr, "#version 110\n"
|
||||
+ "\n"
|
||||
+ "uniform sampler1D tex;\n"
|
||||
+ "uniform vec2 tex_size;\n"
|
||||
+ "uniform vec3 col_tint;\n"
|
||||
+ "uniform vec4 col_border;\n"
|
||||
+ "\n"
|
||||
+ "in vec2 tex_coord;\n"
|
||||
+ "layout(location = 0) out vec4 out_colour;\n"
|
||||
+ "varying vec2 tex_coord;\n"
|
||||
+ "\n"
|
||||
+ "void main()\n"
|
||||
+ "{\n"
|
||||
+ " vec4 in_color = texture(tex, tex_coord.x);\n"
|
||||
+ " vec4 in_color = texture1D(tex, tex_coord.x);\n"
|
||||
+ " float blend_factor = in_color.r-in_color.b;\n"
|
||||
+ " vec4 new_color = vec4(mix(in_color.xyz*col_border.xyz,col_tint,blend_factor),in_color.w);\n"
|
||||
+ " out_colour = new_color;\n"
|
||||
+ " gl_FragColor = new_color;\n"
|
||||
+ "}");
|
||||
GL20.glCompileShader(frgShdr);
|
||||
res = GL20.glGetShaderi(frgShdr, GL20.GL_COMPILE_STATUS);
|
||||
|
||||
@@ -19,10 +19,8 @@ package itdelatrisu.opsu.render;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import org.lwjgl.opengl.EXTFramebufferObject;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL20;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.opengl.GL32;
|
||||
|
||||
/**
|
||||
* Represents a rendertarget. For now this maps to an OpenGL FBO via LWJGL.
|
||||
@@ -50,16 +48,16 @@ public class Rendertarget {
|
||||
private Rendertarget(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
fboID = GL30.glGenFramebuffers();
|
||||
fboID = EXTFramebufferObject.glGenFramebuffersEXT();
|
||||
textureID = GL11.glGenTextures();
|
||||
depthBufferID = GL30.glGenRenderbuffers();
|
||||
depthBufferID = EXTFramebufferObject.glGenRenderbuffersEXT();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind this rendertarget as the primary framebuffer.
|
||||
*/
|
||||
public void bind() {
|
||||
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, fboID);
|
||||
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, fboID);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +81,7 @@ public class Rendertarget {
|
||||
* Bind the default framebuffer.
|
||||
*/
|
||||
public static void unbind() {
|
||||
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, 0);
|
||||
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +91,7 @@ public class Rendertarget {
|
||||
* @param height the height
|
||||
*/
|
||||
public static Rendertarget createRTTFramebuffer(int width, int height) {
|
||||
int old_framebuffer = GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING);
|
||||
int old_framebuffer = GL11.glGetInteger(EXTFramebufferObject.GL_FRAMEBUFFER_BINDING_EXT);
|
||||
int old_texture = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D);
|
||||
Rendertarget buffer = new Rendertarget(width,height);
|
||||
buffer.bind();
|
||||
@@ -104,15 +102,14 @@ public class Rendertarget {
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST);
|
||||
GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST);
|
||||
|
||||
GL30.glBindRenderbuffer(GL30.GL_RENDERBUFFER, buffer.depthBufferID);
|
||||
GL30.glRenderbufferStorage(GL30.GL_RENDERBUFFER, GL11.GL_DEPTH_COMPONENT, width, height);
|
||||
GL30.glFramebufferRenderbuffer(GL30.GL_FRAMEBUFFER, GL30.GL_DEPTH_ATTACHMENT, GL30.GL_RENDERBUFFER, buffer.depthBufferID);
|
||||
EXTFramebufferObject.glBindRenderbufferEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID);
|
||||
EXTFramebufferObject.glRenderbufferStorageEXT(EXTFramebufferObject.GL_RENDERBUFFER_EXT, GL11.GL_DEPTH_COMPONENT, width, height);
|
||||
EXTFramebufferObject.glFramebufferRenderbufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_DEPTH_ATTACHMENT_EXT, EXTFramebufferObject.GL_RENDERBUFFER_EXT, buffer.depthBufferID);
|
||||
|
||||
GL32.glFramebufferTexture(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, fboTexture, 0);
|
||||
GL20.glDrawBuffers(GL30.GL_COLOR_ATTACHMENT0);
|
||||
EXTFramebufferObject.glFramebufferTexture2DEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, EXTFramebufferObject.GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, fboTexture, 0);
|
||||
|
||||
GL11.glBindTexture(GL11.GL_TEXTURE_2D, old_texture);
|
||||
GL30.glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, old_framebuffer);
|
||||
EXTFramebufferObject.glBindFramebufferEXT(EXTFramebufferObject.GL_FRAMEBUFFER_EXT, old_framebuffer);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
@@ -122,8 +119,8 @@ public class Rendertarget {
|
||||
* to use this rendertarget with OpenGL after calling this method.
|
||||
*/
|
||||
public void destroyRTT() {
|
||||
GL30.glDeleteFramebuffers(fboID);
|
||||
GL30.glDeleteRenderbuffers(depthBufferID);
|
||||
EXTFramebufferObject.glDeleteFramebuffersEXT(fboID);
|
||||
EXTFramebufferObject.glDeleteRenderbuffersEXT(depthBufferID);
|
||||
GL11.glDeleteTextures(textureID);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ package itdelatrisu.opsu.replay;
|
||||
*/
|
||||
public class LifeFrame {
|
||||
/** Time. */
|
||||
private int time;
|
||||
private final int time;
|
||||
|
||||
/** Percentage. */
|
||||
private float percentage;
|
||||
private final float percentage;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
||||
@@ -35,14 +35,14 @@ public enum PlaybackSpeed {
|
||||
HALF (GameImage.REPLAY_PLAYBACK_HALF, 0.5f);
|
||||
|
||||
/** The button image. */
|
||||
private GameImage gameImage;
|
||||
private final GameImage gameImage;
|
||||
|
||||
/** The playback speed modifier. */
|
||||
private final float modifier;
|
||||
|
||||
/** The button. */
|
||||
private MenuButton button;
|
||||
|
||||
/** The playback speed modifier. */
|
||||
private float modifier;
|
||||
|
||||
/** Enum values. */
|
||||
private static PlaybackSpeed[] values = PlaybackSpeed.values();
|
||||
|
||||
|
||||
@@ -41,11 +41,11 @@ import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import lzma.streams.LzmaOutputStream;
|
||||
|
||||
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
import lzma.streams.LzmaOutputStream;
|
||||
|
||||
/**
|
||||
* Captures osu! replay data.
|
||||
* https://osu.ppy.sh/wiki/Osr_%28file_format%29
|
||||
|
||||
@@ -38,13 +38,13 @@ public class ReplayFrame {
|
||||
private int timeDiff;
|
||||
|
||||
/** Time, in milliseconds. */
|
||||
private int time;
|
||||
private final int time;
|
||||
|
||||
/** Cursor coordinates (in OsuPixels). */
|
||||
private float x, y;
|
||||
private final float x, y;
|
||||
|
||||
/** Keys pressed (bitmask). */
|
||||
private int keys;
|
||||
private final int keys;
|
||||
|
||||
/**
|
||||
* Returns the start frame.
|
||||
@@ -81,7 +81,8 @@ public class ReplayFrame {
|
||||
public int getTimeDiff() { return timeDiff; }
|
||||
|
||||
/**
|
||||
* Sets the time since the previous action, in milliseconds.
|
||||
* Sets the time since the previous action.
|
||||
* @param diff the time difference, in milliseconds
|
||||
*/
|
||||
public void setTimeDiff(int diff) { this.timeDiff = diff; }
|
||||
|
||||
|
||||
@@ -29,8 +29,11 @@ import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.audio.SoundEffect;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.MenuButton;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -187,15 +190,15 @@ public class ButtonMenu extends BasicGameState {
|
||||
float mult = GameMod.getScoreMultiplier();
|
||||
String multString = String.format("Score Multiplier: %.2fx", mult);
|
||||
Color multColor = (mult == 1f) ? Color.white : (mult > 1f) ? Color.green : Color.red;
|
||||
float multY = Utils.FONT_LARGE.getLineHeight() * 2 + height * 0.06f;
|
||||
Utils.FONT_LARGE.drawString(
|
||||
(width - Utils.FONT_LARGE.getWidth(multString)) / 2f,
|
||||
float multY = Fonts.LARGE.getLineHeight() * 2 + height * 0.06f;
|
||||
Fonts.LARGE.drawString(
|
||||
(width - Fonts.LARGE.getWidth(multString)) / 2f,
|
||||
multY, multString, multColor);
|
||||
|
||||
// category text
|
||||
for (GameMod.Category category : GameMod.Category.values()) {
|
||||
Utils.FONT_LARGE.drawString(category.getX(),
|
||||
category.getY() - Utils.FONT_LARGE.getLineHeight() / 2f,
|
||||
Fonts.LARGE.drawString(category.getX(),
|
||||
category.getY() - Fonts.LARGE.getLineHeight() / 2f,
|
||||
category.getName(), category.getColor());
|
||||
}
|
||||
|
||||
@@ -217,7 +220,7 @@ public class ButtonMenu extends BasicGameState {
|
||||
}
|
||||
|
||||
// tooltips
|
||||
if (hoverMod != null && hoverMod.isImplemented())
|
||||
if (hoverMod != null)
|
||||
UI.updateTooltip(delta, hoverMod.getDescription(), true);
|
||||
}
|
||||
|
||||
@@ -253,7 +256,7 @@ public class ButtonMenu extends BasicGameState {
|
||||
};
|
||||
|
||||
/** The buttons in the state. */
|
||||
private Button[] buttons;
|
||||
private final Button[] buttons;
|
||||
|
||||
/** The associated MenuButton objects. */
|
||||
private MenuButton[] menuButtons;
|
||||
@@ -261,8 +264,11 @@ public class ButtonMenu extends BasicGameState {
|
||||
/** The actual title string list, generated upon entering the state. */
|
||||
private List<String> actualTitle;
|
||||
|
||||
/** The horizontal center offset, used for the initial button animation. */
|
||||
private AnimatedValue centerOffset;
|
||||
|
||||
/** Initial x coordinate offsets left/right of center (for shifting animation), times width. (TODO) */
|
||||
private static final float OFFSET_WIDTH_RATIO = 1 / 18f;
|
||||
private static final float OFFSET_WIDTH_RATIO = 1 / 25f;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -288,7 +294,7 @@ public class ButtonMenu extends BasicGameState {
|
||||
menuButtons = new MenuButton[buttons.length];
|
||||
for (int i = 0; i < buttons.length; i++) {
|
||||
MenuButton b = new MenuButton(button, buttonL, buttonR, center, baseY + (i * offsetY));
|
||||
b.setText(String.format("%d. %s", i + 1, buttons[i].getText()), Utils.FONT_XLARGE, Color.white);
|
||||
b.setText(String.format("%d. %s", i + 1, buttons[i].getText()), Fonts.XLARGE, Color.white);
|
||||
b.setHoverFade();
|
||||
menuButtons[i] = b;
|
||||
}
|
||||
@@ -301,7 +307,7 @@ public class ButtonMenu extends BasicGameState {
|
||||
*/
|
||||
protected float getBaseY(GameContainer container, StateBasedGame game) {
|
||||
float baseY = container.getHeight() * 0.2f;
|
||||
baseY += ((getTitle(container, game).length - 1) * Utils.FONT_LARGE.getLineHeight());
|
||||
baseY += ((getTitle(container, game).length - 1) * Fonts.LARGE.getLineHeight());
|
||||
return baseY;
|
||||
}
|
||||
|
||||
@@ -315,9 +321,9 @@ public class ButtonMenu extends BasicGameState {
|
||||
// draw title
|
||||
if (actualTitle != null) {
|
||||
float marginX = container.getWidth() * 0.015f, marginY = container.getHeight() * 0.01f;
|
||||
int lineHeight = Utils.FONT_LARGE.getLineHeight();
|
||||
int lineHeight = Fonts.LARGE.getLineHeight();
|
||||
for (int i = 0, size = actualTitle.size(); i < size; i++)
|
||||
Utils.FONT_LARGE.drawString(marginX, marginY + (i * lineHeight), actualTitle.get(i), Color.white);
|
||||
Fonts.LARGE.drawString(marginX, marginY + (i * lineHeight), actualTitle.get(i), Color.white);
|
||||
}
|
||||
|
||||
// draw buttons
|
||||
@@ -336,18 +342,14 @@ public class ButtonMenu extends BasicGameState {
|
||||
*/
|
||||
public void update(GameContainer container, int delta, int mouseX, int mouseY) {
|
||||
float center = container.getWidth() / 2f;
|
||||
boolean centerOffsetUpdated = centerOffset.update(delta);
|
||||
float centerOffsetX = centerOffset.getValue();
|
||||
for (int i = 0; i < buttons.length; i++) {
|
||||
menuButtons[i].hoverUpdate(delta, mouseX, mouseY);
|
||||
|
||||
// move button to center
|
||||
float x = menuButtons[i].getX();
|
||||
if (i % 2 == 0) {
|
||||
if (x < center)
|
||||
menuButtons[i].setX(Math.min(x + (delta / 5f), center));
|
||||
} else {
|
||||
if (x > center)
|
||||
menuButtons[i].setX(Math.max(x - (delta / 5f), center));
|
||||
}
|
||||
if (centerOffsetUpdated)
|
||||
menuButtons[i].setX((i % 2 == 0) ? center + centerOffsetX : center - centerOffsetX);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,9 +406,10 @@ public class ButtonMenu extends BasicGameState {
|
||||
*/
|
||||
public void enter(GameContainer container, StateBasedGame game) {
|
||||
float center = container.getWidth() / 2f;
|
||||
float centerOffset = container.getWidth() * OFFSET_WIDTH_RATIO;
|
||||
float centerOffsetX = container.getWidth() * OFFSET_WIDTH_RATIO;
|
||||
centerOffset = new AnimatedValue(700, centerOffsetX, 0, AnimationEquation.OUT_BOUNCE);
|
||||
for (int i = 0; i < buttons.length; i++) {
|
||||
menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffset * -1 : centerOffset));
|
||||
menuButtons[i].setX(center + ((i % 2 == 0) ? centerOffsetX : centerOffsetX * -1));
|
||||
menuButtons[i].resetHover();
|
||||
}
|
||||
|
||||
@@ -416,8 +419,8 @@ public class ButtonMenu extends BasicGameState {
|
||||
int maxLineWidth = (int) (container.getWidth() * 0.96f);
|
||||
for (int i = 0; i < title.length; i++) {
|
||||
// wrap text if too long
|
||||
if (Utils.FONT_LARGE.getWidth(title[i]) > maxLineWidth) {
|
||||
List<String> list = Utils.wrap(title[i], Utils.FONT_LARGE, maxLineWidth);
|
||||
if (Fonts.LARGE.getWidth(title[i]) > maxLineWidth) {
|
||||
List<String> list = Fonts.wrap(Fonts.LARGE, title[i], maxLineWidth);
|
||||
actualTitle.addAll(list);
|
||||
} else
|
||||
actualTitle.add(title[i]);
|
||||
@@ -545,10 +548,10 @@ public class ButtonMenu extends BasicGameState {
|
||||
};
|
||||
|
||||
/** The text to show on the button. */
|
||||
private String text;
|
||||
private final String text;
|
||||
|
||||
/** The button color. */
|
||||
private Color color;
|
||||
private final Color color;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -591,7 +594,7 @@ public class ButtonMenu extends BasicGameState {
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
private Input input;
|
||||
private int state;
|
||||
private final int state;
|
||||
|
||||
public ButtonMenu(int state) {
|
||||
this.state = state;
|
||||
|
||||
@@ -21,7 +21,6 @@ package itdelatrisu.opsu.states;
|
||||
import itdelatrisu.opsu.GameImage;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.OszUnpacker;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
@@ -29,13 +28,19 @@ import itdelatrisu.opsu.audio.SoundEffect;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||
import itdelatrisu.opsu.beatmap.OszUnpacker;
|
||||
import itdelatrisu.opsu.downloads.Download;
|
||||
import itdelatrisu.opsu.downloads.DownloadList;
|
||||
import itdelatrisu.opsu.downloads.DownloadNode;
|
||||
import itdelatrisu.opsu.downloads.servers.BloodcatServer;
|
||||
import itdelatrisu.opsu.downloads.servers.DownloadServer;
|
||||
import itdelatrisu.opsu.downloads.servers.HexideServer;
|
||||
import itdelatrisu.opsu.downloads.servers.OsuMirrorServer;
|
||||
import itdelatrisu.opsu.downloads.servers.MengSkyServer;
|
||||
import itdelatrisu.opsu.downloads.servers.MnetworkServer;
|
||||
import itdelatrisu.opsu.downloads.servers.YaSOnlineServer;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
import itdelatrisu.opsu.ui.DropdownMenu;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.KinecticScrolling;
|
||||
import itdelatrisu.opsu.ui.MenuButton;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
@@ -75,10 +80,10 @@ public class DownloadsMenu extends BasicGameState {
|
||||
private static final int MIN_REQUEST_INTERVAL = 300;
|
||||
|
||||
/** Available beatmap download servers. */
|
||||
private static final DownloadServer[] SERVERS = { new BloodcatServer(), new OsuMirrorServer(), new HexideServer() };
|
||||
|
||||
/** The beatmap download server index. */
|
||||
private int serverIndex = 0;
|
||||
private static final DownloadServer[] SERVERS = {
|
||||
new BloodcatServer(), new HexideServer(), new YaSOnlineServer(),
|
||||
new MnetworkServer(), new MengSkyServer()
|
||||
};
|
||||
|
||||
/** The current list of search results. */
|
||||
private DownloadNode[] resultList;
|
||||
@@ -137,14 +142,14 @@ public class DownloadsMenu extends BasicGameState {
|
||||
/** Page direction for last query. */
|
||||
private Page lastQueryDir = Page.RESET;
|
||||
|
||||
/** Number of active requests. */
|
||||
private int activeRequests = 0;
|
||||
|
||||
/** Previous and next page buttons. */
|
||||
private MenuButton prevPage, nextPage;
|
||||
|
||||
/** Buttons. */
|
||||
private MenuButton clearButton, importButton, resetButton, rankedButton, serverButton;
|
||||
private MenuButton clearButton, importButton, resetButton, rankedButton;
|
||||
|
||||
/** Dropdown menu. */
|
||||
private DropdownMenu<DownloadServer> serverMenu;
|
||||
|
||||
/** Beatmap importing thread. */
|
||||
private Thread importThread;
|
||||
@@ -155,11 +160,97 @@ public class DownloadsMenu extends BasicGameState {
|
||||
/** The bar notification to send upon entering the state. */
|
||||
private String barNotificationOnLoad;
|
||||
|
||||
/** Search query, executed in {@code queryThread}. */
|
||||
private SearchQuery searchQuery;
|
||||
|
||||
/** Search query helper class. */
|
||||
private class SearchQuery implements Runnable {
|
||||
/** The search query. */
|
||||
private final String query;
|
||||
|
||||
/** The download server. */
|
||||
private final DownloadServer server;
|
||||
|
||||
/** Whether the query was interrupted. */
|
||||
private boolean interrupted = false;
|
||||
|
||||
/** Whether the query has completed execution. */
|
||||
private boolean complete = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param query the search query
|
||||
* @param server the download server
|
||||
*/
|
||||
public SearchQuery(String query, DownloadServer server) {
|
||||
this.query = query;
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
/** Interrupt the query and prevent the results from being processed, if not already complete. */
|
||||
public void interrupt() { interrupted = true; }
|
||||
|
||||
/** Returns whether the query has completed execution. */
|
||||
public boolean isComplete() { return complete; }
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// check page direction
|
||||
Page lastPageDir = pageDir;
|
||||
pageDir = Page.RESET;
|
||||
int lastPageSize = (resultList != null) ? resultList.length : 0;
|
||||
int newPage = page;
|
||||
if (lastPageDir == Page.RESET)
|
||||
newPage = 1;
|
||||
else if (lastPageDir == Page.NEXT)
|
||||
newPage++;
|
||||
else if (lastPageDir == Page.PREVIOUS)
|
||||
newPage--;
|
||||
try {
|
||||
DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
|
||||
if (!interrupted) {
|
||||
// update page total
|
||||
page = newPage;
|
||||
if (nodes != null) {
|
||||
if (lastPageDir == Page.NEXT)
|
||||
pageResultTotal += nodes.length;
|
||||
else if (lastPageDir == Page.PREVIOUS)
|
||||
pageResultTotal -= lastPageSize;
|
||||
else if (lastPageDir == Page.RESET)
|
||||
pageResultTotal = nodes.length;
|
||||
} else
|
||||
pageResultTotal = 0;
|
||||
|
||||
resultList = nodes;
|
||||
totalResults = server.totalResults();
|
||||
focusResult = -1;
|
||||
startResultPos.setPosition(0);
|
||||
if (nodes == null)
|
||||
searchResultString = "An error has occurred.";
|
||||
else {
|
||||
if (query.isEmpty())
|
||||
searchResultString = "Type to search!";
|
||||
else if (totalResults == 0 || resultList.length == 0)
|
||||
searchResultString = "No results found.";
|
||||
else
|
||||
searchResultString = String.format("%d result%s found!",
|
||||
totalResults, (totalResults == 1) ? "" : "s");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (!interrupted)
|
||||
searchResultString = "Could not establish connection to server.";
|
||||
} finally {
|
||||
complete = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// game-related variables
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
private Input input;
|
||||
private int state;
|
||||
private final int state;
|
||||
|
||||
public DownloadsMenu(int state) {
|
||||
this.state = state;
|
||||
@@ -175,17 +266,17 @@ public class DownloadsMenu extends BasicGameState {
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
float baseX = width * 0.024f;
|
||||
float searchY = (height * 0.04f) + Utils.FONT_LARGE.getLineHeight();
|
||||
float searchY = (height * 0.04f) + Fonts.LARGE.getLineHeight();
|
||||
float searchWidth = width * 0.3f;
|
||||
|
||||
// search
|
||||
searchTimer = SEARCH_DELAY;
|
||||
searchResultString = "Loading data from server...";
|
||||
search = new TextField(
|
||||
container, Utils.FONT_DEFAULT, (int) baseX, (int) searchY,
|
||||
(int) searchWidth, Utils.FONT_MEDIUM.getLineHeight()
|
||||
container, Fonts.DEFAULT, (int) baseX, (int) searchY,
|
||||
(int) searchWidth, Fonts.MEDIUM.getLineHeight()
|
||||
);
|
||||
search.setBackgroundColor(DownloadNode.BG_NORMAL);
|
||||
search.setBackgroundColor(Colors.BLACK_BG_NORMAL);
|
||||
search.setBorderColor(Color.white);
|
||||
search.setTextColor(Color.white);
|
||||
search.setConsumeEvents(false);
|
||||
@@ -208,9 +299,8 @@ public class DownloadsMenu extends BasicGameState {
|
||||
float buttonHeight = height * 0.038f;
|
||||
float resetWidth = width * 0.085f;
|
||||
float rankedWidth = width * 0.15f;
|
||||
float serverWidth = width * 0.12f;
|
||||
float lowerWidth = width * 0.12f;
|
||||
float topButtonY = searchY + Utils.FONT_MEDIUM.getLineHeight() / 2f;
|
||||
float topButtonY = searchY + Fonts.MEDIUM.getLineHeight() / 2f;
|
||||
float lowerButtonY = height * 0.995f - searchY - buttonHeight / 2f;
|
||||
Image button = GameImage.MENU_BUTTON_MID.getImage();
|
||||
Image buttonL = GameImage.MENU_BUTTON_LEFT.getImage();
|
||||
@@ -220,11 +310,9 @@ public class DownloadsMenu extends BasicGameState {
|
||||
int lrButtonWidth = buttonL.getWidth() + buttonR.getWidth();
|
||||
Image resetButtonImage = button.getScaledCopy((int) resetWidth - lrButtonWidth, (int) buttonHeight);
|
||||
Image rankedButtonImage = button.getScaledCopy((int) rankedWidth - lrButtonWidth, (int) buttonHeight);
|
||||
Image serverButtonImage = button.getScaledCopy((int) serverWidth - lrButtonWidth, (int) buttonHeight);
|
||||
Image lowerButtonImage = button.getScaledCopy((int) lowerWidth - lrButtonWidth, (int) buttonHeight);
|
||||
float resetButtonWidth = resetButtonImage.getWidth() + lrButtonWidth;
|
||||
float rankedButtonWidth = rankedButtonImage.getWidth() + lrButtonWidth;
|
||||
float serverButtonWidth = serverButtonImage.getWidth() + lrButtonWidth;
|
||||
float lowerButtonWidth = lowerButtonImage.getWidth() + lrButtonWidth;
|
||||
clearButton = new MenuButton(lowerButtonImage, buttonL, buttonR,
|
||||
width * 0.75f + buttonMarginX + lowerButtonWidth / 2f, lowerButtonY);
|
||||
@@ -234,16 +322,48 @@ public class DownloadsMenu extends BasicGameState {
|
||||
baseX + searchWidth + buttonMarginX + resetButtonWidth / 2f, topButtonY);
|
||||
rankedButton = new MenuButton(rankedButtonImage, buttonL, buttonR,
|
||||
baseX + searchWidth + buttonMarginX * 2f + resetButtonWidth + rankedButtonWidth / 2f, topButtonY);
|
||||
serverButton = new MenuButton(serverButtonImage, buttonL, buttonR,
|
||||
baseX + searchWidth + buttonMarginX * 3f + resetButtonWidth + rankedButtonWidth + serverButtonWidth / 2f, topButtonY);
|
||||
clearButton.setText("Clear", Utils.FONT_MEDIUM, Color.white);
|
||||
importButton.setText("Import All", Utils.FONT_MEDIUM, Color.white);
|
||||
resetButton.setText("Reset", Utils.FONT_MEDIUM, Color.white);
|
||||
clearButton.setText("Clear", Fonts.MEDIUM, Color.white);
|
||||
importButton.setText("Import All", Fonts.MEDIUM, Color.white);
|
||||
resetButton.setText("Reset", Fonts.MEDIUM, Color.white);
|
||||
clearButton.setHoverFade();
|
||||
importButton.setHoverFade();
|
||||
resetButton.setHoverFade();
|
||||
rankedButton.setHoverFade();
|
||||
serverButton.setHoverFade();
|
||||
|
||||
// dropdown menu
|
||||
int serverWidth = (int) (width * 0.12f);
|
||||
serverMenu = new DropdownMenu<DownloadServer>(container, SERVERS,
|
||||
baseX + searchWidth + buttonMarginX * 3f + resetButtonWidth + rankedButtonWidth, searchY, serverWidth) {
|
||||
@Override
|
||||
public void itemSelected(int index, DownloadServer item) {
|
||||
resultList = null;
|
||||
startResultPos.setPosition(0);
|
||||
focusResult = -1;
|
||||
totalResults = 0;
|
||||
page = 0;
|
||||
pageResultTotal = 1;
|
||||
pageDir = Page.RESET;
|
||||
searchResultString = "Loading data from server...";
|
||||
lastQuery = null;
|
||||
pageDir = Page.RESET;
|
||||
if (searchQuery != null)
|
||||
searchQuery.interrupt();
|
||||
resetSearchTimer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean menuClicked(int index) {
|
||||
// block input during beatmap importing
|
||||
if (importThread != null)
|
||||
return false;
|
||||
|
||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
serverMenu.setBackgroundColor(Colors.BLACK_BG_HOVER);
|
||||
serverMenu.setBorderColor(Color.black);
|
||||
serverMenu.setChevronRightColor(Color.white);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -252,18 +372,19 @@ public class DownloadsMenu extends BasicGameState {
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
||||
boolean inDropdownMenu = serverMenu.contains(mouseX, mouseY);
|
||||
|
||||
// background
|
||||
GameImage.SEARCH_BG.getImage().draw();
|
||||
|
||||
// title
|
||||
Utils.FONT_LARGE.drawString(width * 0.024f, height * 0.03f, "Download Beatmaps!", Color.white);
|
||||
Fonts.LARGE.drawString(width * 0.024f, height * 0.03f, "Download Beatmaps!", Color.white);
|
||||
|
||||
// search
|
||||
g.setColor(Color.white);
|
||||
g.setLineWidth(2f);
|
||||
search.render(container, g);
|
||||
Utils.FONT_BOLD.drawString(
|
||||
Fonts.BOLD.drawString(
|
||||
search.getX() + search.getWidth() * 0.01f, search.getY() + search.getHeight() * 1.3f,
|
||||
searchResultString, Color.white
|
||||
);
|
||||
@@ -283,7 +404,7 @@ public class DownloadsMenu extends BasicGameState {
|
||||
if (index >= nodes.length)
|
||||
break;
|
||||
nodes[index].drawResult(g, offset + i * DownloadNode.getButtonOffset(),
|
||||
DownloadNode.resultContains(mouseX, mouseY - offset, i),
|
||||
DownloadNode.resultContains(mouseX, mouseY - offset, i) && !inDropdownMenu,
|
||||
(index == focusResult), (previewID == nodes[index].getID()));
|
||||
}
|
||||
g.clearClip();
|
||||
@@ -297,9 +418,9 @@ public class DownloadsMenu extends BasicGameState {
|
||||
float baseX = width * 0.024f;
|
||||
float buttonY = height * 0.2f;
|
||||
float buttonWidth = width * 0.7f;
|
||||
Utils.FONT_BOLD.drawString(
|
||||
baseX + (buttonWidth - Utils.FONT_BOLD.getWidth("Page 1")) / 2f,
|
||||
buttonY - Utils.FONT_BOLD.getLineHeight() * 1.3f,
|
||||
Fonts.BOLD.drawString(
|
||||
baseX + (buttonWidth - Fonts.BOLD.getWidth("Page 1")) / 2f,
|
||||
buttonY - Fonts.BOLD.getLineHeight() * 1.3f,
|
||||
String.format("Page %d", page), Color.white
|
||||
);
|
||||
if (page > 1)
|
||||
@@ -311,10 +432,10 @@ public class DownloadsMenu extends BasicGameState {
|
||||
|
||||
// downloads
|
||||
float downloadsX = width * 0.75f, downloadsY = search.getY();
|
||||
g.setColor(DownloadNode.BG_NORMAL);
|
||||
g.setColor(Colors.BLACK_BG_NORMAL);
|
||||
g.fillRect(downloadsX, downloadsY,
|
||||
width * 0.25f, height - downloadsY * 2f);
|
||||
Utils.FONT_LARGE.drawString(downloadsX + width * 0.015f, downloadsY + height * 0.015f, "Downloads", Color.white);
|
||||
Fonts.LARGE.drawString(downloadsX + width * 0.015f, downloadsY + height * 0.015f, "Downloads", Color.white);
|
||||
int downloadsSize = DownloadList.get().size();
|
||||
if (downloadsSize > 0) {
|
||||
int maxDownloadsShown = DownloadNode.maxDownloadsShown();
|
||||
@@ -344,15 +465,16 @@ public class DownloadsMenu extends BasicGameState {
|
||||
clearButton.draw(Color.gray);
|
||||
importButton.draw(Color.orange);
|
||||
resetButton.draw(Color.red);
|
||||
rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Utils.FONT_MEDIUM, Color.white);
|
||||
rankedButton.setText((rankedOnly) ? "Show Unranked" : "Hide Unranked", Fonts.MEDIUM, Color.white);
|
||||
rankedButton.draw(Color.magenta);
|
||||
serverButton.setText(SERVERS[serverIndex].getName(), Utils.FONT_MEDIUM, Color.white);
|
||||
serverButton.draw(Color.blue);
|
||||
|
||||
// dropdown menu
|
||||
serverMenu.render(container, g);
|
||||
|
||||
// importing beatmaps
|
||||
if (importThread != null) {
|
||||
// darken the screen
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, height);
|
||||
|
||||
UI.drawLoadingProgress(g);
|
||||
@@ -379,7 +501,6 @@ public class DownloadsMenu extends BasicGameState {
|
||||
importButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
resetButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
rankedButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
serverButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
|
||||
if (DownloadList.get() != null)
|
||||
startDownloadIndexPos.setMinMax(0, DownloadNode.getInfoHeight() * (DownloadList.get().size() - DownloadNode.maxDownloadsShown()));
|
||||
@@ -399,72 +520,22 @@ public class DownloadsMenu extends BasicGameState {
|
||||
searchTimer = 0;
|
||||
searchTimerReset = false;
|
||||
|
||||
final String query = search.getText().trim().toLowerCase();
|
||||
final DownloadServer server = SERVERS[serverIndex];
|
||||
String query = search.getText().trim().toLowerCase();
|
||||
DownloadServer server = serverMenu.getSelectedItem();
|
||||
if ((lastQuery == null || !query.equals(lastQuery)) &&
|
||||
(query.length() == 0 || query.length() >= server.minQueryLength())) {
|
||||
lastQuery = query;
|
||||
lastQueryDir = pageDir;
|
||||
|
||||
if (queryThread != null && queryThread.isAlive())
|
||||
if (queryThread != null && queryThread.isAlive()) {
|
||||
queryThread.interrupt();
|
||||
if (searchQuery != null)
|
||||
searchQuery.interrupt();
|
||||
}
|
||||
|
||||
// execute query
|
||||
queryThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
activeRequests++;
|
||||
|
||||
// check page direction
|
||||
Page lastPageDir = pageDir;
|
||||
pageDir = Page.RESET;
|
||||
int lastPageSize = (resultList != null) ? resultList.length : 0;
|
||||
int newPage = page;
|
||||
if (lastPageDir == Page.RESET)
|
||||
newPage = 1;
|
||||
else if (lastPageDir == Page.NEXT)
|
||||
newPage++;
|
||||
else if (lastPageDir == Page.PREVIOUS)
|
||||
newPage--;
|
||||
try {
|
||||
DownloadNode[] nodes = server.resultList(query, newPage, rankedOnly);
|
||||
if (activeRequests - 1 == 0) {
|
||||
// update page total
|
||||
page = newPage;
|
||||
if (nodes != null) {
|
||||
if (lastPageDir == Page.NEXT)
|
||||
pageResultTotal += nodes.length;
|
||||
else if (lastPageDir == Page.PREVIOUS)
|
||||
pageResultTotal -= lastPageSize;
|
||||
else if (lastPageDir == Page.RESET)
|
||||
pageResultTotal = nodes.length;
|
||||
} else
|
||||
pageResultTotal = 0;
|
||||
|
||||
resultList = nodes;
|
||||
totalResults = server.totalResults();
|
||||
focusResult = -1;
|
||||
startResultPos.setPosition(0);
|
||||
if (nodes == null)
|
||||
searchResultString = "An error has occurred.";
|
||||
else {
|
||||
if (query.isEmpty())
|
||||
searchResultString = "Type to search!";
|
||||
else if (totalResults == 0 || resultList.length == 0)
|
||||
searchResultString = "No results found.";
|
||||
else
|
||||
searchResultString = String.format("%d result%s found!",
|
||||
totalResults, (totalResults == 1) ? "" : "s");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
searchResultString = "Could not establish connection to server.";
|
||||
} finally {
|
||||
activeRequests--;
|
||||
queryThread = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
searchQuery = new SearchQuery(query, server);
|
||||
queryThread = new Thread(searchQuery);
|
||||
queryThread.start();
|
||||
}
|
||||
}
|
||||
@@ -474,7 +545,7 @@ public class DownloadsMenu extends BasicGameState {
|
||||
UI.updateTooltip(delta, "Reset the current search.", false);
|
||||
else if (rankedButton.contains(mouseX, mouseY))
|
||||
UI.updateTooltip(delta, "Toggle the display of unranked maps.\nSome download servers may not support this option.", true);
|
||||
else if (serverButton.contains(mouseX, mouseY))
|
||||
else if (serverMenu.baseContains(mouseX, mouseY))
|
||||
UI.updateTooltip(delta, "Select a download server.", false);
|
||||
}
|
||||
|
||||
@@ -534,7 +605,7 @@ public class DownloadsMenu extends BasicGameState {
|
||||
} else {
|
||||
// play preview
|
||||
try {
|
||||
final URL url = new URL(SERVERS[serverIndex].getPreviewURL(node.getID()));
|
||||
final URL url = new URL(serverMenu.getSelectedItem().getPreviewURL(node.getID()));
|
||||
MusicController.pause();
|
||||
new Thread() {
|
||||
@Override
|
||||
@@ -578,7 +649,7 @@ public class DownloadsMenu extends BasicGameState {
|
||||
} else {
|
||||
// start download
|
||||
if (!DownloadList.get().contains(node.getID())) {
|
||||
node.createDownload(SERVERS[serverIndex]);
|
||||
node.createDownload(serverMenu.getSelectedItem());
|
||||
if (node.getDownload() == null)
|
||||
UI.sendBarNotification("The download could not be started.");
|
||||
else {
|
||||
@@ -601,23 +672,27 @@ public class DownloadsMenu extends BasicGameState {
|
||||
// pages
|
||||
if (nodes.length > 0) {
|
||||
if (page > 1 && prevPage.contains(x, y)) {
|
||||
if (lastQueryDir == Page.PREVIOUS && queryThread != null && queryThread.isAlive())
|
||||
if (lastQueryDir == Page.PREVIOUS && searchQuery != null && !searchQuery.isComplete())
|
||||
; // don't send consecutive requests
|
||||
else {
|
||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||
pageDir = Page.PREVIOUS;
|
||||
lastQuery = null;
|
||||
if (searchQuery != null)
|
||||
searchQuery.interrupt();
|
||||
resetSearchTimer();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (pageResultTotal < totalResults && nextPage.contains(x, y)) {
|
||||
if (lastQueryDir == Page.NEXT && queryThread != null && queryThread.isAlive())
|
||||
if (lastQueryDir == Page.NEXT && searchQuery != null && !searchQuery.isComplete())
|
||||
; // don't send consecutive requests
|
||||
else {
|
||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||
pageDir = Page.NEXT;
|
||||
lastQuery = null;
|
||||
if (searchQuery != null)
|
||||
searchQuery.interrupt();
|
||||
resetSearchTimer();
|
||||
return;
|
||||
}
|
||||
@@ -670,6 +745,8 @@ public class DownloadsMenu extends BasicGameState {
|
||||
search.setText("");
|
||||
lastQuery = null;
|
||||
pageDir = Page.RESET;
|
||||
if (searchQuery != null)
|
||||
searchQuery.interrupt();
|
||||
resetSearchTimer();
|
||||
return;
|
||||
}
|
||||
@@ -678,22 +755,8 @@ public class DownloadsMenu extends BasicGameState {
|
||||
rankedOnly = !rankedOnly;
|
||||
lastQuery = null;
|
||||
pageDir = Page.RESET;
|
||||
resetSearchTimer();
|
||||
return;
|
||||
}
|
||||
if (serverButton.contains(x, y)) {
|
||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||
resultList = null;
|
||||
startResultPos.setPosition(0);
|
||||
focusResult = -1;
|
||||
totalResults = 0;
|
||||
page = 0;
|
||||
pageResultTotal = 1;
|
||||
pageDir = Page.RESET;
|
||||
searchResultString = "Loading data from server...";
|
||||
serverIndex = (serverIndex + 1) % SERVERS.length;
|
||||
lastQuery = null;
|
||||
pageDir = Page.RESET;
|
||||
if (searchQuery != null)
|
||||
searchQuery.interrupt();
|
||||
resetSearchTimer();
|
||||
return;
|
||||
}
|
||||
@@ -806,6 +869,8 @@ public class DownloadsMenu extends BasicGameState {
|
||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||
lastQuery = null;
|
||||
pageDir = Page.CURRENT;
|
||||
if (searchQuery != null)
|
||||
searchQuery.interrupt();
|
||||
resetSearchTimer();
|
||||
break;
|
||||
case Input.KEY_F7:
|
||||
@@ -837,7 +902,8 @@ public class DownloadsMenu extends BasicGameState {
|
||||
importButton.resetHover();
|
||||
resetButton.resetHover();
|
||||
rankedButton.resetHover();
|
||||
serverButton.resetHover();
|
||||
serverMenu.activate();
|
||||
serverMenu.reset();
|
||||
focusResult = -1;
|
||||
startResultPos.setPosition(0);
|
||||
startDownloadIndexPos.setPosition(0);
|
||||
@@ -853,6 +919,7 @@ public class DownloadsMenu extends BasicGameState {
|
||||
public void leave(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
search.setFocus(false);
|
||||
serverMenu.deactivate();
|
||||
SoundController.stopTrack();
|
||||
MusicController.resume();
|
||||
}
|
||||
|
||||
@@ -42,12 +42,16 @@ import itdelatrisu.opsu.objects.GameObject;
|
||||
import itdelatrisu.opsu.objects.Slider;
|
||||
import itdelatrisu.opsu.objects.Spinner;
|
||||
import itdelatrisu.opsu.objects.curves.Curve;
|
||||
import itdelatrisu.opsu.objects.curves.Vec2f;
|
||||
import itdelatrisu.opsu.render.FrameBufferCache;
|
||||
import itdelatrisu.opsu.replay.PlaybackSpeed;
|
||||
import itdelatrisu.opsu.replay.Replay;
|
||||
import itdelatrisu.opsu.replay.ReplayFrame;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.MenuButton;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.LinkedList;
|
||||
@@ -116,6 +120,15 @@ public class Game extends BasicGameState {
|
||||
/** Hit object approach time, in milliseconds. */
|
||||
private int approachTime;
|
||||
|
||||
/** The amount of time for hit objects to fade in, in milliseconds. */
|
||||
private int fadeInTime;
|
||||
|
||||
/** Decay time for hit objects in the "Hidden" mod, in milliseconds. */
|
||||
private int hiddenDecayTime;
|
||||
|
||||
/** Time before the hit object time by which the objects have completely faded in the "Hidden" mod, in milliseconds. */
|
||||
private int hiddenTimeDiff;
|
||||
|
||||
/** Time offsets for obtaining each hit result (indexed by HIT_* constants). */
|
||||
private int[] hitResultOffset;
|
||||
|
||||
@@ -146,7 +159,7 @@ public class Game extends BasicGameState {
|
||||
countdown2Sound, countdownGoSound;
|
||||
|
||||
/** Mouse coordinates before game paused. */
|
||||
private int pausedMouseX = -1, pausedMouseY = -1;
|
||||
private Vec2f pausedMousePosition;
|
||||
|
||||
/** Track position when game paused. */
|
||||
private int pauseTime = -1;
|
||||
@@ -209,7 +222,7 @@ public class Game extends BasicGameState {
|
||||
private int flashlightRadius;
|
||||
|
||||
/** The cursor coordinates using the "auto" or "relax" mods. */
|
||||
private int autoMouseX = 0, autoMouseY = 0;
|
||||
private Vec2f autoMousePosition;
|
||||
|
||||
/** Whether or not the cursor should be pressed using the "auto" mod. */
|
||||
private boolean autoMousePressed;
|
||||
@@ -220,11 +233,20 @@ public class Game extends BasicGameState {
|
||||
/** Whether the game is currently seeking to a replay position. */
|
||||
private boolean isSeeking;
|
||||
|
||||
/** Music position bar coordinates and dimensions (for replay seeking). */
|
||||
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
|
||||
|
||||
/** Music position bar background colors. */
|
||||
private static final Color
|
||||
MUSICBAR_NORMAL = new Color(12, 9, 10, 0.25f),
|
||||
MUSICBAR_HOVER = new Color(12, 9, 10, 0.35f),
|
||||
MUSICBAR_FILL = new Color(255, 255, 255, 0.75f);
|
||||
|
||||
// game-related variables
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
private Input input;
|
||||
private int state;
|
||||
private final int state;
|
||||
|
||||
public Game(int state) {
|
||||
this.state = state;
|
||||
@@ -245,6 +267,12 @@ public class Game extends BasicGameState {
|
||||
gOffscreen = offscreen.getGraphics();
|
||||
gOffscreen.setBackground(Color.black);
|
||||
|
||||
// initialize music position bar location
|
||||
musicBarX = width * 0.01f;
|
||||
musicBarY = height * 0.05f;
|
||||
musicBarWidth = Math.max(width * 0.005f, 7);
|
||||
musicBarHeight = height * 0.9f;
|
||||
|
||||
// create the associated GameData object
|
||||
data = new GameData(width, height);
|
||||
}
|
||||
@@ -278,7 +306,7 @@ public class Game extends BasicGameState {
|
||||
else
|
||||
dimLevel = 1f;
|
||||
}
|
||||
if (Options.isDefaultPlayfieldForced() || !beatmap.drawBG(width, height, dimLevel, false)) {
|
||||
if (Options.isDefaultPlayfieldForced() || !beatmap.drawBackground(width, height, dimLevel, false)) {
|
||||
Image playfield = GameImage.PLAYFIELD.getImage();
|
||||
playfield.setAlpha(dimLevel);
|
||||
playfield.draw();
|
||||
@@ -290,32 +318,31 @@ public class Game extends BasicGameState {
|
||||
|
||||
// "auto" and "autopilot" mods: move cursor automatically
|
||||
// TODO: this should really be in update(), not render()
|
||||
autoMouseX = width / 2;
|
||||
autoMouseY = height / 2;
|
||||
autoMousePosition.set(width / 2, height / 2);
|
||||
autoMousePressed = false;
|
||||
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
||||
float[] autoXY = null;
|
||||
Vec2f autoPoint = null;
|
||||
if (isLeadIn()) {
|
||||
// lead-in
|
||||
float progress = Math.max((float) (leadInTime - beatmap.audioLeadIn) / approachTime, 0f);
|
||||
autoMouseY = (int) (height / (2f - progress));
|
||||
autoMousePosition.y = height / (2f - progress);
|
||||
} else if (objectIndex == 0 && trackPosition < firstObjectTime) {
|
||||
// before first object
|
||||
timeDiff = firstObjectTime - trackPosition;
|
||||
if (timeDiff < approachTime) {
|
||||
float[] xy = gameObjects[0].getPointAt(trackPosition);
|
||||
autoXY = getPointAt(autoMouseX, autoMouseY, xy[0], xy[1], 1f - ((float) timeDiff / approachTime));
|
||||
Vec2f point = gameObjects[0].getPointAt(trackPosition);
|
||||
autoPoint = getPointAt(autoMousePosition.x, autoMousePosition.y, point.x, point.y, 1f - ((float) timeDiff / approachTime));
|
||||
}
|
||||
} else if (objectIndex < beatmap.objects.length) {
|
||||
// normal object
|
||||
int objectTime = beatmap.objects[objectIndex].getTime();
|
||||
if (trackPosition < objectTime) {
|
||||
float[] xyStart = gameObjects[objectIndex - 1].getPointAt(trackPosition);
|
||||
Vec2f startPoint = gameObjects[objectIndex - 1].getPointAt(trackPosition);
|
||||
int startTime = gameObjects[objectIndex - 1].getEndTime();
|
||||
if (beatmap.breaks != null && breakIndex < beatmap.breaks.size()) {
|
||||
// starting a break: keep cursor at previous hit object position
|
||||
if (breakTime > 0 || objectTime > beatmap.breaks.get(breakIndex))
|
||||
autoXY = xyStart;
|
||||
autoPoint = startPoint;
|
||||
|
||||
// after a break ends: move startTime to break end time
|
||||
else if (breakIndex > 1) {
|
||||
@@ -324,10 +351,10 @@ public class Game extends BasicGameState {
|
||||
startTime = lastBreakEndTime;
|
||||
}
|
||||
}
|
||||
if (autoXY == null) {
|
||||
float[] xyEnd = gameObjects[objectIndex].getPointAt(trackPosition);
|
||||
if (autoPoint == null) {
|
||||
Vec2f endPoint = gameObjects[objectIndex].getPointAt(trackPosition);
|
||||
int totalTime = objectTime - startTime;
|
||||
autoXY = getPointAt(xyStart[0], xyStart[1], xyEnd[0], xyEnd[1], (float) (trackPosition - startTime) / totalTime);
|
||||
autoPoint = getPointAt(startPoint.x, startPoint.y, endPoint.x, endPoint.y, (float) (trackPosition - startTime) / totalTime);
|
||||
|
||||
// hit circles: show a mouse press
|
||||
int offset300 = hitResultOffset[GameData.HIT_300];
|
||||
@@ -336,19 +363,17 @@ public class Game extends BasicGameState {
|
||||
autoMousePressed = true;
|
||||
}
|
||||
} else {
|
||||
autoXY = gameObjects[objectIndex].getPointAt(trackPosition);
|
||||
autoPoint = gameObjects[objectIndex].getPointAt(trackPosition);
|
||||
autoMousePressed = true;
|
||||
}
|
||||
} else {
|
||||
// last object
|
||||
autoXY = gameObjects[objectIndex - 1].getPointAt(trackPosition);
|
||||
autoPoint = gameObjects[objectIndex - 1].getPointAt(trackPosition);
|
||||
}
|
||||
|
||||
// set mouse coordinates
|
||||
if (autoXY != null) {
|
||||
autoMouseX = (int) autoXY[0];
|
||||
autoMouseY = (int) autoXY[1];
|
||||
}
|
||||
if (autoPoint != null)
|
||||
autoMousePosition.set(autoPoint.x, autoPoint.y);
|
||||
}
|
||||
|
||||
// "flashlight" mod: restricted view of hit objects around cursor
|
||||
@@ -366,12 +391,12 @@ public class Game extends BasicGameState {
|
||||
g.setDrawMode(Graphics.MODE_ALPHA_MAP);
|
||||
g.clearAlphaMap();
|
||||
int mouseX, mouseY;
|
||||
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
|
||||
mouseX = pausedMouseX;
|
||||
mouseY = pausedMouseY;
|
||||
if (pauseTime > -1 && pausedMousePosition != null) {
|
||||
mouseX = (int) pausedMousePosition.x;
|
||||
mouseY = (int) pausedMousePosition.y;
|
||||
} else if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
||||
mouseX = autoMouseX;
|
||||
mouseY = autoMouseY;
|
||||
mouseX = (int) autoMousePosition.x;
|
||||
mouseY = (int) autoMousePosition.y;
|
||||
} else if (isReplay) {
|
||||
mouseX = replayX;
|
||||
mouseY = replayY;
|
||||
@@ -455,15 +480,15 @@ public class Game extends BasicGameState {
|
||||
GameImage.SCOREBAR_BG.getImage().getHeight(),
|
||||
GameImage.SCOREBAR_KI.getImage().getHeight()
|
||||
);
|
||||
float oldAlpha = Utils.COLOR_WHITE_FADE.a;
|
||||
float oldAlpha = Colors.WHITE_FADE.a;
|
||||
if (timeDiff < -500)
|
||||
Utils.COLOR_WHITE_FADE.a = (1000 + timeDiff) / 500f;
|
||||
Utils.FONT_MEDIUM.drawString(
|
||||
Colors.WHITE_FADE.a = (1000 + timeDiff) / 500f;
|
||||
Fonts.MEDIUM.drawString(
|
||||
2 + (width / 100), retryHeight,
|
||||
String.format("%d retries and counting...", retries),
|
||||
Utils.COLOR_WHITE_FADE
|
||||
Colors.WHITE_FADE
|
||||
);
|
||||
Utils.COLOR_WHITE_FADE.a = oldAlpha;
|
||||
Colors.WHITE_FADE.a = oldAlpha;
|
||||
}
|
||||
|
||||
if (isLeadIn())
|
||||
@@ -525,28 +550,40 @@ public class Game extends BasicGameState {
|
||||
if (isReplay || GameMod.AUTO.isActive())
|
||||
playbackSpeed.getButton().draw();
|
||||
|
||||
// draw music position bar (for replay seeking)
|
||||
if (isReplay && Options.isReplaySeekingEnabled()) {
|
||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
||||
g.setColor((musicPositionBarContains(mouseX, mouseY)) ? MUSICBAR_HOVER : MUSICBAR_NORMAL);
|
||||
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
|
||||
if (!isLeadIn()) {
|
||||
g.setColor(MUSICBAR_FILL);
|
||||
float musicBarPosition = Math.min((float) trackPosition / beatmap.endTime, 1f);
|
||||
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight * musicBarPosition, 4);
|
||||
}
|
||||
}
|
||||
|
||||
// returning from pause screen
|
||||
if (pauseTime > -1 && pausedMouseX > -1 && pausedMouseY > -1) {
|
||||
if (pauseTime > -1 && pausedMousePosition != null) {
|
||||
// darken the screen
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, height);
|
||||
|
||||
// draw glowing hit select circle and pulse effect
|
||||
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth();
|
||||
Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleRadius, circleRadius);
|
||||
int circleDiameter = GameImage.HITCIRCLE.getImage().getWidth();
|
||||
Image cursorCircle = GameImage.HITCIRCLE_SELECT.getImage().getScaledCopy(circleDiameter, circleDiameter);
|
||||
cursorCircle.setAlpha(1.0f);
|
||||
cursorCircle.drawCentered(pausedMouseX, pausedMouseY);
|
||||
cursorCircle.drawCentered(pausedMousePosition.x, pausedMousePosition.y);
|
||||
Image cursorCirclePulse = cursorCircle.getScaledCopy(1f + pausePulse);
|
||||
cursorCirclePulse.setAlpha(1f - pausePulse);
|
||||
cursorCirclePulse.drawCentered(pausedMouseX, pausedMouseY);
|
||||
cursorCirclePulse.drawCentered(pausedMousePosition.x, pausedMousePosition.y);
|
||||
}
|
||||
|
||||
if (isReplay)
|
||||
UI.draw(g, replayX, replayY, replayKeyPressed);
|
||||
else if (GameMod.AUTO.isActive())
|
||||
UI.draw(g, autoMouseX, autoMouseY, autoMousePressed);
|
||||
UI.draw(g, (int) autoMousePosition.x, (int) autoMousePosition.y, autoMousePressed);
|
||||
else if (GameMod.AUTOPILOT.isActive())
|
||||
UI.draw(g, autoMouseX, autoMouseY, Utils.isGameKeyPressed());
|
||||
UI.draw(g, (int) autoMousePosition.x, (int) autoMousePosition.y, Utils.isGameKeyPressed());
|
||||
else
|
||||
UI.draw(g);
|
||||
}
|
||||
@@ -564,8 +601,7 @@ public class Game extends BasicGameState {
|
||||
// returning from pause screen: must click previous mouse position
|
||||
if (pauseTime > -1) {
|
||||
// paused during lead-in or break, or "relax" or "autopilot": continue immediately
|
||||
if ((pausedMouseX < 0 && pausedMouseY < 0) ||
|
||||
(GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
|
||||
if (pausedMousePosition == null || (GameMod.RELAX.isActive() || GameMod.AUTOPILOT.isActive())) {
|
||||
pauseTime = -1;
|
||||
if (!isLeadIn())
|
||||
MusicController.resume();
|
||||
@@ -603,6 +639,17 @@ public class Game extends BasicGameState {
|
||||
return;
|
||||
}
|
||||
|
||||
// "Easy" mod: multiple "lives"
|
||||
if (GameMod.EASY.isActive() && deathTime > -1) {
|
||||
if (data.getHealth() < 99f) {
|
||||
data.changeHealth(delta / 10f);
|
||||
data.updateDisplays(delta);
|
||||
return;
|
||||
}
|
||||
MusicController.resume();
|
||||
deathTime = -1;
|
||||
}
|
||||
|
||||
// normal game update
|
||||
if (!isReplay)
|
||||
addReplayFrameAndRun(mouseX, mouseY, lastKeysPressed, trackPosition);
|
||||
@@ -613,7 +660,7 @@ public class Game extends BasicGameState {
|
||||
if (replayIndex >= replay.frames.length)
|
||||
updateGame(replayX, replayY, delta, MusicController.getPosition(), lastKeysPressed);
|
||||
|
||||
//TODO probably should to disable sounds then reseek to the new position
|
||||
// seeking to a position earlier than original track position
|
||||
if (isSeeking && replayIndex - 1 >= 1 && replayIndex < replay.frames.length &&
|
||||
trackPosition < replay.frames[replayIndex - 1].getTime()) {
|
||||
replayIndex = 0;
|
||||
@@ -633,7 +680,6 @@ public class Game extends BasicGameState {
|
||||
timingPointIndex++;
|
||||
}
|
||||
}
|
||||
isSeeking = false;
|
||||
}
|
||||
|
||||
// update and run replay frames
|
||||
@@ -648,6 +694,12 @@ public class Game extends BasicGameState {
|
||||
}
|
||||
mouseX = replayX;
|
||||
mouseY = replayY;
|
||||
|
||||
// unmute sounds
|
||||
if (isSeeking) {
|
||||
isSeeking = false;
|
||||
SoundController.mute(false);
|
||||
}
|
||||
}
|
||||
|
||||
data.updateDisplays(delta);
|
||||
@@ -662,16 +714,6 @@ public class Game extends BasicGameState {
|
||||
* @param keys the keys that are pressed
|
||||
*/
|
||||
private void updateGame(int mouseX, int mouseY, int delta, int trackPosition, int keys) {
|
||||
// "Easy" mod: multiple "lives"
|
||||
if (GameMod.EASY.isActive() && deathTime > -1) {
|
||||
if (data.getHealth() < 99f)
|
||||
data.changeHealth(delta / 10f);
|
||||
else {
|
||||
MusicController.resume();
|
||||
deathTime = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// map complete!
|
||||
if (objectIndex >= gameObjects.length || (MusicController.trackEnded() && objectIndex > 0)) {
|
||||
// track ended before last object was processed: force a hit result
|
||||
@@ -699,6 +741,7 @@ public class Game extends BasicGameState {
|
||||
r.save();
|
||||
}
|
||||
ScoreData score = data.getScoreData(beatmap);
|
||||
data.setGameplay(!isReplay);
|
||||
|
||||
// add score to database
|
||||
if (!unranked && !isReplay)
|
||||
@@ -745,8 +788,7 @@ public class Game extends BasicGameState {
|
||||
// pause game if focus lost
|
||||
if (!container.hasFocus() && !GameMod.AUTO.isActive() && !isReplay) {
|
||||
if (pauseTime < 0) {
|
||||
pausedMouseX = mouseX;
|
||||
pausedMouseY = mouseY;
|
||||
pausedMousePosition = new Vec2f(mouseX, mouseY);
|
||||
pausePulse = 0f;
|
||||
}
|
||||
if (MusicController.isPlaying() || isLeadIn())
|
||||
@@ -819,8 +861,7 @@ public class Game extends BasicGameState {
|
||||
|
||||
// pause game
|
||||
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
|
||||
pausedMouseX = mouseX;
|
||||
pausedMouseY = mouseY;
|
||||
pausedMousePosition = new Vec2f(mouseX, mouseY);
|
||||
pausePulse = 0f;
|
||||
}
|
||||
if (MusicController.isPlaying() || isLeadIn())
|
||||
@@ -888,6 +929,13 @@ public class Game extends BasicGameState {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case Input.KEY_F:
|
||||
// change playback speed
|
||||
if (isReplay || GameMod.AUTO.isActive()) {
|
||||
playbackSpeed = playbackSpeed.next();
|
||||
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
|
||||
}
|
||||
break;
|
||||
case Input.KEY_UP:
|
||||
UI.changeVolume(1);
|
||||
break;
|
||||
@@ -923,9 +971,10 @@ public class Game extends BasicGameState {
|
||||
MusicController.setPitch(GameMod.getSpeedMultiplier() * playbackSpeed.getModifier());
|
||||
}
|
||||
|
||||
// TODO
|
||||
else if (!GameMod.AUTO.isActive() && y < 50) {
|
||||
float pos = (float) x / container.getWidth() * beatmap.endTime;
|
||||
// replay seeking
|
||||
else if (Options.isReplaySeekingEnabled() && !GameMod.AUTO.isActive() && musicPositionBarContains(x, y)) {
|
||||
SoundController.mute(true); // mute sounds while seeking
|
||||
float pos = (y - musicBarY) / musicBarHeight * beatmap.endTime;
|
||||
MusicController.setPosition((int) pos);
|
||||
isSeeking = true;
|
||||
}
|
||||
@@ -939,8 +988,7 @@ public class Game extends BasicGameState {
|
||||
if (button == Input.MOUSE_MIDDLE_BUTTON && !Options.isMouseWheelDisabled()) {
|
||||
int trackPosition = MusicController.getPosition();
|
||||
if (pauseTime < 0 && breakTime <= 0 && trackPosition >= beatmap.objects[0].getTime()) {
|
||||
pausedMouseX = x;
|
||||
pausedMouseY = y;
|
||||
pausedMousePosition = new Vec2f(x, y);
|
||||
pausePulse = 0f;
|
||||
}
|
||||
if (MusicController.isPlaying() || isLeadIn())
|
||||
@@ -969,13 +1017,12 @@ public class Game extends BasicGameState {
|
||||
private void gameKeyPressed(int keys, int x, int y, int trackPosition) {
|
||||
// returning from pause screen
|
||||
if (pauseTime > -1) {
|
||||
double distance = Math.hypot(pausedMouseX - x, pausedMouseY - y);
|
||||
double distance = Math.hypot(pausedMousePosition.x - x, pausedMousePosition.y - y);
|
||||
int circleRadius = GameImage.HITCIRCLE.getImage().getWidth() / 2;
|
||||
if (distance < circleRadius) {
|
||||
// unpause the game
|
||||
pauseTime = -1;
|
||||
pausedMouseX = -1;
|
||||
pausedMouseY = -1;
|
||||
pausedMousePosition = null;
|
||||
if (!isLeadIn())
|
||||
MusicController.resume();
|
||||
}
|
||||
@@ -1065,6 +1112,15 @@ public class Game extends BasicGameState {
|
||||
|
||||
// restart the game
|
||||
if (restart != Restart.FALSE) {
|
||||
// load mods
|
||||
if (isReplay) {
|
||||
previousMods = GameMod.getModState();
|
||||
GameMod.loadModState(replay.mods);
|
||||
}
|
||||
|
||||
data.setGameplay(true);
|
||||
|
||||
// check restart state
|
||||
if (restart == Restart.NEW) {
|
||||
// new game
|
||||
loadImages();
|
||||
@@ -1149,10 +1205,6 @@ public class Game extends BasicGameState {
|
||||
|
||||
// load replay frames
|
||||
if (isReplay) {
|
||||
// load mods
|
||||
previousMods = GameMod.getModState();
|
||||
GameMod.loadModState(replay.mods);
|
||||
|
||||
// load initial data
|
||||
replayX = container.getWidth() / 2;
|
||||
replayY = container.getHeight() / 2;
|
||||
@@ -1188,6 +1240,8 @@ public class Game extends BasicGameState {
|
||||
MusicController.setPosition(0);
|
||||
MusicController.setPitch(GameMod.getSpeedMultiplier());
|
||||
MusicController.pause();
|
||||
|
||||
SoundController.mute(false);
|
||||
}
|
||||
|
||||
skipButton.resetHover();
|
||||
@@ -1242,10 +1296,10 @@ public class Game extends BasicGameState {
|
||||
final int followPointInterval = container.getHeight() / 14;
|
||||
int lastObjectEndTime = gameObjects[lastObjectIndex].getEndTime() + 1;
|
||||
int objectStartTime = beatmap.objects[index].getTime();
|
||||
float[] startXY = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
|
||||
float[] endXY = gameObjects[index].getPointAt(objectStartTime);
|
||||
float xDiff = endXY[0] - startXY[0];
|
||||
float yDiff = endXY[1] - startXY[1];
|
||||
Vec2f startPoint = gameObjects[lastObjectIndex].getPointAt(lastObjectEndTime);
|
||||
Vec2f endPoint = gameObjects[index].getPointAt(objectStartTime);
|
||||
float xDiff = endPoint.x - startPoint.x;
|
||||
float yDiff = endPoint.y - startPoint.y;
|
||||
float dist = (float) Math.hypot(xDiff, yDiff);
|
||||
int numPoints = (int) ((dist - GameImage.HITCIRCLE.getImage().getWidth()) / followPointInterval);
|
||||
if (numPoints > 0) {
|
||||
@@ -1266,8 +1320,8 @@ public class Game extends BasicGameState {
|
||||
float step = 1f / (numPoints + 1);
|
||||
float t = step;
|
||||
for (int i = 0; i < numPoints; i++) {
|
||||
float x = startXY[0] + xDiff * t;
|
||||
float y = startXY[1] + yDiff * t;
|
||||
float x = startPoint.x + xDiff * t;
|
||||
float y = startPoint.y + yDiff * t;
|
||||
float nextT = t + step;
|
||||
if (lastObjectIndex < objectIndex) { // fade the previous trail
|
||||
if (progress < nextT) {
|
||||
@@ -1321,8 +1375,7 @@ public class Game extends BasicGameState {
|
||||
timingPointIndex = 0;
|
||||
beatLengthBase = beatLength = 1;
|
||||
pauseTime = -1;
|
||||
pausedMouseX = -1;
|
||||
pausedMouseY = -1;
|
||||
pausedMousePosition = null;
|
||||
countdownReadySound = false;
|
||||
countdown3Sound = false;
|
||||
countdown1Sound = false;
|
||||
@@ -1333,8 +1386,7 @@ public class Game extends BasicGameState {
|
||||
deathTime = -1;
|
||||
replayFrames = null;
|
||||
lastReplayTime = 0;
|
||||
autoMouseX = 0;
|
||||
autoMouseY = 0;
|
||||
autoMousePosition = new Vec2f();
|
||||
autoMousePressed = false;
|
||||
flashlightRadius = container.getHeight() * 2 / 3;
|
||||
|
||||
@@ -1376,9 +1428,9 @@ public class Game extends BasicGameState {
|
||||
// set images
|
||||
File parent = beatmap.getFile().getParentFile();
|
||||
for (GameImage img : GameImage.values()) {
|
||||
if (img.isSkinnable()) {
|
||||
if (img.isBeatmapSkinnable()) {
|
||||
img.setDefaultImage();
|
||||
img.setSkinImage(parent);
|
||||
img.setBeatmapSkinImage(parent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1390,6 +1442,8 @@ public class Game extends BasicGameState {
|
||||
Image skip = GameImage.SKIP.getImage();
|
||||
skipButton = new MenuButton(skip, width - skip.getWidth() / 2f, height - (skip.getHeight() / 2f));
|
||||
}
|
||||
skipButton.setHoverAnimationDuration(350);
|
||||
skipButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
|
||||
skipButton.setHoverExpand(1.1f, MenuButton.Expand.UP_LEFT);
|
||||
|
||||
// load other images...
|
||||
@@ -1420,14 +1474,15 @@ public class Game extends BasicGameState {
|
||||
|
||||
// Stack modifier scales with hit object size
|
||||
// StackOffset = HitObjectRadius / 10
|
||||
int diameter = (int) (104 - (circleSize * 8));
|
||||
//int diameter = (int) (104 - (circleSize * 8));
|
||||
float diameter = 108.848f - (circleSize * 8.9646f);
|
||||
HitObject.setStackOffset(diameter * STACK_OFFSET_MODIFIER);
|
||||
|
||||
// initialize objects
|
||||
Circle.init(container, circleSize);
|
||||
Slider.init(container, circleSize, beatmap);
|
||||
Circle.init(container, diameter);
|
||||
Slider.init(container, diameter, beatmap);
|
||||
Spinner.init(container, overallDifficulty);
|
||||
Curve.init(container.getWidth(), container.getHeight(), circleSize, (Options.isBeatmapSkinIgnored()) ?
|
||||
Curve.init(container.getWidth(), container.getHeight(), diameter, (Options.isBeatmapSkinIgnored()) ?
|
||||
Options.getSkin().getSliderBorderColor() : beatmap.getSliderBorderColor());
|
||||
|
||||
// approachRate (hit object approach time)
|
||||
@@ -1438,9 +1493,9 @@ public class Game extends BasicGameState {
|
||||
|
||||
// overallDifficulty (hit result time offsets)
|
||||
hitResultOffset = new int[GameData.HIT_MAX];
|
||||
hitResultOffset[GameData.HIT_300] = (int) (78 - (overallDifficulty * 6));
|
||||
hitResultOffset[GameData.HIT_100] = (int) (138 - (overallDifficulty * 8));
|
||||
hitResultOffset[GameData.HIT_50] = (int) (198 - (overallDifficulty * 10));
|
||||
hitResultOffset[GameData.HIT_300] = (int) (79.5f - (overallDifficulty * 6));
|
||||
hitResultOffset[GameData.HIT_100] = (int) (139.5f - (overallDifficulty * 8));
|
||||
hitResultOffset[GameData.HIT_50] = (int) (199.5f - (overallDifficulty * 10));
|
||||
hitResultOffset[GameData.HIT_MISS] = (int) (500 - (overallDifficulty * 10));
|
||||
//final float mult = 0.608f;
|
||||
//hitResultOffset[GameData.HIT_300] = (int) ((128 - (overallDifficulty * 9.6)) * mult);
|
||||
@@ -1454,6 +1509,14 @@ public class Game extends BasicGameState {
|
||||
|
||||
// difficulty multiplier (scoring)
|
||||
data.calculateDifficultyMultiplier(beatmap.HPDrainRate, beatmap.circleSize, beatmap.overallDifficulty);
|
||||
|
||||
// hit object fade-in time (TODO: formula)
|
||||
fadeInTime = Math.min(375, (int) (approachTime / 2.5f));
|
||||
|
||||
// fade times ("Hidden" mod)
|
||||
// TODO: find the actual formulas for this
|
||||
hiddenDecayTime = (int) (approachTime / 3.6f);
|
||||
hiddenTimeDiff = (int) (approachTime / 3.3f);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1477,6 +1540,22 @@ public class Game extends BasicGameState {
|
||||
*/
|
||||
public int getApproachTime() { return approachTime; }
|
||||
|
||||
/**
|
||||
* Returns the amount of time for hit objects to fade in, in milliseconds.
|
||||
*/
|
||||
public int getFadeInTime() { return fadeInTime; }
|
||||
|
||||
/**
|
||||
* Returns the object decay time in the "Hidden" mod, in milliseconds.
|
||||
*/
|
||||
public int getHiddenDecayTime() { return hiddenDecayTime; }
|
||||
|
||||
/**
|
||||
* Returns the time before the hit object time by which the objects have
|
||||
* completely faded in the "Hidden" mod, in milliseconds.
|
||||
*/
|
||||
public int getHiddenTimeDiff() { return hiddenTimeDiff; }
|
||||
|
||||
/**
|
||||
* Returns an array of hit result offset times, in milliseconds (indexed by GameData.HIT_* constants).
|
||||
*/
|
||||
@@ -1536,8 +1615,8 @@ public class Game extends BasicGameState {
|
||||
public synchronized void addReplayFrameAndRun(int x, int y, int keys, int time){
|
||||
// "auto" and "autopilot" mods: use automatic cursor coordinates
|
||||
if (GameMod.AUTO.isActive() || GameMod.AUTOPILOT.isActive()) {
|
||||
x = autoMouseX;
|
||||
y = autoMouseY;
|
||||
x = (int) autoMousePosition.x;
|
||||
y = (int) autoMousePosition.y;
|
||||
}
|
||||
|
||||
ReplayFrame frame = addReplayFrame(x, y, keys, time);
|
||||
@@ -1611,17 +1690,13 @@ public class Game extends BasicGameState {
|
||||
* @param endX the ending x coordinate
|
||||
* @param endY the ending y coordinate
|
||||
* @param t the t value [0, 1]
|
||||
* @return the [x,y] coordinates
|
||||
* @return the position vector
|
||||
*/
|
||||
private float[] getPointAt(float startX, float startY, float endX, float endY, float t) {
|
||||
private Vec2f getPointAt(float startX, float startY, float endX, float endY, float t) {
|
||||
// "autopilot" mod: move quicker between objects
|
||||
if (GameMod.AUTOPILOT.isActive())
|
||||
t = Utils.clamp(t * 2f, 0f, 1f);
|
||||
|
||||
float[] xy = new float[2];
|
||||
xy[0] = startX + (endX - startX) * t;
|
||||
xy[1] = startY + (endY - startY) * t;
|
||||
return xy;
|
||||
return new Vec2f(startX + (endX - startX) * t, startY + (endY - startY) * t);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1715,9 +1790,9 @@ public class Game extends BasicGameState {
|
||||
// possible special case: if slider end in the stack,
|
||||
// all next hit objects in stack move right down
|
||||
if (hitObjectN.isSlider()) {
|
||||
float[] p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
|
||||
float[] p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
|
||||
float distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
|
||||
Vec2f p1 = gameObjects[i].getPointAt(hitObjectI.getTime());
|
||||
Vec2f p2 = gameObjects[n].getPointAt(gameObjects[n].getEndTime());
|
||||
float distance = Utils.distance(p1.x, p1.y, p2.x, p2.y);
|
||||
|
||||
// check if hit object part of this stack
|
||||
if (distance < STACK_LENIENCE * HitObject.getXMultiplier()) {
|
||||
@@ -1725,7 +1800,7 @@ public class Game extends BasicGameState {
|
||||
for (int j = n + 1; j <= i; j++) {
|
||||
HitObject hitObjectJ = beatmap.objects[j];
|
||||
p1 = gameObjects[j].getPointAt(hitObjectJ.getTime());
|
||||
distance = Utils.distance(p1[0], p1[1], p2[0], p2[1]);
|
||||
distance = Utils.distance(p1.x, p1.y, p2.x, p2.y);
|
||||
|
||||
// hit object below slider end
|
||||
if (distance < STACK_LENIENCE * HitObject.getXMultiplier())
|
||||
@@ -1753,4 +1828,14 @@ public class Game extends BasicGameState {
|
||||
gameObjects[i].updatePosition();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the coordinates are within the music position bar bounds.
|
||||
* @param cx the x coordinate
|
||||
* @param cy the y coordinate
|
||||
*/
|
||||
private boolean musicPositionBarContains(float cx, float cy) {
|
||||
return ((cx > musicBarX && cx < musicBarX + musicBarWidth) &&
|
||||
(cy > musicBarY && cy < musicBarY + musicBarHeight));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.audio.SoundEffect;
|
||||
import itdelatrisu.opsu.ui.MenuButton;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import org.lwjgl.input.Keyboard;
|
||||
import org.newdawn.slick.Color;
|
||||
@@ -61,7 +62,7 @@ public class GamePauseMenu extends BasicGameState {
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
private Input input;
|
||||
private int state;
|
||||
private final int state;
|
||||
private Game gameState;
|
||||
|
||||
public GamePauseMenu(int state) {
|
||||
@@ -86,10 +87,10 @@ public class GamePauseMenu extends BasicGameState {
|
||||
|
||||
// don't draw default background if button skinned and background unskinned
|
||||
boolean buttonsSkinned =
|
||||
GameImage.PAUSE_CONTINUE.hasSkinImage() ||
|
||||
GameImage.PAUSE_RETRY.hasSkinImage() ||
|
||||
GameImage.PAUSE_BACK.hasSkinImage();
|
||||
if (!buttonsSkinned || bg.hasSkinImage())
|
||||
GameImage.PAUSE_CONTINUE.hasBeatmapSkinImage() ||
|
||||
GameImage.PAUSE_RETRY.hasBeatmapSkinImage() ||
|
||||
GameImage.PAUSE_BACK.hasBeatmapSkinImage();
|
||||
if (!buttonsSkinned || bg.hasBeatmapSkinImage())
|
||||
bg.getImage().draw();
|
||||
else
|
||||
g.setBackground(Color.black);
|
||||
@@ -133,7 +134,7 @@ public class GamePauseMenu extends BasicGameState {
|
||||
SoundController.playSound(SoundEffect.MENUBACK);
|
||||
((SongMenu) game.getState(Opsu.STATE_SONGMENU)).resetGameDataOnLoad();
|
||||
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
|
||||
if (UI.getCursor().isSkinned())
|
||||
if (UI.getCursor().isBeatmapSkinned())
|
||||
UI.getCursor().reset();
|
||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
} else {
|
||||
@@ -187,7 +188,7 @@ public class GamePauseMenu extends BasicGameState {
|
||||
MusicController.playAt(MusicController.getBeatmap().previewTime, true);
|
||||
else
|
||||
MusicController.resume();
|
||||
if (UI.getCursor().isSkinned())
|
||||
if (UI.getCursor().isBeatmapSkinned())
|
||||
UI.getCursor().reset();
|
||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
}
|
||||
@@ -227,6 +228,14 @@ public class GamePauseMenu extends BasicGameState {
|
||||
continueButton = new MenuButton(GameImage.PAUSE_CONTINUE.getImage(), width / 2f, height * 0.25f);
|
||||
retryButton = new MenuButton(GameImage.PAUSE_RETRY.getImage(), width / 2f, height * 0.5f);
|
||||
backButton = new MenuButton(GameImage.PAUSE_BACK.getImage(), width / 2f, height * 0.75f);
|
||||
final int buttonAnimationDuration = 300;
|
||||
continueButton.setHoverAnimationDuration(buttonAnimationDuration);
|
||||
retryButton.setHoverAnimationDuration(buttonAnimationDuration);
|
||||
backButton.setHoverAnimationDuration(buttonAnimationDuration);
|
||||
final AnimationEquation buttonAnimationEquation = AnimationEquation.IN_OUT_BACK;
|
||||
continueButton.setHoverAnimationEquation(buttonAnimationEquation);
|
||||
retryButton.setHoverAnimationEquation(buttonAnimationEquation);
|
||||
backButton.setHoverAnimationEquation(buttonAnimationEquation);
|
||||
continueButton.setHoverExpand();
|
||||
retryButton.setHoverExpand();
|
||||
backButton.setHoverExpand();
|
||||
|
||||
@@ -20,6 +20,7 @@ package itdelatrisu.opsu.states;
|
||||
|
||||
import itdelatrisu.opsu.GameData;
|
||||
import itdelatrisu.opsu.GameImage;
|
||||
import itdelatrisu.opsu.GameMod;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
@@ -68,7 +69,7 @@ public class GameRanking extends BasicGameState {
|
||||
// game-related variables
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
private int state;
|
||||
private final int state;
|
||||
private Input input;
|
||||
|
||||
public GameRanking(int state) {
|
||||
@@ -105,7 +106,7 @@ public class GameRanking extends BasicGameState {
|
||||
Beatmap beatmap = MusicController.getBeatmap();
|
||||
|
||||
// background
|
||||
if (!beatmap.drawBG(width, height, 0.7f, true))
|
||||
if (!beatmap.drawBackground(width, height, 0.7f, true))
|
||||
GameImage.PLAYFIELD.getImage().draw(0,0);
|
||||
|
||||
// ranking screen elements
|
||||
@@ -113,7 +114,7 @@ public class GameRanking extends BasicGameState {
|
||||
|
||||
// buttons
|
||||
replayButton.draw();
|
||||
if (data.isGameplay())
|
||||
if (data.isGameplay() && !GameMod.AUTO.isActive())
|
||||
retryButton.draw();
|
||||
UI.getBackButton().draw();
|
||||
|
||||
@@ -175,7 +176,8 @@ public class GameRanking extends BasicGameState {
|
||||
// replay
|
||||
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
|
||||
boolean returnToGame = false;
|
||||
if (replayButton.contains(x, y)) {
|
||||
boolean replayButtonPressed = replayButton.contains(x, y);
|
||||
if (replayButtonPressed && !(data.isGameplay() && GameMod.AUTO.isActive())) {
|
||||
Replay r = data.getReplay(null, null);
|
||||
if (r != null) {
|
||||
try {
|
||||
@@ -194,7 +196,9 @@ public class GameRanking extends BasicGameState {
|
||||
}
|
||||
|
||||
// retry
|
||||
else if (data.isGameplay() && retryButton.contains(x, y)) {
|
||||
else if (data.isGameplay() &&
|
||||
(!GameMod.AUTO.isActive() && retryButton.contains(x, y)) ||
|
||||
(GameMod.AUTO.isActive() && replayButtonPressed)) {
|
||||
gameState.setReplay(null);
|
||||
gameState.setRestart(Game.Restart.MANUAL);
|
||||
returnToGame = true;
|
||||
@@ -221,7 +225,7 @@ public class GameRanking extends BasicGameState {
|
||||
} else {
|
||||
SoundController.playSound(SoundEffect.APPLAUSE);
|
||||
retryButton.resetHover();
|
||||
replayButton.setY(replayY);
|
||||
replayButton.setY(!GameMod.AUTO.isActive() ? replayY : retryY);
|
||||
}
|
||||
replayButton.resetHover();
|
||||
}
|
||||
@@ -239,12 +243,11 @@ public class GameRanking extends BasicGameState {
|
||||
*/
|
||||
private void returnToSongMenu() {
|
||||
SoundController.playSound(SoundEffect.MENUBACK);
|
||||
if (data.isGameplay()) {
|
||||
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
|
||||
songMenu.resetGameDataOnLoad();
|
||||
SongMenu songMenu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
|
||||
if (data.isGameplay())
|
||||
songMenu.resetTrackOnLoad();
|
||||
}
|
||||
if (UI.getCursor().isSkinned())
|
||||
songMenu.resetGameDataOnLoad();
|
||||
if (UI.getCursor().isBeatmapSkinned())
|
||||
UI.getCursor().reset();
|
||||
game.enterState(Opsu.STATE_SONGMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
}
|
||||
|
||||
@@ -31,9 +31,13 @@ import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||
import itdelatrisu.opsu.downloads.Updater;
|
||||
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.MenuButton;
|
||||
import itdelatrisu.opsu.ui.MenuButton.Expand;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import java.awt.Desktop;
|
||||
import java.io.IOException;
|
||||
@@ -61,7 +65,7 @@ import org.newdawn.slick.state.transition.FadeOutTransition;
|
||||
*/
|
||||
public class MainMenu extends BasicGameState {
|
||||
/** Idle time, in milliseconds, before returning the logo to its original position. */
|
||||
private static final short MOVE_DELAY = 5000;
|
||||
private static final short LOGO_IDLE_DELAY = 10000;
|
||||
|
||||
/** Max alpha level of the menu background. */
|
||||
private static final float BG_MAX_ALPHA = 0.9f;
|
||||
@@ -69,12 +73,21 @@ public class MainMenu extends BasicGameState {
|
||||
/** Logo button that reveals other buttons on click. */
|
||||
private MenuButton logo;
|
||||
|
||||
/** Whether or not the logo has been clicked. */
|
||||
private boolean logoClicked = false;
|
||||
/** Logo states. */
|
||||
private enum LogoState { DEFAULT, OPENING, OPEN, CLOSING }
|
||||
|
||||
/** Current logo state. */
|
||||
private LogoState logoState = LogoState.DEFAULT;
|
||||
|
||||
/** Delay timer, in milliseconds, before starting to move the logo back to the center. */
|
||||
private int logoTimer = 0;
|
||||
|
||||
/** Logo horizontal offset for opening and closing actions. */
|
||||
private AnimatedValue logoOpen, logoClose;
|
||||
|
||||
/** Logo button alpha levels. */
|
||||
private AnimatedValue logoButtonAlpha;
|
||||
|
||||
/** Main "Play" and "Exit" buttons. */
|
||||
private MenuButton playButton, exitButton;
|
||||
|
||||
@@ -87,8 +100,8 @@ public class MainMenu extends BasicGameState {
|
||||
/** Button linking to repository. */
|
||||
private MenuButton repoButton;
|
||||
|
||||
/** Button for installing updates. */
|
||||
private MenuButton updateButton;
|
||||
/** Buttons for installing updates. */
|
||||
private MenuButton updateButton, restartButton;
|
||||
|
||||
/** Application start time, for drawing the total running time. */
|
||||
private long programStartTime;
|
||||
@@ -97,7 +110,7 @@ public class MainMenu extends BasicGameState {
|
||||
private Stack<Integer> previous;
|
||||
|
||||
/** Background alpha level (for fade-in effect). */
|
||||
private float bgAlpha = 0f;
|
||||
private AnimatedValue bgAlpha = new AnimatedValue(1100, 0f, BG_MAX_ALPHA, AnimationEquation.LINEAR);
|
||||
|
||||
/** Whether or not a notification was already sent upon entering. */
|
||||
private boolean enterNotification = false;
|
||||
@@ -105,16 +118,11 @@ public class MainMenu extends BasicGameState {
|
||||
/** Music position bar coordinates and dimensions. */
|
||||
private float musicBarX, musicBarY, musicBarWidth, musicBarHeight;
|
||||
|
||||
/** Music position bar background colors. */
|
||||
private static final Color
|
||||
BG_NORMAL = new Color(0, 0, 0, 0.25f),
|
||||
BG_HOVER = new Color(0, 0, 0, 0.5f);
|
||||
|
||||
// game-related variables
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
private Input input;
|
||||
private int state;
|
||||
private final int state;
|
||||
|
||||
public MainMenu(int state) {
|
||||
this.state = state;
|
||||
@@ -145,9 +153,18 @@ public class MainMenu extends BasicGameState {
|
||||
exitButton = new MenuButton(exitImg,
|
||||
width * 0.75f - exitOffset, (height / 2) + (exitImg.getHeight() / 2f)
|
||||
);
|
||||
logo.setHoverExpand(1.05f);
|
||||
playButton.setHoverExpand(1.05f);
|
||||
exitButton.setHoverExpand(1.05f);
|
||||
final int logoAnimationDuration = 350;
|
||||
logo.setHoverAnimationDuration(logoAnimationDuration);
|
||||
playButton.setHoverAnimationDuration(logoAnimationDuration);
|
||||
exitButton.setHoverAnimationDuration(logoAnimationDuration);
|
||||
final AnimationEquation logoAnimationEquation = AnimationEquation.IN_OUT_BACK;
|
||||
logo.setHoverAnimationEquation(logoAnimationEquation);
|
||||
playButton.setHoverAnimationEquation(logoAnimationEquation);
|
||||
exitButton.setHoverAnimationEquation(logoAnimationEquation);
|
||||
final float logoHoverScale = 1.08f;
|
||||
logo.setHoverExpand(logoHoverScale);
|
||||
playButton.setHoverExpand(logoHoverScale);
|
||||
exitButton.setHoverExpand(logoHoverScale);
|
||||
|
||||
// initialize music buttons
|
||||
int musicWidth = GameImage.MUSIC_PLAY.getImage().getWidth();
|
||||
@@ -170,24 +187,40 @@ public class MainMenu extends BasicGameState {
|
||||
// initialize downloads button
|
||||
Image dlImg = GameImage.DOWNLOADS.getImage();
|
||||
downloadsButton = new MenuButton(dlImg, width - dlImg.getWidth() / 2f, height / 2f);
|
||||
downloadsButton.setHoverAnimationDuration(350);
|
||||
downloadsButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
|
||||
downloadsButton.setHoverExpand(1.03f, Expand.LEFT);
|
||||
|
||||
// initialize repository button
|
||||
float startX = width * 0.997f, startY = height * 0.997f;
|
||||
if (Desktop.isDesktopSupported()) { // only if a webpage can be opened
|
||||
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { // only if a webpage can be opened
|
||||
Image repoImg = GameImage.REPOSITORY.getImage();
|
||||
repoButton = new MenuButton(repoImg,
|
||||
startX - repoImg.getWidth(), startY - repoImg.getHeight()
|
||||
);
|
||||
repoButton.setHoverAnimationDuration(350);
|
||||
repoButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
|
||||
repoButton.setHoverExpand();
|
||||
startX -= repoImg.getWidth() * 1.75f;
|
||||
} else
|
||||
startX -= width * 0.005f;
|
||||
}
|
||||
|
||||
// initialize update button
|
||||
Image bangImg = GameImage.BANG.getImage();
|
||||
updateButton = new MenuButton(bangImg, startX - bangImg.getWidth(), startY - bangImg.getHeight());
|
||||
updateButton.setHoverExpand(1.15f);
|
||||
// initialize update buttons
|
||||
float updateX = width / 2f, updateY = height * 17 / 18f;
|
||||
Image downloadImg = GameImage.DOWNLOAD.getImage();
|
||||
updateButton = new MenuButton(downloadImg, updateX, updateY);
|
||||
updateButton.setHoverAnimationDuration(400);
|
||||
updateButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_QUAD);
|
||||
updateButton.setHoverExpand(1.1f);
|
||||
Image updateImg = GameImage.UPDATE.getImage();
|
||||
restartButton = new MenuButton(updateImg, updateX, updateY);
|
||||
restartButton.setHoverAnimationDuration(2000);
|
||||
restartButton.setHoverAnimationEquation(AnimationEquation.LINEAR);
|
||||
restartButton.setHoverRotate(360);
|
||||
|
||||
// logo animations
|
||||
float centerOffsetX = width / 5f;
|
||||
logoOpen = new AnimatedValue(400, 0, centerOffsetX, AnimationEquation.OUT_QUAD);
|
||||
logoClose = new AnimatedValue(2200, centerOffsetX, 0, AnimationEquation.OUT_QUAD);
|
||||
logoButtonAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);
|
||||
|
||||
reset();
|
||||
}
|
||||
@@ -201,27 +234,27 @@ public class MainMenu extends BasicGameState {
|
||||
// draw background
|
||||
Beatmap beatmap = MusicController.getBeatmap();
|
||||
if (Options.isDynamicBackgroundEnabled() &&
|
||||
beatmap != null && beatmap.drawBG(width, height, bgAlpha, true))
|
||||
beatmap != null && beatmap.drawBackground(width, height, bgAlpha.getValue(), true))
|
||||
;
|
||||
else {
|
||||
Image bg = GameImage.MENU_BG.getImage();
|
||||
bg.setAlpha(bgAlpha);
|
||||
bg.setAlpha(bgAlpha.getValue());
|
||||
bg.draw();
|
||||
}
|
||||
|
||||
// top/bottom horizontal bars
|
||||
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
|
||||
Utils.COLOR_BLACK_ALPHA.a = 0.2f;
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
float oldAlpha = Colors.BLACK_ALPHA.a;
|
||||
Colors.BLACK_ALPHA.a = 0.2f;
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, height / 9f);
|
||||
g.fillRect(0, height * 8 / 9f, width, height / 9f);
|
||||
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
|
||||
Colors.BLACK_ALPHA.a = oldAlpha;
|
||||
|
||||
// draw downloads button
|
||||
downloadsButton.draw();
|
||||
|
||||
// draw buttons
|
||||
if (logoTimer > 0) {
|
||||
if (logoState == LogoState.OPEN || logoState == LogoState.CLOSING) {
|
||||
playButton.draw();
|
||||
exitButton.draw();
|
||||
}
|
||||
@@ -237,7 +270,7 @@ public class MainMenu extends BasicGameState {
|
||||
|
||||
// draw music position bar
|
||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
||||
g.setColor((musicPositionBarContains(mouseX, mouseY)) ? BG_HOVER : BG_NORMAL);
|
||||
g.setColor((musicPositionBarContains(mouseX, mouseY)) ? Colors.BLACK_BG_HOVER : Colors.BLACK_BG_NORMAL);
|
||||
g.fillRoundRect(musicBarX, musicBarY, musicBarWidth, musicBarHeight, 4);
|
||||
g.setColor(Color.white);
|
||||
if (!MusicController.isTrackLoading() && beatmap != null) {
|
||||
@@ -251,35 +284,26 @@ public class MainMenu extends BasicGameState {
|
||||
|
||||
// draw update button
|
||||
if (Updater.get().showButton()) {
|
||||
Color updateColor = null;
|
||||
switch (Updater.get().getStatus()) {
|
||||
case UPDATE_AVAILABLE:
|
||||
updateColor = Color.red;
|
||||
break;
|
||||
case UPDATE_DOWNLOADED:
|
||||
updateColor = Color.green;
|
||||
break;
|
||||
case UPDATE_DOWNLOADING:
|
||||
updateColor = Color.yellow;
|
||||
break;
|
||||
default:
|
||||
updateColor = Color.white;
|
||||
break;
|
||||
}
|
||||
updateButton.draw(updateColor);
|
||||
Updater.Status status = Updater.get().getStatus();
|
||||
if (status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING)
|
||||
updateButton.draw();
|
||||
else if (status == Updater.Status.UPDATE_DOWNLOADED)
|
||||
restartButton.draw();
|
||||
}
|
||||
|
||||
// draw text
|
||||
float marginX = width * 0.015f, topMarginY = height * 0.01f, bottomMarginY = height * 0.015f;
|
||||
g.setFont(Utils.FONT_MEDIUM);
|
||||
float lineHeight = Utils.FONT_MEDIUM.getLineHeight() * 0.925f;
|
||||
g.setFont(Fonts.MEDIUM);
|
||||
float lineHeight = Fonts.MEDIUM.getLineHeight() * 0.925f;
|
||||
g.drawString(String.format("Loaded %d songs and %d beatmaps.",
|
||||
BeatmapSetList.get().getMapSetCount(), BeatmapSetList.get().getMapCount()), marginX, topMarginY);
|
||||
if (MusicController.isTrackLoading())
|
||||
g.drawString("Track loading...", marginX, topMarginY + lineHeight);
|
||||
else if (MusicController.trackExists()) {
|
||||
if (Options.useUnicodeMetadata()) // load glyphs
|
||||
Utils.loadGlyphs(Utils.FONT_MEDIUM, beatmap.titleUnicode, beatmap.artistUnicode);
|
||||
if (Options.useUnicodeMetadata()) { // load glyphs
|
||||
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.titleUnicode);
|
||||
Fonts.loadGlyphs(Fonts.MEDIUM, beatmap.artistUnicode);
|
||||
}
|
||||
g.drawString((MusicController.isPlaying()) ? "Now Playing:" : "Paused:", marginX, topMarginY + lineHeight);
|
||||
g.drawString(String.format("%s: %s", beatmap.getArtist(), beatmap.getTitle()), marginX + 25, topMarginY + (lineHeight * 2));
|
||||
}
|
||||
@@ -305,7 +329,10 @@ public class MainMenu extends BasicGameState {
|
||||
exitButton.hoverUpdate(delta, mouseX, mouseY, 0.25f);
|
||||
if (repoButton != null)
|
||||
repoButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
updateButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
if (Updater.get().showButton()) {
|
||||
updateButton.autoHoverUpdate(delta, true);
|
||||
restartButton.autoHoverUpdate(delta, false);
|
||||
}
|
||||
downloadsButton.hoverUpdate(delta, mouseX, mouseY);
|
||||
// ensure only one button is in hover state at once
|
||||
boolean noHoverUpdate = musicPositionBarContains(mouseX, mouseY);
|
||||
@@ -322,46 +349,46 @@ public class MainMenu extends BasicGameState {
|
||||
MusicController.toggleTrackDimmed(0.33f);
|
||||
|
||||
// fade in background
|
||||
if (bgAlpha < BG_MAX_ALPHA) {
|
||||
bgAlpha += delta / 1000f;
|
||||
if (bgAlpha > BG_MAX_ALPHA)
|
||||
bgAlpha = BG_MAX_ALPHA;
|
||||
}
|
||||
Beatmap beatmap = MusicController.getBeatmap();
|
||||
if (!(Options.isDynamicBackgroundEnabled() && beatmap != null && beatmap.isBackgroundLoading()))
|
||||
bgAlpha.update(delta);
|
||||
|
||||
// buttons
|
||||
if (logoClicked) {
|
||||
if (logoTimer == 0) { // shifting to left
|
||||
if (logo.getX() > container.getWidth() / 3.3f)
|
||||
logo.setX(logo.getX() - delta);
|
||||
else
|
||||
logoTimer = 1;
|
||||
} else if (logoTimer >= MOVE_DELAY) // timer over: shift back to center
|
||||
logoClicked = false;
|
||||
else { // increment timer
|
||||
int centerX = container.getWidth() / 2;
|
||||
float currentLogoButtonAlpha;
|
||||
switch (logoState) {
|
||||
case DEFAULT:
|
||||
break;
|
||||
case OPENING:
|
||||
if (logoOpen.update(delta)) // shifting to left
|
||||
logo.setX(centerX - logoOpen.getValue());
|
||||
else {
|
||||
logoState = LogoState.OPEN;
|
||||
logoTimer = 0;
|
||||
logoButtonAlpha.setTime(0);
|
||||
}
|
||||
break;
|
||||
case OPEN:
|
||||
if (logoButtonAlpha.update(delta)) { // fade in buttons
|
||||
currentLogoButtonAlpha = logoButtonAlpha.getValue();
|
||||
playButton.getImage().setAlpha(currentLogoButtonAlpha);
|
||||
exitButton.getImage().setAlpha(currentLogoButtonAlpha);
|
||||
} else if (logoTimer >= LOGO_IDLE_DELAY) { // timer over: shift back to center
|
||||
logoState = LogoState.CLOSING;
|
||||
logoClose.setTime(0);
|
||||
logoTimer = 0;
|
||||
} else // increment timer
|
||||
logoTimer += delta;
|
||||
if (logoTimer <= 500) {
|
||||
// fade in buttons
|
||||
playButton.getImage().setAlpha(logoTimer / 400f);
|
||||
exitButton.getImage().setAlpha(logoTimer / 400f);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// fade out buttons
|
||||
if (logoTimer > 0) {
|
||||
float alpha = playButton.getImage().getAlpha();
|
||||
if (alpha > 0f) {
|
||||
playButton.getImage().setAlpha(alpha - (delta / 200f));
|
||||
exitButton.getImage().setAlpha(alpha - (delta / 200f));
|
||||
} else
|
||||
logoTimer = 0;
|
||||
}
|
||||
|
||||
// move back to original location
|
||||
if (logo.getX() < container.getWidth() / 2) {
|
||||
logo.setX(logo.getX() + (delta / 3f));
|
||||
if (logo.getX() > container.getWidth() / 2)
|
||||
logo.setX(container.getWidth() / 2);
|
||||
break;
|
||||
case CLOSING:
|
||||
if (logoButtonAlpha.update(-delta)) { // fade out buttons
|
||||
currentLogoButtonAlpha = logoButtonAlpha.getValue();
|
||||
playButton.getImage().setAlpha(currentLogoButtonAlpha);
|
||||
exitButton.getImage().setAlpha(currentLogoButtonAlpha);
|
||||
}
|
||||
if (logoClose.update(delta)) // shifting to right
|
||||
logo.setX(centerX - logoClose.getValue());
|
||||
break;
|
||||
}
|
||||
|
||||
// tooltips
|
||||
@@ -373,8 +400,12 @@ public class MainMenu extends BasicGameState {
|
||||
UI.updateTooltip(delta, "Next track", false);
|
||||
else if (musicPrevious.contains(mouseX, mouseY))
|
||||
UI.updateTooltip(delta, "Previous track", false);
|
||||
else if (Updater.get().showButton() && updateButton.contains(mouseX, mouseY))
|
||||
UI.updateTooltip(delta, Updater.get().getStatus().getDescription(), true);
|
||||
else if (Updater.get().showButton()) {
|
||||
Updater.Status status = Updater.get().getStatus();
|
||||
if (((status == Updater.Status.UPDATE_AVAILABLE || status == Updater.Status.UPDATE_DOWNLOADING) && updateButton.contains(mouseX, mouseY)) ||
|
||||
(status == Updater.Status.UPDATE_DOWNLOADED && restartButton.contains(mouseX, mouseY)))
|
||||
UI.updateTooltip(delta, status.getDescription(), true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -412,8 +443,8 @@ public class MainMenu extends BasicGameState {
|
||||
musicPrevious.resetHover();
|
||||
if (repoButton != null && !repoButton.contains(mouseX, mouseY))
|
||||
repoButton.resetHover();
|
||||
if (!updateButton.contains(mouseX, mouseY))
|
||||
updateButton.resetHover();
|
||||
updateButton.resetHover();
|
||||
restartButton.resetHover();
|
||||
if (!downloadsButton.contains(mouseX, mouseY))
|
||||
downloadsButton.resetHover();
|
||||
}
|
||||
@@ -449,71 +480,85 @@ public class MainMenu extends BasicGameState {
|
||||
MusicController.resume();
|
||||
UI.sendBarNotification("Play");
|
||||
}
|
||||
return;
|
||||
} else if (musicNext.contains(x, y)) {
|
||||
nextTrack();
|
||||
UI.sendBarNotification(">> Next");
|
||||
return;
|
||||
} else if (musicPrevious.contains(x, y)) {
|
||||
if (!previous.isEmpty()) {
|
||||
SongMenu menu = (SongMenu) game.getState(Opsu.STATE_SONGMENU);
|
||||
menu.setFocus(BeatmapSetList.get().getBaseNode(previous.pop()), -1, true, false);
|
||||
if (Options.isDynamicBackgroundEnabled())
|
||||
bgAlpha = 0f;
|
||||
bgAlpha.setTime(0);
|
||||
} else
|
||||
MusicController.setPosition(0);
|
||||
UI.sendBarNotification("<< Previous");
|
||||
return;
|
||||
}
|
||||
|
||||
// downloads button actions
|
||||
else if (downloadsButton.contains(x, y)) {
|
||||
if (downloadsButton.contains(x, y)) {
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
game.enterState(Opsu.STATE_DOWNLOADSMENU, new FadeOutTransition(Color.black), new FadeInTransition(Color.black));
|
||||
return;
|
||||
}
|
||||
|
||||
// repository button actions
|
||||
else if (repoButton != null && repoButton.contains(x, y)) {
|
||||
if (repoButton != null && repoButton.contains(x, y)) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(Options.REPOSITORY_URI);
|
||||
} catch (UnsupportedOperationException e) {
|
||||
UI.sendBarNotification("The repository web page could not be opened.");
|
||||
} catch (IOException e) {
|
||||
ErrorHandler.error("Could not browse to repository URI.", e, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// update button actions
|
||||
else if (Updater.get().showButton() && updateButton.contains(x, y)) {
|
||||
switch (Updater.get().getStatus()) {
|
||||
case UPDATE_AVAILABLE:
|
||||
if (Updater.get().showButton()) {
|
||||
Updater.Status status = Updater.get().getStatus();
|
||||
if (updateButton.contains(x, y) && status == Updater.Status.UPDATE_AVAILABLE) {
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
Updater.get().startDownload();
|
||||
break;
|
||||
case UPDATE_DOWNLOADED:
|
||||
updateButton.removeHoverEffects();
|
||||
updateButton.setHoverAnimationDuration(800);
|
||||
updateButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_QUAD);
|
||||
updateButton.setHoverFade(0.6f);
|
||||
return;
|
||||
} else if (restartButton.contains(x, y) && status == Updater.Status.UPDATE_DOWNLOADED) {
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
Updater.get().prepareUpdate();
|
||||
container.setForceExit(false);
|
||||
container.exit();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// start moving logo (if clicked)
|
||||
else if (!logoClicked) {
|
||||
if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) {
|
||||
if (logo.contains(x, y, 0.25f)) {
|
||||
logoClicked = true;
|
||||
logoState = LogoState.OPENING;
|
||||
logoOpen.setTime(0);
|
||||
logoTimer = 0;
|
||||
playButton.getImage().setAlpha(0f);
|
||||
exitButton.getImage().setAlpha(0f);
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// other button actions (if visible)
|
||||
else if (logoClicked) {
|
||||
else if (logoState == LogoState.OPEN || logoState == LogoState.OPENING) {
|
||||
if (logo.contains(x, y, 0.25f) || playButton.contains(x, y, 0.25f)) {
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
enterSongMenu();
|
||||
} else if (exitButton.contains(x, y, 0.25f))
|
||||
return;
|
||||
} else if (exitButton.contains(x, y, 0.25f)) {
|
||||
container.exit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -532,8 +577,9 @@ public class MainMenu extends BasicGameState {
|
||||
break;
|
||||
case Input.KEY_P:
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
if (!logoClicked) {
|
||||
logoClicked = true;
|
||||
if (logoState == LogoState.DEFAULT || logoState == LogoState.CLOSING) {
|
||||
logoState = LogoState.OPENING;
|
||||
logoOpen.setTime(0);
|
||||
logoTimer = 0;
|
||||
playButton.getImage().setAlpha(0f);
|
||||
exitButton.getImage().setAlpha(0f);
|
||||
@@ -581,8 +627,11 @@ public class MainMenu extends BasicGameState {
|
||||
public void reset() {
|
||||
// reset logo
|
||||
logo.setX(container.getWidth() / 2);
|
||||
logoClicked = false;
|
||||
logoOpen.setTime(0);
|
||||
logoClose.setTime(0);
|
||||
logoButtonAlpha.setTime(0);
|
||||
logoTimer = 0;
|
||||
logoState = LogoState.DEFAULT;
|
||||
|
||||
logo.resetHover();
|
||||
playButton.resetHover();
|
||||
@@ -594,6 +643,7 @@ public class MainMenu extends BasicGameState {
|
||||
if (repoButton != null)
|
||||
repoButton.resetHover();
|
||||
updateButton.resetHover();
|
||||
restartButton.resetHover();
|
||||
downloadsButton.resetHover();
|
||||
}
|
||||
|
||||
@@ -611,7 +661,7 @@ public class MainMenu extends BasicGameState {
|
||||
previous.add(node.index);
|
||||
}
|
||||
if (Options.isDynamicBackgroundEnabled() && !sameAudio && !MusicController.isThemePlaying())
|
||||
bgAlpha = 0f;
|
||||
bgAlpha.setTime(0);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,8 @@ import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.audio.SoundEffect;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.MenuButton;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
|
||||
@@ -86,14 +88,18 @@ public class OptionsMenu extends BasicGameState {
|
||||
GameOption.DISABLE_MOUSE_WHEEL,
|
||||
GameOption.DISABLE_MOUSE_BUTTONS,
|
||||
GameOption.CURSOR_SIZE,
|
||||
GameOption.NEW_CURSOR
|
||||
GameOption.NEW_CURSOR,
|
||||
GameOption.DISABLE_CURSOR
|
||||
}),
|
||||
CUSTOM ("Custom", new GameOption[] {
|
||||
GameOption.FIXED_CS,
|
||||
GameOption.FIXED_HP,
|
||||
GameOption.FIXED_AR,
|
||||
GameOption.FIXED_OD,
|
||||
GameOption.CHECKPOINT
|
||||
GameOption.CHECKPOINT,
|
||||
GameOption.REPLAY_SEEKING,
|
||||
GameOption.DISABLE_UPDATER,
|
||||
GameOption.ENABLE_WATCH_SERVICE
|
||||
});
|
||||
|
||||
/** Total number of tabs. */
|
||||
@@ -110,10 +116,10 @@ public class OptionsMenu extends BasicGameState {
|
||||
private static OptionTab[] values = values();
|
||||
|
||||
/** Tab name. */
|
||||
private String name;
|
||||
private final String name;
|
||||
|
||||
/** Options array. */
|
||||
public GameOption[] options;
|
||||
public final GameOption[] options;
|
||||
|
||||
/** Associated tab button. */
|
||||
public MenuButton button;
|
||||
@@ -163,7 +169,7 @@ public class OptionsMenu extends BasicGameState {
|
||||
private StateBasedGame game;
|
||||
private Input input;
|
||||
private Graphics g;
|
||||
private int state;
|
||||
private final int state;
|
||||
|
||||
public OptionsMenu(int state) {
|
||||
this.state = state;
|
||||
@@ -182,8 +188,8 @@ public class OptionsMenu extends BasicGameState {
|
||||
|
||||
// option tabs
|
||||
Image tabImage = GameImage.MENU_TAB.getImage();
|
||||
float tabX = width * 0.032f + Utils.FONT_DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
|
||||
float tabY = Utils.FONT_XLARGE.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
|
||||
float tabX = width * 0.032f + Fonts.DEFAULT.getWidth("Change the way opsu! behaves") + (tabImage.getWidth() / 2);
|
||||
float tabY = Fonts.XLARGE.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
|
||||
height * 0.015f - (tabImage.getHeight() / 2f);
|
||||
int tabOffset = Math.min(tabImage.getWidth(), width / OptionTab.SIZE);
|
||||
for (OptionTab tab : OptionTab.values())
|
||||
@@ -198,22 +204,19 @@ public class OptionsMenu extends BasicGameState {
|
||||
@Override
|
||||
public void render(GameContainer container, StateBasedGame game, Graphics g)
|
||||
throws SlickException {
|
||||
g.setBackground(Utils.COLOR_BLACK_ALPHA);
|
||||
|
||||
int width = container.getWidth();
|
||||
int height = container.getHeight();
|
||||
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
|
||||
float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
|
||||
|
||||
// background
|
||||
GameImage.OPTIONS_BG.getImage().draw();
|
||||
|
||||
// title
|
||||
float marginX = width * 0.015f, marginY = height * 0.01f;
|
||||
Utils.FONT_XLARGE.drawString(marginX, marginY, "Options", Color.white);
|
||||
Utils.FONT_DEFAULT.drawString(marginX, marginY + Utils.FONT_XLARGE.getLineHeight() * 0.92f,
|
||||
Fonts.XLARGE.drawString(marginX, marginY, "Options", Color.white);
|
||||
Fonts.DEFAULT.drawString(marginX, marginY + Fonts.XLARGE.getLineHeight() * 0.92f,
|
||||
"Change the way opsu! behaves", Color.white);
|
||||
|
||||
// background
|
||||
GameImage.OPTIONS_BG.getImage().draw(0, lineY);
|
||||
|
||||
// game options
|
||||
g.setLineWidth(1f);
|
||||
GameOption hoverOption = (keyEntryLeft) ? GameOption.KEY_LEFT :
|
||||
@@ -241,6 +244,7 @@ public class OptionsMenu extends BasicGameState {
|
||||
currentTab.getName(), true, false);
|
||||
g.setColor(Color.white);
|
||||
g.setLineWidth(2f);
|
||||
float lineY = OptionTab.DISPLAY.button.getY() + (GameImage.MENU_TAB.getImage().getHeight() / 2f);
|
||||
g.drawLine(0, lineY, width, lineY);
|
||||
g.resetLineWidth();
|
||||
|
||||
@@ -248,15 +252,15 @@ public class OptionsMenu extends BasicGameState {
|
||||
|
||||
// key entry state
|
||||
if (keyEntryLeft || keyEntryRight) {
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, height);
|
||||
g.setColor(Color.white);
|
||||
String prompt = (keyEntryLeft) ?
|
||||
"Please press the new left-click key." :
|
||||
"Please press the new right-click key.";
|
||||
Utils.FONT_LARGE.drawString(
|
||||
(width / 2) - (Utils.FONT_LARGE.getWidth(prompt) / 2),
|
||||
(height / 2) - Utils.FONT_LARGE.getLineHeight(), prompt
|
||||
Fonts.LARGE.drawString(
|
||||
(width / 2) - (Fonts.LARGE.getWidth(prompt) / 2),
|
||||
(height / 2) - Fonts.LARGE.getLineHeight(), prompt
|
||||
);
|
||||
}
|
||||
|
||||
@@ -413,14 +417,14 @@ public class OptionsMenu extends BasicGameState {
|
||||
*/
|
||||
private void drawOption(GameOption option, int pos, boolean focus) {
|
||||
int width = container.getWidth();
|
||||
int textHeight = Utils.FONT_LARGE.getLineHeight();
|
||||
int textHeight = Fonts.LARGE.getLineHeight();
|
||||
float y = textY + (pos * offsetY);
|
||||
Color color = (focus) ? Color.cyan : Color.white;
|
||||
|
||||
Utils.FONT_LARGE.drawString(width / 30, y, option.getName(), color);
|
||||
Utils.FONT_LARGE.drawString(width / 2, y, option.getValueString(), color);
|
||||
Utils.FONT_SMALL.drawString(width / 30, y + textHeight, option.getDescription(), color);
|
||||
g.setColor(Utils.COLOR_WHITE_ALPHA);
|
||||
Fonts.LARGE.drawString(width / 30, y, option.getName(), color);
|
||||
Fonts.LARGE.drawString(width / 2, y, option.getValueString(), color);
|
||||
Fonts.SMALL.drawString(width / 30, y + textHeight, option.getDescription(), color);
|
||||
g.setColor(Colors.WHITE_ALPHA);
|
||||
g.drawLine(0, y + textHeight, width, y + textHeight);
|
||||
}
|
||||
|
||||
@@ -433,7 +437,7 @@ public class OptionsMenu extends BasicGameState {
|
||||
if (y < textY || y > textY + (offsetY * maxOptionsScreen))
|
||||
return null;
|
||||
|
||||
int index = (y - textY + Utils.FONT_LARGE.getLineHeight()) / offsetY;
|
||||
int index = (y - textY + Fonts.LARGE.getLineHeight()) / offsetY;
|
||||
if (index >= currentTab.options.length)
|
||||
return null;
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import itdelatrisu.opsu.GameImage;
|
||||
import itdelatrisu.opsu.GameMod;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.OszUnpacker;
|
||||
import itdelatrisu.opsu.ScoreData;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.audio.MultiClip;
|
||||
@@ -32,18 +31,32 @@ import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.audio.SoundEffect;
|
||||
import itdelatrisu.opsu.beatmap.Beatmap;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapDifficultyCalculator;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSet;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSetNode;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSortOrder;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapWatchService.BeatmapWatchServiceListener;
|
||||
import itdelatrisu.opsu.beatmap.LRUCache;
|
||||
import itdelatrisu.opsu.beatmap.OszUnpacker;
|
||||
import itdelatrisu.opsu.db.BeatmapDB;
|
||||
import itdelatrisu.opsu.db.ScoreDB;
|
||||
import itdelatrisu.opsu.states.ButtonMenu.MenuState;
|
||||
import itdelatrisu.opsu.ui.KinecticScrolling;
|
||||
import itdelatrisu.opsu.ui.Colors;
|
||||
import itdelatrisu.opsu.ui.Fonts;
|
||||
import itdelatrisu.opsu.ui.MenuButton;
|
||||
import itdelatrisu.opsu.ui.StarStream;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardWatchEventKinds;
|
||||
import java.nio.file.WatchEvent.Kind;
|
||||
import java.util.Map;
|
||||
import java.util.Stack;
|
||||
|
||||
@@ -145,8 +158,8 @@ public class SongMenu extends BasicGameState {
|
||||
/** Button coordinate values. */
|
||||
private float buttonX, buttonY, buttonOffset, buttonWidth, buttonHeight;
|
||||
|
||||
/** Current x offset of song buttons for mouse hover, in pixels. */
|
||||
private float hoverOffset = 0f;
|
||||
/** Horizontal offset of song buttons for mouse hover, in pixels. */
|
||||
private AnimatedValue hoverOffset = new AnimatedValue(250, 0, MAX_HOVER_OFFSET, AnimationEquation.OUT_QUART);
|
||||
|
||||
/** Current index of hovered song button. */
|
||||
private BeatmapSetNode hoverIndex = null;
|
||||
@@ -209,11 +222,52 @@ public class SongMenu extends BasicGameState {
|
||||
/** The text length of the last string in the search TextField. */
|
||||
private int lastSearchTextLength = -1;
|
||||
|
||||
/** Whether the song folder changed (notified via the watch service). */
|
||||
private boolean songFolderChanged = false;
|
||||
|
||||
/** The last background image. */
|
||||
private File lastBackgroundImage;
|
||||
|
||||
/** Background alpha level (for fade-in effect). */
|
||||
private AnimatedValue bgAlpha = new AnimatedValue(800, 0f, 1f, AnimationEquation.OUT_QUAD);
|
||||
|
||||
/** Timer for animations when a new song node is selected. */
|
||||
private AnimatedValue songChangeTimer = new AnimatedValue(900, 0f, 1f, AnimationEquation.LINEAR);
|
||||
|
||||
/** Timer for the music icon animation when a new song node is selected. */
|
||||
private AnimatedValue musicIconBounceTimer = new AnimatedValue(350, 0f, 1f, AnimationEquation.LINEAR);
|
||||
|
||||
/**
|
||||
* Beatmaps whose difficulties were recently computed (if flag is non-null).
|
||||
* Unless the Boolean flag is null, then upon removal, the beatmap's objects will
|
||||
* be cleared (to be garbage collected). If the flag is true, also clear the
|
||||
* beatmap's array fields (timing points, etc.).
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
private LRUCache<Beatmap, Boolean> beatmapsCalculated = new LRUCache<Beatmap, Boolean>(12) {
|
||||
@Override
|
||||
public void eldestRemoved(Map.Entry<Beatmap, Boolean> eldest) {
|
||||
Boolean b = eldest.getValue();
|
||||
if (b != null) {
|
||||
Beatmap beatmap = eldest.getKey();
|
||||
beatmap.objects = null;
|
||||
if (b) {
|
||||
beatmap.timingPoints = null;
|
||||
beatmap.breaks = null;
|
||||
beatmap.combo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** The star stream. */
|
||||
private StarStream starStream;
|
||||
|
||||
// game-related variables
|
||||
private GameContainer container;
|
||||
private StateBasedGame game;
|
||||
private Input input;
|
||||
private int state;
|
||||
private final int state;
|
||||
|
||||
public SongMenu(int state) {
|
||||
this.state = state;
|
||||
@@ -231,8 +285,8 @@ public class SongMenu extends BasicGameState {
|
||||
|
||||
// header/footer coordinates
|
||||
headerY = height * 0.0075f + GameImage.MENU_MUSICNOTE.getImage().getHeight() +
|
||||
Utils.FONT_BOLD.getLineHeight() + Utils.FONT_DEFAULT.getLineHeight() +
|
||||
Utils.FONT_SMALL.getLineHeight();
|
||||
Fonts.BOLD.getLineHeight() + Fonts.DEFAULT.getLineHeight() +
|
||||
Fonts.SMALL.getLineHeight();
|
||||
footerY = height - GameImage.SELECTION_MODS.getImage().getHeight();
|
||||
|
||||
// initialize sorts
|
||||
@@ -253,11 +307,11 @@ public class SongMenu extends BasicGameState {
|
||||
buttonOffset = (footerY - headerY - DIVIDER_LINE_WIDTH) / MAX_SONG_BUTTONS;
|
||||
|
||||
// search
|
||||
int textFieldX = (int) (width * 0.7125f + Utils.FONT_BOLD.getWidth("Search: "));
|
||||
int textFieldY = (int) (headerY + Utils.FONT_BOLD.getLineHeight() / 2);
|
||||
int textFieldX = (int) (width * 0.7125f + Fonts.BOLD.getWidth("Search: "));
|
||||
int textFieldY = (int) (headerY + Fonts.BOLD.getLineHeight() / 2);
|
||||
search = new TextField(
|
||||
container, Utils.FONT_BOLD, textFieldX, textFieldY,
|
||||
(int) (width * 0.99f) - textFieldX, Utils.FONT_BOLD.getLineHeight()
|
||||
container, Fonts.BOLD, textFieldX, textFieldY,
|
||||
(int) (width * 0.99f) - textFieldX, Fonts.BOLD.getLineHeight()
|
||||
);
|
||||
search.setBackgroundColor(Color.transparent);
|
||||
search.setBorderColor(Color.transparent);
|
||||
@@ -287,6 +341,22 @@ public class SongMenu extends BasicGameState {
|
||||
int loaderDim = GameImage.MENU_MUSICNOTE.getImage().getWidth();
|
||||
SpriteSheet spr = new SpriteSheet(GameImage.MENU_LOADER.getImage(), loaderDim, loaderDim);
|
||||
loader = new Animation(spr, 50);
|
||||
|
||||
// beatmap watch service listener
|
||||
final StateBasedGame game_ = game;
|
||||
BeatmapWatchService.addListener(new BeatmapWatchServiceListener() {
|
||||
@Override
|
||||
public void eventReceived(Kind<?> kind, Path child) {
|
||||
if (!songFolderChanged && kind != StandardWatchEventKinds.ENTRY_MODIFY) {
|
||||
songFolderChanged = true;
|
||||
if (game_.getCurrentStateID() == Opsu.STATE_SONGMENU)
|
||||
UI.sendBarNotification("Changes in Songs folder detected. Hit F5 to refresh.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// star stream
|
||||
starStream = new StarStream(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -300,23 +370,30 @@ public class SongMenu extends BasicGameState {
|
||||
|
||||
// background
|
||||
if (focusNode != null) {
|
||||
Beatmap focusNodeBeatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
|
||||
if (!focusNodeBeatmap.drawBG(width, height, 1.0f, true))
|
||||
Beatmap focusNodeBeatmap = focusNode.getSelectedBeatmap();
|
||||
if (!focusNodeBeatmap.drawBackground(width, height, bgAlpha.getValue(), true))
|
||||
GameImage.PLAYFIELD.getImage().draw();
|
||||
}
|
||||
|
||||
// star stream
|
||||
starStream.draw();
|
||||
|
||||
// song buttons
|
||||
BeatmapSetNode node = startNode;
|
||||
int startNodeOffsetoffset = 0;
|
||||
if (node.prev != null) {
|
||||
startNodeOffsetoffset = -1;
|
||||
node = node.prev;
|
||||
}
|
||||
g.setClip(0, (int) (headerY + DIVIDER_LINE_WIDTH / 2), width, (int) (footerY - headerY));
|
||||
for (int i = startNodeOffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) {
|
||||
for (int i = startNodeOffset + startNodeOffsetoffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) {
|
||||
// draw the node
|
||||
float offset = (node == hoverIndex) ? hoverOffset : 0f;
|
||||
float offset = (node == hoverIndex) ? hoverOffset.getValue() : 0f;
|
||||
float ypos = buttonY + (i*buttonOffset) ;
|
||||
float mid = height/2 - ypos - buttonOffset/2;
|
||||
final float circleRadi = 1000 * GameImage.getUIscale();
|
||||
//finds points along a very large circle
|
||||
// x^2 = h^2 - y^2
|
||||
float t = circleRadi*circleRadi - (mid*mid);
|
||||
final float circleRadi = 700 * GameImage.getUIscale();
|
||||
//finds points along a very large circle (x^2 = h^2 - y^2)
|
||||
float t = circleRadi * circleRadi - (mid * mid);
|
||||
float xpos = (float)(t>0?Math.sqrt(t):0) - circleRadi + 50 * GameImage.getUIscale();
|
||||
ScoreData[] scores = getScoreDataForNode(node, false);
|
||||
node.draw(buttonX - offset - xpos, ypos,
|
||||
@@ -336,7 +413,7 @@ public class SongMenu extends BasicGameState {
|
||||
MAX_SONG_BUTTONS * buttonOffset,
|
||||
width, headerY + DIVIDER_LINE_WIDTH / 2,
|
||||
0, MAX_SONG_BUTTONS * buttonOffset,
|
||||
Utils.COLOR_BLACK_ALPHA, Color.white, true);
|
||||
Colors.BLACK_ALPHA, Color.white, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,14 +422,19 @@ public class SongMenu extends BasicGameState {
|
||||
ScoreData.clipToDownloadArea(g);
|
||||
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
|
||||
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
|
||||
for (int i = 0; i < MAX_SCORE_BUTTONS + 1; i++) {
|
||||
int rank = startScore + i;
|
||||
|
||||
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS + 1);
|
||||
float timerScale = 1f - (1 / 3f) * ((MAX_SCORE_BUTTONS - scoreButtons) / (float) (MAX_SCORE_BUTTONS - 1));
|
||||
int duration = (int) (songChangeTimer.getDuration() * timerScale);
|
||||
int segmentDuration = (int) ((2 / 3f) * songChangeTimer.getDuration());
|
||||
int time = songChangeTimer.getTime();
|
||||
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
|
||||
if (rank < 0)
|
||||
continue;
|
||||
if (rank >= focusScores.length)
|
||||
break;
|
||||
long prevScore = (rank + 1 < focusScores.length) ? focusScores[rank + 1].score : -1;
|
||||
focusScores[rank].draw(g, offset + i*ScoreData.getButtonOffset(), rank, prevScore, ScoreData.buttonContains(mouseX, mouseY-offset, i));
|
||||
float t = Utils.clamp((time - (i * (duration - segmentDuration) / scoreButtons)) / (float) segmentDuration, 0f, 1f);
|
||||
boolean focus = (t >= 0.9999f && ScoreData.buttonContains(mouseX, mouseY - offset, i));
|
||||
focusScores[rank].draw(g, offset + i*ScoreData.getButtonOffset(), rank, prevScore, focus, t);
|
||||
}
|
||||
g.clearClip();
|
||||
|
||||
@@ -360,13 +442,12 @@ public class SongMenu extends BasicGameState {
|
||||
if (focusScores.length > MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY))
|
||||
ScoreData.drawScrollbar(g, startScorePos.getPosition() , focusScores.length * ScoreData.getButtonOffset());
|
||||
}
|
||||
|
||||
|
||||
|
||||
// top/bottom bars
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, headerY);
|
||||
g.fillRect(0, footerY, width, height - footerY);
|
||||
g.setColor(Utils.COLOR_BLUE_DIVIDER);
|
||||
g.setColor(Colors.BLUE_DIVIDER);
|
||||
g.setLineWidth(DIVIDER_LINE_WIDTH);
|
||||
g.drawLine(0, headerY, width, headerY);
|
||||
g.drawLine(0, footerY, width, footerY);
|
||||
@@ -379,8 +460,13 @@ public class SongMenu extends BasicGameState {
|
||||
Image musicNote = GameImage.MENU_MUSICNOTE.getImage();
|
||||
if (MusicController.isTrackLoading())
|
||||
loader.draw(marginX, marginY);
|
||||
else
|
||||
musicNote.draw(marginX, marginY);
|
||||
else {
|
||||
float t = musicIconBounceTimer.getValue() * 2f;
|
||||
if (t > 1)
|
||||
t = 2f - t;
|
||||
float musicNoteScale = 1f + 0.3f * t;
|
||||
musicNote.getScaledCopy(musicNoteScale).drawCentered(marginX + musicNote.getWidth() / 2f, marginY + musicNote.getHeight() / 2f);
|
||||
}
|
||||
int iconWidth = musicNote.getWidth();
|
||||
|
||||
// song info text
|
||||
@@ -388,26 +474,49 @@ public class SongMenu extends BasicGameState {
|
||||
songInfo = focusNode.getInfo();
|
||||
if (Options.useUnicodeMetadata()) { // load glyphs
|
||||
Beatmap beatmap = focusNode.getBeatmapSet().get(0);
|
||||
Utils.loadGlyphs(Utils.FONT_LARGE, beatmap.titleUnicode, beatmap.artistUnicode);
|
||||
Fonts.loadGlyphs(Fonts.LARGE, beatmap.titleUnicode);
|
||||
Fonts.loadGlyphs(Fonts.LARGE, beatmap.artistUnicode);
|
||||
}
|
||||
}
|
||||
marginX += 5;
|
||||
Color c = Colors.WHITE_FADE;
|
||||
float oldAlpha = c.a;
|
||||
float t = AnimationEquation.OUT_QUAD.calc(songChangeTimer.getValue());
|
||||
float headerTextY = marginY * 0.2f;
|
||||
Utils.FONT_LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], Color.white);
|
||||
headerTextY += Utils.FONT_LARGE.getLineHeight() - 6;
|
||||
Utils.FONT_DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], Color.white);
|
||||
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 2;
|
||||
float speedModifier = GameMod.getSpeedMultiplier();
|
||||
Color color2 = (speedModifier == 1f) ? Color.white :
|
||||
(speedModifier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
|
||||
Utils.FONT_BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
|
||||
headerTextY += Utils.FONT_BOLD.getLineHeight() - 4;
|
||||
Utils.FONT_DEFAULT.drawString(marginX, headerTextY, songInfo[3], Color.white);
|
||||
headerTextY += Utils.FONT_DEFAULT.getLineHeight() - 4;
|
||||
float multiplier = GameMod.getDifficultyMultiplier();
|
||||
Color color4 = (multiplier == 1f) ? Color.white :
|
||||
(multiplier > 1f) ? Utils.COLOR_RED_HIGHLIGHT : Utils.COLOR_BLUE_HIGHLIGHT;
|
||||
Utils.FONT_SMALL.drawString(marginX, headerTextY, songInfo[4], color4);
|
||||
c.a = Math.min(t * songInfo.length / 1.5f, 1f);
|
||||
if (c.a > 0)
|
||||
Fonts.LARGE.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[0], c);
|
||||
headerTextY += Fonts.LARGE.getLineHeight() - 6;
|
||||
c.a = Math.min((t - 1f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
|
||||
if (c.a > 0)
|
||||
Fonts.DEFAULT.drawString(marginX + iconWidth * 1.05f, headerTextY, songInfo[1], c);
|
||||
headerTextY += Fonts.DEFAULT.getLineHeight() - 2;
|
||||
c.a = Math.min((t - 2f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
|
||||
if (c.a > 0) {
|
||||
float speedModifier = GameMod.getSpeedMultiplier();
|
||||
Color color2 = (speedModifier == 1f) ? c :
|
||||
(speedModifier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT;
|
||||
float oldAlpha2 = color2.a;
|
||||
color2.a = c.a;
|
||||
Fonts.BOLD.drawString(marginX, headerTextY, songInfo[2], color2);
|
||||
color2.a = oldAlpha2;
|
||||
}
|
||||
headerTextY += Fonts.BOLD.getLineHeight() - 4;
|
||||
c.a = Math.min((t - 3f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
|
||||
if (c.a > 0)
|
||||
Fonts.DEFAULT.drawString(marginX, headerTextY, songInfo[3], c);
|
||||
headerTextY += Fonts.DEFAULT.getLineHeight() - 4;
|
||||
c.a = Math.min((t - 4f / (songInfo.length * 1.5f)) * songInfo.length / 1.5f, 1f);
|
||||
if (c.a > 0) {
|
||||
float multiplier = GameMod.getDifficultyMultiplier();
|
||||
Color color4 = (multiplier == 1f) ? c :
|
||||
(multiplier > 1f) ? Colors.RED_HIGHLIGHT : Colors.BLUE_HIGHLIGHT;
|
||||
float oldAlpha4 = color4.a;
|
||||
color4.a = c.a;
|
||||
Fonts.SMALL.drawString(marginX, headerTextY, songInfo[4], color4);
|
||||
color4.a = oldAlpha4;
|
||||
}
|
||||
c.a = oldAlpha;
|
||||
}
|
||||
|
||||
// selection buttons
|
||||
@@ -440,38 +549,41 @@ public class SongMenu extends BasicGameState {
|
||||
int searchX = search.getX(), searchY = search.getY();
|
||||
float searchBaseX = width * 0.7f;
|
||||
float searchTextX = width * 0.7125f;
|
||||
float searchRectHeight = Utils.FONT_BOLD.getLineHeight() * 2;
|
||||
float searchExtraHeight = Utils.FONT_DEFAULT.getLineHeight() * 0.7f;
|
||||
float searchRectHeight = Fonts.BOLD.getLineHeight() * 2;
|
||||
float searchExtraHeight = Fonts.DEFAULT.getLineHeight() * 0.7f;
|
||||
float searchProgress = (searchTransitionTimer < SEARCH_TRANSITION_TIME) ?
|
||||
((float) searchTransitionTimer / SEARCH_TRANSITION_TIME) : 1f;
|
||||
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
|
||||
float oldAlpha = Colors.BLACK_ALPHA.a;
|
||||
if (searchEmpty) {
|
||||
searchRectHeight += (1f - searchProgress) * searchExtraHeight;
|
||||
Utils.COLOR_BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f;
|
||||
Colors.BLACK_ALPHA.a = 0.5f - searchProgress * 0.3f;
|
||||
} else {
|
||||
searchRectHeight += searchProgress * searchExtraHeight;
|
||||
Utils.COLOR_BLACK_ALPHA.a = 0.2f + searchProgress * 0.3f;
|
||||
Colors.BLACK_ALPHA.a = 0.2f + searchProgress * 0.3f;
|
||||
}
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(searchBaseX, headerY + DIVIDER_LINE_WIDTH / 2, width - searchBaseX, searchRectHeight);
|
||||
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
|
||||
Utils.FONT_BOLD.drawString(searchTextX, searchY, "Search:", Utils.COLOR_GREEN_SEARCH);
|
||||
Colors.BLACK_ALPHA.a = oldAlpha;
|
||||
Fonts.BOLD.drawString(searchTextX, searchY, "Search:", Colors.GREEN_SEARCH);
|
||||
if (searchEmpty)
|
||||
Utils.FONT_BOLD.drawString(searchX, searchY, "Type to search!", Color.white);
|
||||
Fonts.BOLD.drawString(searchX, searchY, "Type to search!", Color.white);
|
||||
else {
|
||||
g.setColor(Color.white);
|
||||
// TODO: why is this needed to correctly position the TextField?
|
||||
search.setLocation(searchX - 3, searchY - 1);
|
||||
search.render(container, g);
|
||||
search.setLocation(searchX, searchY);
|
||||
Utils.FONT_DEFAULT.drawString(searchTextX, searchY + Utils.FONT_BOLD.getLineHeight(),
|
||||
Fonts.DEFAULT.drawString(searchTextX, searchY + Fonts.BOLD.getLineHeight(),
|
||||
(searchResultString == null) ? "Searching..." : searchResultString, Color.white);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// reloading beatmaps
|
||||
if (reloadThread != null) {
|
||||
// darken the screen
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(0, 0, width, height);
|
||||
|
||||
UI.drawLoadingProgress(g);
|
||||
@@ -510,6 +622,21 @@ public class SongMenu extends BasicGameState {
|
||||
}
|
||||
}
|
||||
|
||||
if (focusNode != null) {
|
||||
// fade in background
|
||||
Beatmap focusNodeBeatmap = focusNode.getSelectedBeatmap();
|
||||
if (!focusNodeBeatmap.isBackgroundLoading())
|
||||
bgAlpha.update(delta);
|
||||
|
||||
// song change timers
|
||||
songChangeTimer.update(delta);
|
||||
if (!MusicController.isTrackLoading())
|
||||
musicIconBounceTimer.update(delta);
|
||||
}
|
||||
|
||||
// star stream
|
||||
starStream.update(delta);
|
||||
|
||||
// search
|
||||
search.setFocus(true);
|
||||
searchTimer += delta;
|
||||
@@ -574,14 +701,10 @@ public class SongMenu extends BasicGameState {
|
||||
if ((mouseX > cx && mouseX < cx + buttonWidth) &&
|
||||
(mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) {
|
||||
if (node == hoverIndex) {
|
||||
if (hoverOffset < MAX_HOVER_OFFSET) {
|
||||
hoverOffset += delta / 3f;
|
||||
if (hoverOffset > MAX_HOVER_OFFSET)
|
||||
hoverOffset = MAX_HOVER_OFFSET;
|
||||
}
|
||||
hoverOffset.update(delta);
|
||||
} else {
|
||||
hoverIndex = node ;
|
||||
hoverOffset = 0f;
|
||||
hoverOffset.setTime(0);
|
||||
}
|
||||
isHover = true;
|
||||
break;
|
||||
@@ -589,21 +712,19 @@ public class SongMenu extends BasicGameState {
|
||||
}
|
||||
}
|
||||
if (!isHover) {
|
||||
hoverOffset = 0f;
|
||||
hoverOffset.setTime(0);
|
||||
hoverIndex = null;
|
||||
} else
|
||||
return;
|
||||
|
||||
// tooltips
|
||||
if (focusScores != null) {
|
||||
if (focusScores != null && ScoreData.areaContains(mouseX, mouseY)) {
|
||||
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
|
||||
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
|
||||
for (int i = 0; i < MAX_SCORE_BUTTONS; i++) {
|
||||
int rank = startScore + i;
|
||||
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
|
||||
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
|
||||
if (rank < 0)
|
||||
continue;
|
||||
if (rank >= focusScores.length)
|
||||
break;
|
||||
if (ScoreData.buttonContains(mouseX, mouseY - offset, i)) {
|
||||
UI.updateTooltip(delta, focusScores[rank].getTooltipString(), true);
|
||||
break;
|
||||
@@ -689,7 +810,7 @@ public class SongMenu extends BasicGameState {
|
||||
float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX;
|
||||
if ((x > cx && x < cx + buttonWidth) &&
|
||||
(y > buttonY + (i * buttonOffset) && y < buttonY + (i * buttonOffset) + buttonHeight)) {
|
||||
float oldHoverOffset = hoverOffset;
|
||||
int oldHoverOffsetTime = hoverOffset.getTime();
|
||||
BeatmapSetNode oldHoverIndex = hoverIndex;
|
||||
|
||||
// clicked node is already expanded
|
||||
@@ -714,7 +835,7 @@ public class SongMenu extends BasicGameState {
|
||||
}
|
||||
|
||||
// restore hover data
|
||||
hoverOffset = oldHoverOffset;
|
||||
hoverOffset.setTime(oldHoverOffsetTime);
|
||||
hoverIndex = oldHoverIndex;
|
||||
|
||||
// open beatmap menu
|
||||
@@ -728,12 +849,10 @@ public class SongMenu extends BasicGameState {
|
||||
|
||||
// score buttons
|
||||
if (focusScores != null && ScoreData.areaContains(x, y)) {
|
||||
for (int i = 0; i < MAX_SCORE_BUTTONS + 1; i++) {
|
||||
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
|
||||
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
|
||||
int rank = startScore + i;
|
||||
if (rank >= focusScores.length)
|
||||
break;
|
||||
int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset());
|
||||
int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset());
|
||||
int scoreButtons = Math.min(focusScores.length - startScore, MAX_SCORE_BUTTONS);
|
||||
for (int i = 0, rank = startScore; i < scoreButtons; i++, rank++) {
|
||||
if (ScoreData.buttonContains(x, y - offset, i)) {
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
if (button != Input.MOUSE_RIGHT_BUTTON) {
|
||||
@@ -805,8 +924,12 @@ public class SongMenu extends BasicGameState {
|
||||
break;
|
||||
case Input.KEY_F5:
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD);
|
||||
game.enterState(Opsu.STATE_BUTTONMENU);
|
||||
if (songFolderChanged)
|
||||
reloadBeatmaps(false);
|
||||
else {
|
||||
((ButtonMenu) game.getState(Opsu.STATE_BUTTONMENU)).setMenuState(MenuState.RELOAD);
|
||||
game.enterState(Opsu.STATE_BUTTONMENU);
|
||||
}
|
||||
break;
|
||||
case Input.KEY_DELETE:
|
||||
if (focusNode == null)
|
||||
@@ -851,11 +974,11 @@ public class SongMenu extends BasicGameState {
|
||||
if (next != null) {
|
||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||
BeatmapSetNode oldStartNode = startNode;
|
||||
float oldHoverOffset = hoverOffset;
|
||||
int oldHoverOffsetTime = hoverOffset.getTime();
|
||||
BeatmapSetNode oldHoverIndex = hoverIndex;
|
||||
setFocus(next, 0, false, true);
|
||||
if (startNode == oldStartNode) {
|
||||
hoverOffset = oldHoverOffset;
|
||||
hoverOffset.setTime(oldHoverOffsetTime);
|
||||
hoverIndex = oldHoverIndex;
|
||||
}
|
||||
}
|
||||
@@ -867,11 +990,11 @@ public class SongMenu extends BasicGameState {
|
||||
if (prev != null) {
|
||||
SoundController.playSound(SoundEffect.MENUCLICK);
|
||||
BeatmapSetNode oldStartNode = startNode;
|
||||
float oldHoverOffset = hoverOffset;
|
||||
int oldHoverOffsetTime = hoverOffset.getTime();
|
||||
BeatmapSetNode oldHoverIndex = hoverIndex;
|
||||
setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.getBeatmapSet().size() - 1, false, true);
|
||||
if (startNode == oldStartNode) {
|
||||
hoverOffset = oldHoverOffset;
|
||||
hoverOffset.setTime(oldHoverOffsetTime);
|
||||
hoverIndex = oldHoverIndex;
|
||||
}
|
||||
}
|
||||
@@ -965,18 +1088,26 @@ public class SongMenu extends BasicGameState {
|
||||
selectRandomButton.resetHover();
|
||||
selectMapOptionsButton.resetHover();
|
||||
selectOptionsButton.resetHover();
|
||||
hoverOffset = 0f;
|
||||
hoverOffset.setTime(0);
|
||||
hoverIndex = null;
|
||||
startScorePos.setPosition(0);
|
||||
beatmapMenuTimer = -1;
|
||||
searchTransitionTimer = SEARCH_TRANSITION_TIME;
|
||||
songInfo = null;
|
||||
bgAlpha.setTime(bgAlpha.getDuration());
|
||||
songChangeTimer.setTime(songChangeTimer.getDuration());
|
||||
musicIconBounceTimer.setTime(musicIconBounceTimer.getDuration());
|
||||
starStream.clear();
|
||||
|
||||
// reset song stack
|
||||
randomStack = new Stack<SongNode>();
|
||||
|
||||
// reload beatmaps if song folder changed
|
||||
if (songFolderChanged && stateAction != MenuState.RELOAD)
|
||||
reloadBeatmaps(false);
|
||||
|
||||
// set focus node if not set (e.g. theme song playing)
|
||||
if (focusNode == null && BeatmapSetList.get().size() > 0)
|
||||
else if (focusNode == null && BeatmapSetList.get().size() > 0)
|
||||
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
|
||||
|
||||
// reset music track
|
||||
@@ -1003,13 +1134,13 @@ public class SongMenu extends BasicGameState {
|
||||
|
||||
// destroy skin images, if any
|
||||
for (GameImage img : GameImage.values()) {
|
||||
if (img.isSkinnable())
|
||||
img.destroySkinImage();
|
||||
if (img.isBeatmapSkinnable())
|
||||
img.destroyBeatmapSkinImage();
|
||||
}
|
||||
|
||||
// reload scores
|
||||
if (focusNode != null) {
|
||||
scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
|
||||
scoreMap = ScoreDB.getMapSetScores(focusNode.getSelectedBeatmap());
|
||||
focusScores = getScoreDataForNode(focusNode, true);
|
||||
}
|
||||
|
||||
@@ -1022,7 +1153,7 @@ public class SongMenu extends BasicGameState {
|
||||
case BEATMAP: // clear all scores
|
||||
if (stateActionNode == null || stateActionNode.beatmapIndex == -1)
|
||||
break;
|
||||
Beatmap beatmap = stateActionNode.getBeatmapSet().get(stateActionNode.beatmapIndex);
|
||||
Beatmap beatmap = stateActionNode.getSelectedBeatmap();
|
||||
ScoreDB.deleteScore(beatmap);
|
||||
if (stateActionNode == focusNode) {
|
||||
focusScores = null;
|
||||
@@ -1033,7 +1164,7 @@ public class SongMenu extends BasicGameState {
|
||||
if (stateActionScore == null)
|
||||
break;
|
||||
ScoreDB.deleteScore(stateActionScore);
|
||||
scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex));
|
||||
scoreMap = ScoreDB.getMapSetScores(focusNode.getSelectedBeatmap());
|
||||
focusScores = getScoreDataForNode(focusNode, true);
|
||||
startScorePos.setPosition(0);
|
||||
break;
|
||||
@@ -1095,44 +1226,7 @@ public class SongMenu extends BasicGameState {
|
||||
}
|
||||
break;
|
||||
case RELOAD: // reload beatmaps
|
||||
// reset state and node references
|
||||
MusicController.reset();
|
||||
startNode = focusNode = null;
|
||||
scoreMap = null;
|
||||
focusScores = null;
|
||||
oldFocusNode = null;
|
||||
randomStack = new Stack<SongNode>();
|
||||
songInfo = null;
|
||||
hoverOffset = 0f;
|
||||
hoverIndex = null;
|
||||
search.setText("");
|
||||
searchTimer = SEARCH_DELAY;
|
||||
searchTransitionTimer = SEARCH_TRANSITION_TIME;
|
||||
searchResultString = null;
|
||||
|
||||
// reload songs in new thread
|
||||
reloadThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// clear the beatmap cache
|
||||
BeatmapDB.clearDatabase();
|
||||
|
||||
// invoke unpacker and parser
|
||||
File beatmapDir = Options.getBeatmapDir();
|
||||
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
|
||||
BeatmapParser.parseAllFiles(beatmapDir);
|
||||
|
||||
// initialize song list
|
||||
if (BeatmapSetList.get().size() > 0) {
|
||||
BeatmapSetList.get().init();
|
||||
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
|
||||
} else
|
||||
MusicController.playThemeSong();
|
||||
|
||||
reloadThread = null;
|
||||
}
|
||||
};
|
||||
reloadThread.start();
|
||||
reloadBeatmaps(true);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -1210,15 +1304,25 @@ public class SongMenu extends BasicGameState {
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
hoverOffset = 0f;
|
||||
hoverOffset.setTime(0);
|
||||
hoverIndex = null;
|
||||
songInfo = null;
|
||||
songChangeTimer.setTime(0);
|
||||
musicIconBounceTimer.setTime(0);
|
||||
BeatmapSetNode oldFocus = focusNode;
|
||||
|
||||
// expand node before focusing it
|
||||
int expandedIndex = BeatmapSetList.get().getExpandedIndex();
|
||||
if (node.index != expandedIndex) {
|
||||
node = BeatmapSetList.get().expand(node.index);
|
||||
|
||||
|
||||
// calculate difficulties
|
||||
calculateStarRatings(node.getBeatmapSet());
|
||||
|
||||
// if start node was previously expanded, move it
|
||||
if (startNode != null && startNode.index == expandedIndex)
|
||||
startNode = BeatmapSetList.get().getBaseNode(startNode.index);
|
||||
}
|
||||
|
||||
// check beatmapIndex bounds
|
||||
@@ -1227,7 +1331,7 @@ public class SongMenu extends BasicGameState {
|
||||
beatmapIndex = (int) (Math.random() * length);
|
||||
|
||||
focusNode = BeatmapSetList.get().getNode(node, beatmapIndex);
|
||||
Beatmap beatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex);
|
||||
Beatmap beatmap = focusNode.getSelectedBeatmap();
|
||||
MusicController.play(beatmap, false, preview);
|
||||
|
||||
// load scores
|
||||
@@ -1286,6 +1390,14 @@ public class SongMenu extends BasicGameState {
|
||||
songScrolling.scrollToPosition((focusNode.index + focusNode.getBeatmapSet().size() ) * buttonOffset - (footerY - headerY));
|
||||
//*/
|
||||
|
||||
// load background image
|
||||
beatmap.loadBackground();
|
||||
boolean isBgNull = lastBackgroundImage == null || beatmap.bg == null;
|
||||
if ((isBgNull && lastBackgroundImage != beatmap.bg) || (!isBgNull && !beatmap.bg.equals(lastBackgroundImage))) {
|
||||
bgAlpha.setTime(0);
|
||||
lastBackgroundImage = beatmap.bg;
|
||||
}
|
||||
|
||||
return oldFocus;
|
||||
}
|
||||
|
||||
@@ -1346,7 +1458,7 @@ public class SongMenu extends BasicGameState {
|
||||
if (scoreMap == null || scoreMap.isEmpty() || node.beatmapIndex == -1) // node not expanded
|
||||
return null;
|
||||
|
||||
Beatmap beatmap = node.getBeatmapSet().get(node.beatmapIndex);
|
||||
Beatmap beatmap = node.getSelectedBeatmap();
|
||||
ScoreData[] scores = scoreMap.get(beatmap.version);
|
||||
if (scores == null || scores.length < 1) // no scores
|
||||
return null;
|
||||
@@ -1364,6 +1476,86 @@ public class SongMenu extends BasicGameState {
|
||||
return null; // incorrect map
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads all beatmaps.
|
||||
* @param fullReload if true, also clear the beatmap cache and invoke the unpacker
|
||||
*/
|
||||
private void reloadBeatmaps(final boolean fullReload) {
|
||||
songFolderChanged = false;
|
||||
|
||||
// reset state and node references
|
||||
MusicController.reset();
|
||||
startNode = focusNode = null;
|
||||
scoreMap = null;
|
||||
focusScores = null;
|
||||
oldFocusNode = null;
|
||||
randomStack = new Stack<SongNode>();
|
||||
songInfo = null;
|
||||
hoverOffset.setTime(0);
|
||||
hoverIndex = null;
|
||||
search.setText("");
|
||||
searchTimer = SEARCH_DELAY;
|
||||
searchTransitionTimer = SEARCH_TRANSITION_TIME;
|
||||
searchResultString = null;
|
||||
lastBackgroundImage = null;
|
||||
|
||||
// reload songs in new thread
|
||||
reloadThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
File beatmapDir = Options.getBeatmapDir();
|
||||
|
||||
if (fullReload) {
|
||||
// clear the beatmap cache
|
||||
BeatmapDB.clearDatabase();
|
||||
|
||||
// invoke unpacker
|
||||
OszUnpacker.unpackAllFiles(Options.getOSZDir(), beatmapDir);
|
||||
}
|
||||
|
||||
// invoke parser
|
||||
BeatmapParser.parseAllFiles(beatmapDir);
|
||||
|
||||
// initialize song list
|
||||
if (BeatmapSetList.get().size() > 0) {
|
||||
BeatmapSetList.get().init();
|
||||
setFocus(BeatmapSetList.get().getRandomNode(), -1, true, true);
|
||||
} else
|
||||
MusicController.playThemeSong();
|
||||
|
||||
reloadThread = null;
|
||||
}
|
||||
};
|
||||
reloadThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates all star ratings for a beatmap set.
|
||||
* @param beatmapSet the set of beatmaps
|
||||
*/
|
||||
private void calculateStarRatings(BeatmapSet beatmapSet) {
|
||||
for (Beatmap beatmap : beatmapSet) {
|
||||
if (beatmap.starRating >= 0) { // already calculated
|
||||
beatmapsCalculated.put(beatmap, beatmapsCalculated.get(beatmap));
|
||||
continue;
|
||||
}
|
||||
|
||||
// if timing points are already loaded before this (for whatever reason),
|
||||
// don't clear the array fields to be safe
|
||||
boolean hasTimingPoints = (beatmap.timingPoints != null);
|
||||
|
||||
BeatmapDifficultyCalculator diffCalc = new BeatmapDifficultyCalculator(beatmap);
|
||||
diffCalc.calculate();
|
||||
if (diffCalc.getStarRating() == -1)
|
||||
continue; // calculations failed
|
||||
|
||||
// save star rating
|
||||
beatmap.starRating = diffCalc.getStarRating();
|
||||
BeatmapDB.setStars(beatmap);
|
||||
beatmapsCalculated.put(beatmap, !hasTimingPoints);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the game.
|
||||
*/
|
||||
@@ -1372,8 +1564,12 @@ public class SongMenu extends BasicGameState {
|
||||
return;
|
||||
|
||||
SoundController.playSound(SoundEffect.MENUHIT);
|
||||
MultiClip.destroyExtraClips();
|
||||
Beatmap beatmap = MusicController.getBeatmap();
|
||||
if (focusNode == null || beatmap != focusNode.getSelectedBeatmap()) {
|
||||
UI.sendBarNotification("Unable to load the beatmap audio.");
|
||||
return;
|
||||
}
|
||||
MultiClip.destroyExtraClips();
|
||||
Game gameState = (Game) game.getState(Opsu.STATE_GAME);
|
||||
gameState.loadBeatmap(beatmap);
|
||||
gameState.setRestart(Game.Restart.NEW);
|
||||
|
||||
@@ -21,21 +21,23 @@ package itdelatrisu.opsu.states;
|
||||
import itdelatrisu.opsu.GameImage;
|
||||
import itdelatrisu.opsu.Opsu;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.OszUnpacker;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.audio.MusicController;
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapSetList;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapWatchService;
|
||||
import itdelatrisu.opsu.beatmap.OszUnpacker;
|
||||
import itdelatrisu.opsu.replay.ReplayImporter;
|
||||
import itdelatrisu.opsu.ui.UI;
|
||||
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.newdawn.slick.Color;
|
||||
import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.state.BasicGameState;
|
||||
@@ -47,6 +49,9 @@ import org.newdawn.slick.state.StateBasedGame;
|
||||
* Loads game resources and enters "Main Menu" state.
|
||||
*/
|
||||
public class Splash extends BasicGameState {
|
||||
/** Minimum time, in milliseconds, to display the splash screen (and fade in the logo). */
|
||||
private static final int MIN_SPLASH_TIME = 400;
|
||||
|
||||
/** Whether or not loading has completed. */
|
||||
private boolean finished = false;
|
||||
|
||||
@@ -59,8 +64,14 @@ public class Splash extends BasicGameState {
|
||||
/** Whether the skin being loaded is a new skin (for program restarts). */
|
||||
private boolean newSkin = false;
|
||||
|
||||
/** Whether the watch service is newly enabled (for program restarts). */
|
||||
private boolean watchServiceChange = false;
|
||||
|
||||
/** Logo alpha level. */
|
||||
private AnimatedValue logoAlpha;
|
||||
|
||||
// game-related variables
|
||||
private int state;
|
||||
private final int state;
|
||||
private GameContainer container;
|
||||
private boolean init = false;
|
||||
|
||||
@@ -77,9 +88,14 @@ public class Splash extends BasicGameState {
|
||||
if (Options.getSkin() != null)
|
||||
this.newSkin = (Options.getSkin().getDirectory() != Options.getSkinDir());
|
||||
|
||||
// check if watch service newly enabled
|
||||
this.watchServiceChange = Options.isWatchServiceEnabled() && BeatmapWatchService.get() == null;
|
||||
|
||||
// load Utils class first (needed in other 'init' methods)
|
||||
Utils.init(container, game);
|
||||
|
||||
// fade in logo
|
||||
this.logoAlpha = new AnimatedValue(MIN_SPLASH_TIME, 0f, 1f, AnimationEquation.LINEAR);
|
||||
GameImage.MENU_LOGO.getImage().setAlpha(0f);
|
||||
}
|
||||
|
||||
@@ -99,13 +115,18 @@ public class Splash extends BasicGameState {
|
||||
|
||||
// resources already loaded (from application restart)
|
||||
if (BeatmapSetList.get() != null) {
|
||||
// reload sounds if skin changed
|
||||
if (newSkin) {
|
||||
if (newSkin || watchServiceChange) { // need to reload resources
|
||||
thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
// reload beatmaps if watch service newly enabled
|
||||
if (watchServiceChange)
|
||||
BeatmapParser.parseAllFiles(Options.getBeatmapDir());
|
||||
|
||||
// reload sounds if skin changed
|
||||
// TODO: only reload each sound if actually needed?
|
||||
SoundController.init();
|
||||
if (newSkin)
|
||||
SoundController.init();
|
||||
|
||||
finished = true;
|
||||
thread = null;
|
||||
@@ -144,13 +165,11 @@ public class Splash extends BasicGameState {
|
||||
}
|
||||
|
||||
// fade in logo
|
||||
Image logo = GameImage.MENU_LOGO.getImage();
|
||||
float alpha = logo.getAlpha();
|
||||
if (alpha < 1f)
|
||||
logo.setAlpha(alpha + (delta / 500f));
|
||||
if (logoAlpha.update(delta))
|
||||
GameImage.MENU_LOGO.getImage().setAlpha(logoAlpha.getValue());
|
||||
|
||||
// change states when loading complete
|
||||
if (finished && alpha >= 1f) {
|
||||
if (finished && logoAlpha.getValue() >= 1f) {
|
||||
// initialize song list
|
||||
if (BeatmapSetList.get().size() > 0) {
|
||||
BeatmapSetList.get().init();
|
||||
|
||||
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.Utils;
|
||||
import itdelatrisu.opsu.skins.Skin;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import java.awt.Point;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.lwjgl.BufferUtils;
|
||||
@@ -45,13 +46,25 @@ public class Cursor {
|
||||
private static org.lwjgl.input.Cursor emptyCursor;
|
||||
|
||||
/** Last cursor coordinates. */
|
||||
private int lastX = -1, lastY = -1;
|
||||
private Point lastPosition;
|
||||
|
||||
/** Cursor rotation angle. */
|
||||
private float cursorAngle = 0f;
|
||||
|
||||
/** The time in milliseconds when the cursor was last pressed, used for the scaling animation. */
|
||||
private long lastCursorPressTime = 0L;
|
||||
|
||||
/** Whether or not the cursor was pressed in the last frame, used for the scaling animation. */
|
||||
private boolean lastCursorPressState = false;
|
||||
|
||||
/** The amount the cursor scale increases, if enabled, when pressed. */
|
||||
private static final float CURSOR_SCALE_CHANGE = 0.25f;
|
||||
|
||||
/** The time it takes for the cursor to scale, in milliseconds. */
|
||||
private static final float CURSOR_SCALE_TIME = 125;
|
||||
|
||||
/** Stores all previous cursor locations to display a trail. */
|
||||
private LinkedList<Integer> cursorX, cursorY;
|
||||
private LinkedList<Point> trail = new LinkedList<Point>();
|
||||
|
||||
// game-related variables
|
||||
private static GameContainer container;
|
||||
@@ -81,10 +94,7 @@ public class Cursor {
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public Cursor() {
|
||||
cursorX = new LinkedList<Integer>();
|
||||
cursorY = new LinkedList<Integer>();
|
||||
}
|
||||
public Cursor() {}
|
||||
|
||||
/**
|
||||
* Draws the cursor.
|
||||
@@ -105,85 +115,90 @@ public class Cursor {
|
||||
* @param mousePressed whether or not the mouse button is pressed
|
||||
*/
|
||||
public void draw(int mouseX, int mouseY, boolean mousePressed) {
|
||||
if (Options.isCursorDisabled())
|
||||
return;
|
||||
|
||||
// determine correct cursor image
|
||||
Image cursor = null, cursorMiddle = null, cursorTrail = null;
|
||||
boolean skinned = GameImage.CURSOR.hasSkinImage();
|
||||
boolean beatmapSkinned = GameImage.CURSOR.hasBeatmapSkinImage();
|
||||
boolean newStyle, hasMiddle;
|
||||
if (skinned) {
|
||||
Skin skin = Options.getSkin();
|
||||
if (beatmapSkinned) {
|
||||
newStyle = true; // osu! currently treats all beatmap cursors as new-style cursors
|
||||
hasMiddle = GameImage.CURSOR_MIDDLE.hasSkinImage();
|
||||
hasMiddle = GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage();
|
||||
} else
|
||||
newStyle = hasMiddle = Options.isNewCursorEnabled();
|
||||
if (skinned || newStyle) {
|
||||
if (newStyle || beatmapSkinned) {
|
||||
cursor = GameImage.CURSOR.getImage();
|
||||
cursorTrail = GameImage.CURSOR_TRAIL.getImage();
|
||||
} else {
|
||||
cursor = GameImage.CURSOR_OLD.getImage();
|
||||
cursorTrail = GameImage.CURSOR_TRAIL_OLD.getImage();
|
||||
cursor = GameImage.CURSOR.hasGameSkinImage() ? GameImage.CURSOR.getImage() : GameImage.CURSOR_OLD.getImage();
|
||||
cursorTrail = GameImage.CURSOR_TRAIL.hasGameSkinImage() ? GameImage.CURSOR_TRAIL.getImage() : GameImage.CURSOR_TRAIL_OLD.getImage();
|
||||
}
|
||||
if (hasMiddle)
|
||||
cursorMiddle = GameImage.CURSOR_MIDDLE.getImage();
|
||||
|
||||
int removeCount = 0;
|
||||
int FPSmod = (Options.getTargetFPS() / 60);
|
||||
Skin skin = Options.getSkin();
|
||||
|
||||
// scale cursor
|
||||
float cursorScale = Options.getCursorScale();
|
||||
if (mousePressed && skin.isCursorExpanded())
|
||||
cursorScale *= 1.25f; // increase the cursor size if pressed
|
||||
float cursorScaleAnimated = 1f;
|
||||
if (skin.isCursorExpanded()) {
|
||||
if (lastCursorPressState != mousePressed) {
|
||||
lastCursorPressState = mousePressed;
|
||||
lastCursorPressTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
float cursorScaleChange = CURSOR_SCALE_CHANGE * AnimationEquation.IN_OUT_CUBIC.calc(
|
||||
Utils.clamp(System.currentTimeMillis() - lastCursorPressTime, 0, CURSOR_SCALE_TIME) / CURSOR_SCALE_TIME);
|
||||
cursorScaleAnimated = 1f + ((mousePressed) ? cursorScaleChange : CURSOR_SCALE_CHANGE - cursorScaleChange);
|
||||
}
|
||||
float cursorScale = cursorScaleAnimated * Options.getCursorScale();
|
||||
if (cursorScale != 1f) {
|
||||
cursor = cursor.getScaledCopy(cursorScale);
|
||||
cursorTrail = cursorTrail.getScaledCopy(cursorScale);
|
||||
if (hasMiddle)
|
||||
cursorMiddle = cursorMiddle.getScaledCopy(cursorScale);
|
||||
}
|
||||
|
||||
// TODO: use an image buffer
|
||||
int removeCount = 0;
|
||||
float FPSmod = Math.max(container.getFPS(), 1) / 60f;
|
||||
if (newStyle) {
|
||||
// new style: add all points between cursor movements
|
||||
if (lastX < 0) {
|
||||
lastX = mouseX;
|
||||
lastY = mouseY;
|
||||
if (lastPosition == null) {
|
||||
lastPosition = new Point(mouseX, mouseY);
|
||||
return;
|
||||
}
|
||||
addCursorPoints(lastX, lastY, mouseX, mouseY);
|
||||
lastX = mouseX;
|
||||
lastY = mouseY;
|
||||
addCursorPoints(lastPosition.x, lastPosition.y, mouseX, mouseY);
|
||||
lastPosition.move(mouseX, mouseY);
|
||||
|
||||
removeCount = (cursorX.size() / (6 * FPSmod)) + 1;
|
||||
removeCount = (int) (trail.size() / (6 * FPSmod)) + 1;
|
||||
} else {
|
||||
// old style: sample one point at a time
|
||||
cursorX.add(mouseX);
|
||||
cursorY.add(mouseY);
|
||||
trail.add(new Point(mouseX, mouseY));
|
||||
|
||||
int max = 10 * FPSmod;
|
||||
if (cursorX.size() > max)
|
||||
removeCount = cursorX.size() - max;
|
||||
int max = (int) (10 * FPSmod);
|
||||
if (trail.size() > max)
|
||||
removeCount = trail.size() - max;
|
||||
}
|
||||
|
||||
// remove points from the lists
|
||||
for (int i = 0; i < removeCount && !cursorX.isEmpty(); i++) {
|
||||
cursorX.remove();
|
||||
cursorY.remove();
|
||||
}
|
||||
for (int i = 0; i < removeCount && !trail.isEmpty(); i++)
|
||||
trail.remove();
|
||||
|
||||
// draw a fading trail
|
||||
float alpha = 0f;
|
||||
float t = 2f / cursorX.size();
|
||||
if (skin.isCursorTrailRotated())
|
||||
cursorTrail.setRotation(cursorAngle);
|
||||
Iterator<Integer> iterX = cursorX.iterator();
|
||||
Iterator<Integer> iterY = cursorY.iterator();
|
||||
while (iterX.hasNext()) {
|
||||
int cx = iterX.next();
|
||||
int cy = iterY.next();
|
||||
float t = 2f / trail.size();
|
||||
int cursorTrailWidth = cursorTrail.getWidth(), cursorTrailHeight = cursorTrail.getHeight();
|
||||
float cursorTrailRotation = (skin.isCursorTrailRotated()) ? cursorAngle : 0;
|
||||
cursorTrail.startUse();
|
||||
for (Point p : trail) {
|
||||
alpha += t;
|
||||
cursorTrail.setAlpha(alpha);
|
||||
// if (cx != x || cy != y)
|
||||
cursorTrail.drawCentered(cx, cy);
|
||||
cursorTrail.setImageColor(1f, 1f, 1f, alpha);
|
||||
cursorTrail.drawEmbedded(
|
||||
p.x - (cursorTrailWidth / 2f), p.y - (cursorTrailHeight / 2f),
|
||||
cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
|
||||
}
|
||||
cursorTrail.drawCentered(mouseX, mouseY);
|
||||
cursorTrail.drawEmbedded(
|
||||
mouseX - (cursorTrailWidth / 2f), mouseY - (cursorTrailHeight / 2f),
|
||||
cursorTrailWidth, cursorTrailHeight, cursorTrailRotation);
|
||||
cursorTrail.endUse();
|
||||
|
||||
// draw the other components
|
||||
if (newStyle && skin.isCursorRotated())
|
||||
@@ -212,8 +227,7 @@ public class Cursor {
|
||||
if (dy <= dx) {
|
||||
for (int i = 0; ; i++) {
|
||||
if (i == k) {
|
||||
cursorX.add(x1);
|
||||
cursorY.add(y1);
|
||||
trail.add(new Point(x1, y1));
|
||||
i = 0;
|
||||
}
|
||||
if (x1 == x2)
|
||||
@@ -228,8 +242,7 @@ public class Cursor {
|
||||
} else {
|
||||
for (int i = 0; ; i++) {
|
||||
if (i == k) {
|
||||
cursorX.add(x1);
|
||||
cursorY.add(y1);
|
||||
trail.add(new Point(x1, y1));
|
||||
i = 0;
|
||||
}
|
||||
if (y1 == y2)
|
||||
@@ -255,13 +268,13 @@ public class Cursor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets all cursor data and skins.
|
||||
* Resets all cursor data and beatmap skins.
|
||||
*/
|
||||
public void reset() {
|
||||
// destroy skin images
|
||||
GameImage.CURSOR.destroySkinImage();
|
||||
GameImage.CURSOR_MIDDLE.destroySkinImage();
|
||||
GameImage.CURSOR_TRAIL.destroySkinImage();
|
||||
GameImage.CURSOR.destroyBeatmapSkinImage();
|
||||
GameImage.CURSOR_MIDDLE.destroyBeatmapSkinImage();
|
||||
GameImage.CURSOR_TRAIL.destroyBeatmapSkinImage();
|
||||
|
||||
// reset locations
|
||||
resetLocations();
|
||||
@@ -276,18 +289,17 @@ public class Cursor {
|
||||
* Resets all cursor location data.
|
||||
*/
|
||||
public void resetLocations() {
|
||||
lastX = lastY = -1;
|
||||
cursorX.clear();
|
||||
cursorY.clear();
|
||||
lastPosition = null;
|
||||
trail.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the cursor is skinned.
|
||||
*/
|
||||
public boolean isSkinned() {
|
||||
return (GameImage.CURSOR.hasSkinImage() ||
|
||||
GameImage.CURSOR_MIDDLE.hasSkinImage() ||
|
||||
GameImage.CURSOR_TRAIL.hasSkinImage());
|
||||
public boolean isBeatmapSkinned() {
|
||||
return (GameImage.CURSOR.hasBeatmapSkinImage() ||
|
||||
GameImage.CURSOR_MIDDLE.hasBeatmapSkinImage() ||
|
||||
GameImage.CURSOR_TRAIL.hasBeatmapSkinImage());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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;
|
||||
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import org.newdawn.slick.Animation;
|
||||
import org.newdawn.slick.Color;
|
||||
@@ -63,11 +65,26 @@ public class MenuButton {
|
||||
/** The hover actions for this button. */
|
||||
private int hoverEffect = 0;
|
||||
|
||||
/** The current and max scale of the button. */
|
||||
private float scale = 1f, hoverScale = 1.25f;
|
||||
/** The hover animation duration, in milliseconds. */
|
||||
private int animationDuration = 100;
|
||||
|
||||
/** The current and base alpha level of the button. */
|
||||
private float alpha = 1f, baseAlpha = 0.75f;
|
||||
/** The hover animation equation. */
|
||||
private AnimationEquation animationEqn = AnimationEquation.LINEAR;
|
||||
|
||||
/** Whether the animation is advancing forwards (if advancing automatically). */
|
||||
private boolean autoAnimationForward = true;
|
||||
|
||||
/** The scale of the button. */
|
||||
private AnimatedValue scale;
|
||||
|
||||
/** The default max scale of the button. */
|
||||
private static final float DEFAULT_SCALE_MAX = 1.25f;
|
||||
|
||||
/** The alpha level of the button. */
|
||||
private AnimatedValue alpha;
|
||||
|
||||
/** The default base alpha level of the button. */
|
||||
private static final float DEFAULT_ALPHA_BASE = 0.75f;
|
||||
|
||||
/** The scaled expansion direction for the button. */
|
||||
private Expand dir = Expand.CENTER;
|
||||
@@ -75,8 +92,11 @@ public class MenuButton {
|
||||
/** Scaled expansion directions. */
|
||||
public enum Expand { CENTER, UP, RIGHT, LEFT, DOWN, UP_RIGHT, UP_LEFT, DOWN_RIGHT, DOWN_LEFT; }
|
||||
|
||||
/** The current and max rotation angles of the button. */
|
||||
private float angle = 0f, maxAngle = 30f;
|
||||
/** The rotation angle of the button. */
|
||||
private AnimatedValue angle;
|
||||
|
||||
/** The default max rotation angle of the button. */
|
||||
private static final float DEFAULT_ANGLE_MAX = 30f;
|
||||
|
||||
/**
|
||||
* Creates a new button from an Image.
|
||||
@@ -126,11 +146,13 @@ public class MenuButton {
|
||||
|
||||
/**
|
||||
* Sets a new center x coordinate.
|
||||
* @param x the x coordinate
|
||||
*/
|
||||
public void setX(float x) { this.x = x; }
|
||||
|
||||
/**
|
||||
* Sets a new center y coordinate.
|
||||
* @param y the y coordinate
|
||||
*/
|
||||
public void setY(float y) { this.y = y; }
|
||||
|
||||
@@ -192,15 +214,15 @@ public class MenuButton {
|
||||
float oldAlpha = image.getAlpha();
|
||||
float oldAngle = image.getRotation();
|
||||
if ((hoverEffect & EFFECT_EXPAND) > 0) {
|
||||
if (scale != 1f) {
|
||||
image = image.getScaledCopy(scale);
|
||||
if (scale.getValue() != 1f) {
|
||||
image = image.getScaledCopy(scale.getValue());
|
||||
image.setAlpha(oldAlpha);
|
||||
}
|
||||
}
|
||||
if ((hoverEffect & EFFECT_FADE) > 0)
|
||||
image.setAlpha(alpha);
|
||||
image.setAlpha(alpha.getValue());
|
||||
if ((hoverEffect & EFFECT_ROTATE) > 0)
|
||||
image.setRotation(angle);
|
||||
image.setRotation(angle.getValue());
|
||||
image.draw(x - xRadius, y - yRadius, filter);
|
||||
if (image == this.img) {
|
||||
image.setAlpha(oldAlpha);
|
||||
@@ -217,9 +239,10 @@ public class MenuButton {
|
||||
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
|
||||
} else if ((hoverEffect & EFFECT_FADE) > 0) {
|
||||
float a = image.getAlpha(), aL = imgL.getAlpha(), aR = imgR.getAlpha();
|
||||
image.setAlpha(alpha);
|
||||
imgL.setAlpha(alpha);
|
||||
imgR.setAlpha(alpha);
|
||||
float currentAlpha = alpha.getValue();
|
||||
image.setAlpha(currentAlpha);
|
||||
imgL.setAlpha(currentAlpha);
|
||||
imgR.setAlpha(currentAlpha);
|
||||
image.draw(x - xRadius + imgL.getWidth(), y - yRadius, filter);
|
||||
imgL.draw(x - xRadius, y - yRadius, filter);
|
||||
imgR.draw(x + xRadius - imgR.getWidth(), y - yRadius, filter);
|
||||
@@ -267,28 +290,63 @@ public class MenuButton {
|
||||
*/
|
||||
public void resetHover() {
|
||||
if ((hoverEffect & EFFECT_EXPAND) > 0) {
|
||||
this.scale = 1f;
|
||||
scale.setTime(0);
|
||||
setHoverRadius();
|
||||
}
|
||||
if ((hoverEffect & EFFECT_FADE) > 0)
|
||||
this.alpha = baseAlpha;
|
||||
alpha.setTime(0);
|
||||
if ((hoverEffect & EFFECT_ROTATE) > 0)
|
||||
this.angle = 0f;
|
||||
angle.setTime(0);
|
||||
autoAnimationForward = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all hover effects that have been set for the button.
|
||||
*/
|
||||
public void removeHoverEffects() { hoverEffect = 0; }
|
||||
public void removeHoverEffects() {
|
||||
this.hoverEffect = 0;
|
||||
this.scale = null;
|
||||
this.alpha = null;
|
||||
this.angle = null;
|
||||
autoAnimationForward = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hover animation duration.
|
||||
* @param duration the duration, in milliseconds
|
||||
*/
|
||||
public void setHoverAnimationDuration(int duration) {
|
||||
this.animationDuration = duration;
|
||||
if (scale != null)
|
||||
scale.setDuration(duration);
|
||||
if (alpha != null)
|
||||
alpha.setDuration(duration);
|
||||
if (angle != null)
|
||||
angle.setDuration(duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the hover animation equation.
|
||||
* @param eqn the equation to use
|
||||
*/
|
||||
public void setHoverAnimationEquation(AnimationEquation eqn) {
|
||||
this.animationEqn = eqn;
|
||||
if (scale != null)
|
||||
scale.setEquation(eqn);
|
||||
if (alpha != null)
|
||||
alpha.setEquation(eqn);
|
||||
if (angle != null)
|
||||
angle.setEquation(eqn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the "expand" hover effect.
|
||||
*/
|
||||
public void setHoverExpand() { hoverEffect |= EFFECT_EXPAND; }
|
||||
public void setHoverExpand() { setHoverExpand(DEFAULT_SCALE_MAX, this.dir); }
|
||||
|
||||
/**
|
||||
* Sets the "expand" hover effect.
|
||||
* @param scale the maximum scale factor (default 1.25f)
|
||||
* @param scale the maximum scale factor
|
||||
*/
|
||||
public void setHoverExpand(float scale) { setHoverExpand(scale, this.dir); }
|
||||
|
||||
@@ -296,45 +354,45 @@ public class MenuButton {
|
||||
* Sets the "expand" hover effect.
|
||||
* @param dir the expansion direction
|
||||
*/
|
||||
public void setHoverExpand(Expand dir) { setHoverExpand(this.hoverScale, dir); }
|
||||
public void setHoverExpand(Expand dir) { setHoverExpand(DEFAULT_SCALE_MAX, dir); }
|
||||
|
||||
/**
|
||||
* Sets the "expand" hover effect.
|
||||
* @param scale the maximum scale factor (default 1.25f)
|
||||
* @param scale the maximum scale factor
|
||||
* @param dir the expansion direction
|
||||
*/
|
||||
public void setHoverExpand(float scale, Expand dir) {
|
||||
hoverEffect |= EFFECT_EXPAND;
|
||||
this.hoverScale = scale;
|
||||
this.scale = new AnimatedValue(animationDuration, 1f, scale, animationEqn);
|
||||
this.dir = dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the "fade" hover effect.
|
||||
*/
|
||||
public void setHoverFade() { hoverEffect |= EFFECT_FADE; }
|
||||
public void setHoverFade() { setHoverFade(DEFAULT_ALPHA_BASE); }
|
||||
|
||||
/**
|
||||
* Sets the "fade" hover effect.
|
||||
* @param baseAlpha the base alpha level to fade in from (default 0.7f)
|
||||
* @param baseAlpha the base alpha level to fade in from
|
||||
*/
|
||||
public void setHoverFade(float baseAlpha) {
|
||||
hoverEffect |= EFFECT_FADE;
|
||||
this.baseAlpha = baseAlpha;
|
||||
this.alpha = new AnimatedValue(animationDuration, baseAlpha, 1f, animationEqn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the "rotate" hover effect.
|
||||
*/
|
||||
public void setHoverRotate() { hoverEffect |= EFFECT_ROTATE; }
|
||||
public void setHoverRotate() { setHoverRotate(DEFAULT_ANGLE_MAX); }
|
||||
|
||||
/**
|
||||
* Sets the "rotate" hover effect.
|
||||
* @param maxAngle the maximum rotation angle, in degrees (default 30f)
|
||||
* @param maxAngle the maximum rotation angle, in degrees
|
||||
*/
|
||||
public void setHoverRotate(float maxAngle) {
|
||||
hoverEffect |= EFFECT_ROTATE;
|
||||
this.maxAngle = maxAngle;
|
||||
this.angle = new AnimatedValue(animationDuration, 0f, maxAngle, animationEqn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -371,45 +429,53 @@ public class MenuButton {
|
||||
if (hoverEffect == 0)
|
||||
return;
|
||||
|
||||
int d = delta * (isHover ? 1 : -1);
|
||||
|
||||
// scale the button
|
||||
if ((hoverEffect & EFFECT_EXPAND) > 0) {
|
||||
int sign = 0;
|
||||
if (isHover && scale < hoverScale)
|
||||
sign = 1;
|
||||
else if (!isHover && scale > 1f)
|
||||
sign = -1;
|
||||
if (sign != 0) {
|
||||
scale = Utils.getBoundedValue(scale, sign * (hoverScale - 1f) * delta / 100f, 1, hoverScale);
|
||||
if (scale.update(d))
|
||||
setHoverRadius();
|
||||
}
|
||||
}
|
||||
|
||||
// fade the button
|
||||
if ((hoverEffect & EFFECT_FADE) > 0) {
|
||||
int sign = 0;
|
||||
if (isHover && alpha < 1f)
|
||||
sign = 1;
|
||||
else if (!isHover && alpha > baseAlpha)
|
||||
sign = -1;
|
||||
if (sign != 0)
|
||||
alpha = Utils.getBoundedValue(alpha, sign * (1f - baseAlpha) * delta / 200f, baseAlpha, 1f);
|
||||
}
|
||||
if ((hoverEffect & EFFECT_FADE) > 0)
|
||||
alpha.update(d);
|
||||
|
||||
// rotate the button
|
||||
if ((hoverEffect & EFFECT_ROTATE) > 0) {
|
||||
int sign = 0;
|
||||
boolean right = (maxAngle > 0);
|
||||
if (isHover && angle != maxAngle)
|
||||
sign = (right) ? 1 : -1;
|
||||
else if (!isHover && angle != 0)
|
||||
sign = (right) ? -1 : 1;
|
||||
if (sign != 0) {
|
||||
float diff = sign * Math.abs(maxAngle) * delta / 125f;
|
||||
angle = (right) ?
|
||||
Utils.getBoundedValue(angle, diff, 0, maxAngle) :
|
||||
Utils.getBoundedValue(angle, diff, maxAngle, 0);
|
||||
if ((hoverEffect & EFFECT_ROTATE) > 0)
|
||||
angle.update(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically advances the hover animation in a loop.
|
||||
* @param delta the delta interval
|
||||
* @param reverseAtEnd whether to reverse or restart the animation upon reaching the end
|
||||
*/
|
||||
public void autoHoverUpdate(int delta, boolean reverseAtEnd) {
|
||||
if (hoverEffect == 0)
|
||||
return;
|
||||
|
||||
int time = ((hoverEffect & EFFECT_EXPAND) > 0) ? scale.getTime() :
|
||||
((hoverEffect & EFFECT_FADE) > 0) ? alpha.getTime() :
|
||||
((hoverEffect & EFFECT_ROTATE) > 0) ? angle.getTime() : -1;
|
||||
if (time == -1)
|
||||
return;
|
||||
|
||||
int d = delta * (autoAnimationForward ? 1 : -1);
|
||||
if (Utils.clamp(time + d, 0, animationDuration) == time) {
|
||||
if (reverseAtEnd)
|
||||
autoAnimationForward = !autoAnimationForward;
|
||||
else {
|
||||
if ((hoverEffect & EFFECT_EXPAND) > 0)
|
||||
scale.setTime(0);
|
||||
if ((hoverEffect & EFFECT_FADE) > 0)
|
||||
alpha.setTime(0);
|
||||
if ((hoverEffect & EFFECT_ROTATE) > 0)
|
||||
angle.setTime(0);
|
||||
}
|
||||
}
|
||||
|
||||
hoverUpdate(delta, autoAnimationForward);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -422,10 +488,11 @@ public class MenuButton {
|
||||
image = anim.getCurrentFrame();
|
||||
|
||||
int xOffset = 0, yOffset = 0;
|
||||
float currentScale = scale.getValue();
|
||||
if (dir != Expand.CENTER) {
|
||||
// offset by difference between normal/scaled image dimensions
|
||||
xOffset = (int) ((scale - 1f) * image.getWidth());
|
||||
yOffset = (int) ((scale - 1f) * image.getHeight());
|
||||
xOffset = (int) ((currentScale - 1f) * image.getWidth());
|
||||
yOffset = (int) ((currentScale - 1f) * image.getHeight());
|
||||
if (dir == Expand.UP || dir == Expand.DOWN)
|
||||
xOffset = 0; // no horizontal offset
|
||||
if (dir == Expand.RIGHT || dir == Expand.LEFT)
|
||||
@@ -435,7 +502,7 @@ public class MenuButton {
|
||||
if (dir == Expand.DOWN || dir == Expand.DOWN_LEFT || dir == Expand.DOWN_RIGHT)
|
||||
yOffset *= -1; // flip y for down
|
||||
}
|
||||
this.xRadius = ((image.getWidth() * scale) + xOffset) / 2f;
|
||||
this.yRadius = ((image.getHeight() * scale) + yOffset) / 2f;
|
||||
this.xRadius = ((image.getWidth() * currentScale) + xOffset) / 2f;
|
||||
this.yRadius = ((image.getHeight() * currentScale) + yOffset) / 2f;
|
||||
}
|
||||
}
|
||||
|
||||
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.GameImage;
|
||||
import itdelatrisu.opsu.Options;
|
||||
import itdelatrisu.opsu.OszUnpacker;
|
||||
import itdelatrisu.opsu.Utils;
|
||||
import itdelatrisu.opsu.audio.SoundController;
|
||||
import itdelatrisu.opsu.beatmap.BeatmapParser;
|
||||
import itdelatrisu.opsu.beatmap.OszUnpacker;
|
||||
import itdelatrisu.opsu.replay.ReplayImporter;
|
||||
import itdelatrisu.opsu.ui.animations.AnimatedValue;
|
||||
import itdelatrisu.opsu.ui.animations.AnimationEquation;
|
||||
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.UIManager;
|
||||
@@ -36,7 +38,6 @@ import org.newdawn.slick.GameContainer;
|
||||
import org.newdawn.slick.Graphics;
|
||||
import org.newdawn.slick.Image;
|
||||
import org.newdawn.slick.Input;
|
||||
import org.newdawn.slick.SlickException;
|
||||
import org.newdawn.slick.state.StateBasedGame;
|
||||
|
||||
/**
|
||||
@@ -62,7 +63,7 @@ public class UI {
|
||||
private static int barNotifTimer = -1;
|
||||
|
||||
/** Duration, in milliseconds, to display bar notifications. */
|
||||
private static final int BAR_NOTIFICATION_TIME = 1250;
|
||||
private static final int BAR_NOTIFICATION_TIME = 1500;
|
||||
|
||||
/** The current tooltip. */
|
||||
private static String tooltip;
|
||||
@@ -70,11 +71,8 @@ public class UI {
|
||||
/** Whether or not to check the current tooltip for line breaks. */
|
||||
private static boolean tooltipNewlines;
|
||||
|
||||
/** The current tooltip timer. */
|
||||
private static int tooltipTimer = -1;
|
||||
|
||||
/** Duration, in milliseconds, to fade tooltips. */
|
||||
private static final int TOOLTIP_FADE_TIME = 200;
|
||||
/** The alpha level of the current tooltip (if any). */
|
||||
private static AnimatedValue tooltipAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);
|
||||
|
||||
// game-related variables
|
||||
private static GameContainer container;
|
||||
@@ -87,10 +85,8 @@ public class UI {
|
||||
* Initializes UI data.
|
||||
* @param container the game container
|
||||
* @param game the game object
|
||||
* @throws SlickException
|
||||
*/
|
||||
public static void init(GameContainer container, StateBasedGame game)
|
||||
throws SlickException {
|
||||
public static void init(GameContainer container, StateBasedGame game) {
|
||||
UI.container = container;
|
||||
UI.input = container.getInput();
|
||||
|
||||
@@ -106,6 +102,8 @@ public class UI {
|
||||
Image back = GameImage.MENU_BACK.getImage();
|
||||
backButton = new MenuButton(back, back.getWidth() / 2f, container.getHeight() - (back.getHeight() / 2f));
|
||||
}
|
||||
backButton.setHoverAnimationDuration(350);
|
||||
backButton.setHoverAnimationEquation(AnimationEquation.IN_OUT_BACK);
|
||||
backButton.setHoverExpand(MenuButton.Expand.UP_RIGHT);
|
||||
}
|
||||
|
||||
@@ -117,12 +115,11 @@ public class UI {
|
||||
cursor.update(delta);
|
||||
updateVolumeDisplay(delta);
|
||||
updateBarNotification(delta);
|
||||
if (tooltipTimer > 0)
|
||||
tooltipTimer -= delta;
|
||||
tooltipAlpha.update(-delta);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
|
||||
* Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications.
|
||||
* @param g the graphics context
|
||||
*/
|
||||
public static void draw(Graphics g) {
|
||||
@@ -134,7 +131,7 @@ public class UI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the global UI components: cursor, FPS, volume bar, bar notifications.
|
||||
* Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications.
|
||||
* @param g the graphics context
|
||||
* @param mouseX the mouse x coordinate
|
||||
* @param mouseY the mouse y coordinate
|
||||
@@ -178,18 +175,18 @@ public class UI {
|
||||
*/
|
||||
public static void drawTab(float x, float y, String text, boolean selected, boolean isHover) {
|
||||
Image tabImage = GameImage.MENU_TAB.getImage();
|
||||
float tabTextX = x - (Utils.FONT_MEDIUM.getWidth(text) / 2);
|
||||
float tabTextX = x - (Fonts.MEDIUM.getWidth(text) / 2);
|
||||
float tabTextY = y - (tabImage.getHeight() / 2);
|
||||
Color filter, textColor;
|
||||
if (selected) {
|
||||
filter = Color.white;
|
||||
textColor = Color.black;
|
||||
} else {
|
||||
filter = (isHover) ? Utils.COLOR_RED_HOVER : Color.red;
|
||||
filter = (isHover) ? Colors.RED_HOVER : Color.red;
|
||||
textColor = Color.white;
|
||||
}
|
||||
tabImage.drawCentered(x, y, filter);
|
||||
Utils.FONT_MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
|
||||
Fonts.MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,14 +198,14 @@ public class UI {
|
||||
return;
|
||||
|
||||
String fps = String.format("%dFPS", container.getFPS());
|
||||
Utils.FONT_BOLD.drawString(
|
||||
container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth(fps),
|
||||
container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight(fps),
|
||||
Fonts.BOLD.drawString(
|
||||
container.getWidth() * 0.997f - Fonts.BOLD.getWidth(fps),
|
||||
container.getHeight() * 0.997f - Fonts.BOLD.getHeight(fps),
|
||||
Integer.toString(container.getFPS()), Color.white
|
||||
);
|
||||
Utils.FONT_DEFAULT.drawString(
|
||||
container.getWidth() * 0.997f - Utils.FONT_BOLD.getWidth("FPS"),
|
||||
container.getHeight() * 0.997f - Utils.FONT_BOLD.getHeight("FPS"),
|
||||
Fonts.DEFAULT.drawString(
|
||||
container.getWidth() * 0.997f - Fonts.BOLD.getWidth("FPS"),
|
||||
container.getHeight() * 0.997f - Fonts.BOLD.getHeight("FPS"),
|
||||
"FPS", Color.white
|
||||
);
|
||||
}
|
||||
@@ -263,7 +260,7 @@ public class UI {
|
||||
*/
|
||||
public static void changeVolume(int units) {
|
||||
final float UNIT_OFFSET = 0.05f;
|
||||
Options.setMasterVolume(container, Utils.getBoundedValue(Options.getMasterVolume(), UNIT_OFFSET * units, 0f, 1f));
|
||||
Options.setMasterVolume(container, Utils.clamp(Options.getMasterVolume() + (UNIT_OFFSET * units), 0f, 1f));
|
||||
if (volumeDisplay == -1)
|
||||
volumeDisplay = 0;
|
||||
else if (volumeDisplay >= VOLUME_DISPLAY_TIME / 10)
|
||||
@@ -271,8 +268,9 @@ public class UI {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws loading progress (OSZ unpacking, beatmap parsing, sound loading)
|
||||
* Draws loading progress (OSZ unpacking, beatmap parsing, replay importing, sound loading)
|
||||
* at the bottom of the screen.
|
||||
* @param g the graphics context
|
||||
*/
|
||||
public static void drawLoadingProgress(Graphics g) {
|
||||
String text, file;
|
||||
@@ -298,16 +296,16 @@ public class UI {
|
||||
// draw loading info
|
||||
float marginX = container.getWidth() * 0.02f, marginY = container.getHeight() * 0.02f;
|
||||
float lineY = container.getHeight() - marginY;
|
||||
int lineOffsetY = Utils.FONT_MEDIUM.getLineHeight();
|
||||
int lineOffsetY = Fonts.MEDIUM.getLineHeight();
|
||||
if (Options.isLoadVerbose()) {
|
||||
// verbose: display percentages and file names
|
||||
Utils.FONT_MEDIUM.drawString(
|
||||
Fonts.MEDIUM.drawString(
|
||||
marginX, lineY - (lineOffsetY * 2),
|
||||
String.format("%s (%d%%)", text, progress), Color.white);
|
||||
Utils.FONT_MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
|
||||
Fonts.MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Color.white);
|
||||
} else {
|
||||
// draw loading bar
|
||||
Utils.FONT_MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
|
||||
Fonts.MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Color.white);
|
||||
g.setColor(Color.white);
|
||||
g.fillRoundRect(marginX, lineY - (lineOffsetY / 2f),
|
||||
(container.getWidth() - (marginX * 2f)) * progress / 100f, lineOffsetY / 4f, 4
|
||||
@@ -357,12 +355,7 @@ public class UI {
|
||||
if (s != null) {
|
||||
tooltip = s;
|
||||
tooltipNewlines = newlines;
|
||||
if (tooltipTimer <= 0)
|
||||
tooltipTimer = delta;
|
||||
else
|
||||
tooltipTimer += delta * 2;
|
||||
if (tooltipTimer > TOOLTIP_FADE_TIME)
|
||||
tooltipTimer = TOOLTIP_FADE_TIME;
|
||||
tooltipAlpha.update(delta * 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,26 +365,26 @@ public class UI {
|
||||
* @param g the graphics context
|
||||
*/
|
||||
public static void drawTooltip(Graphics g) {
|
||||
if (tooltipTimer <= 0 || tooltip == null)
|
||||
if (tooltipAlpha.getTime() == 0 || tooltip == null)
|
||||
return;
|
||||
|
||||
int containerWidth = container.getWidth(), containerHeight = container.getHeight();
|
||||
int margin = containerWidth / 100, textMarginX = 2;
|
||||
int offset = GameImage.CURSOR_MIDDLE.getImage().getWidth() / 2;
|
||||
int lineHeight = Utils.FONT_SMALL.getLineHeight();
|
||||
int lineHeight = Fonts.SMALL.getLineHeight();
|
||||
int textWidth = textMarginX * 2, textHeight = lineHeight;
|
||||
if (tooltipNewlines) {
|
||||
String[] lines = tooltip.split("\\n");
|
||||
int maxWidth = Utils.FONT_SMALL.getWidth(lines[0]);
|
||||
int maxWidth = Fonts.SMALL.getWidth(lines[0]);
|
||||
for (int i = 1; i < lines.length; i++) {
|
||||
int w = Utils.FONT_SMALL.getWidth(lines[i]);
|
||||
int w = Fonts.SMALL.getWidth(lines[i]);
|
||||
if (w > maxWidth)
|
||||
maxWidth = w;
|
||||
}
|
||||
textWidth += maxWidth;
|
||||
textHeight += lineHeight * (lines.length - 1);
|
||||
} else
|
||||
textWidth += Utils.FONT_SMALL.getWidth(tooltip);
|
||||
textWidth += Fonts.SMALL.getWidth(tooltip);
|
||||
|
||||
// get drawing coordinates
|
||||
int x = input.getMouseX() + offset, y = input.getMouseY() + offset;
|
||||
@@ -405,29 +398,29 @@ public class UI {
|
||||
y = margin;
|
||||
|
||||
// draw tooltip text inside a filled rectangle
|
||||
float alpha = (float) tooltipTimer / TOOLTIP_FADE_TIME;
|
||||
float oldAlpha = Utils.COLOR_BLACK_ALPHA.a;
|
||||
Utils.COLOR_BLACK_ALPHA.a = alpha;
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
Utils.COLOR_BLACK_ALPHA.a = oldAlpha;
|
||||
float alpha = tooltipAlpha.getValue();
|
||||
float oldAlpha = Colors.BLACK_ALPHA.a;
|
||||
Colors.BLACK_ALPHA.a = alpha;
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
Colors.BLACK_ALPHA.a = oldAlpha;
|
||||
g.fillRect(x, y, textWidth, textHeight);
|
||||
oldAlpha = Utils.COLOR_DARK_GRAY.a;
|
||||
Utils.COLOR_DARK_GRAY.a = alpha;
|
||||
g.setColor(Utils.COLOR_DARK_GRAY);
|
||||
oldAlpha = Colors.DARK_GRAY.a;
|
||||
Colors.DARK_GRAY.a = alpha;
|
||||
g.setColor(Colors.DARK_GRAY);
|
||||
g.setLineWidth(1);
|
||||
g.drawRect(x, y, textWidth, textHeight);
|
||||
Utils.COLOR_DARK_GRAY.a = oldAlpha;
|
||||
oldAlpha = Utils.COLOR_WHITE_ALPHA.a;
|
||||
Utils.COLOR_WHITE_ALPHA.a = alpha;
|
||||
Utils.FONT_SMALL.drawString(x + textMarginX, y, tooltip, Utils.COLOR_WHITE_ALPHA);
|
||||
Utils.COLOR_WHITE_ALPHA.a = oldAlpha;
|
||||
Colors.DARK_GRAY.a = oldAlpha;
|
||||
oldAlpha = Colors.WHITE_ALPHA.a;
|
||||
Colors.WHITE_ALPHA.a = alpha;
|
||||
Fonts.SMALL.drawString(x + textMarginX, y, tooltip, Colors.WHITE_ALPHA);
|
||||
Colors.WHITE_ALPHA.a = oldAlpha;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the tooltip.
|
||||
*/
|
||||
public static void resetTooltip() {
|
||||
tooltipTimer = -1;
|
||||
tooltipAlpha.setTime(0);
|
||||
tooltip = null;
|
||||
}
|
||||
|
||||
@@ -475,18 +468,18 @@ public class UI {
|
||||
if (barNotifTimer >= BAR_NOTIFICATION_TIME * 0.9f)
|
||||
alpha -= 1 - ((BAR_NOTIFICATION_TIME - barNotifTimer) / (BAR_NOTIFICATION_TIME * 0.1f));
|
||||
int midX = container.getWidth() / 2, midY = container.getHeight() / 2;
|
||||
float barHeight = Utils.FONT_LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
|
||||
float oldAlphaB = Utils.COLOR_BLACK_ALPHA.a, oldAlphaW = Utils.COLOR_WHITE_ALPHA.a;
|
||||
Utils.COLOR_BLACK_ALPHA.a *= alpha;
|
||||
Utils.COLOR_WHITE_ALPHA.a = alpha;
|
||||
g.setColor(Utils.COLOR_BLACK_ALPHA);
|
||||
float barHeight = Fonts.LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
|
||||
float oldAlphaB = Colors.BLACK_ALPHA.a, oldAlphaW = Colors.WHITE_ALPHA.a;
|
||||
Colors.BLACK_ALPHA.a *= alpha;
|
||||
Colors.WHITE_ALPHA.a = alpha;
|
||||
g.setColor(Colors.BLACK_ALPHA);
|
||||
g.fillRect(0, midY - barHeight / 2f, container.getWidth(), barHeight);
|
||||
Utils.FONT_LARGE.drawString(
|
||||
midX - Utils.FONT_LARGE.getWidth(barNotif) / 2f,
|
||||
midY - Utils.FONT_LARGE.getLineHeight() / 2.2f,
|
||||
barNotif, Utils.COLOR_WHITE_ALPHA);
|
||||
Utils.COLOR_BLACK_ALPHA.a = oldAlphaB;
|
||||
Utils.COLOR_WHITE_ALPHA.a = oldAlphaW;
|
||||
Fonts.LARGE.drawString(
|
||||
midX - Fonts.LARGE.getWidth(barNotif) / 2f,
|
||||
midY - Fonts.LARGE.getLineHeight() / 2.2f,
|
||||
barNotif, Colors.WHITE_ALPHA);
|
||||
Colors.BLACK_ALPHA.a = oldAlphaB;
|
||||
Colors.WHITE_ALPHA.a = oldAlphaW;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
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.renderer.Renderer;
|
||||
import org.newdawn.slick.opengl.renderer.SGL;
|
||||
import org.newdawn.slick.util.FastTrig;
|
||||
import org.newdawn.slick.util.Log;
|
||||
|
||||
/**
|
||||
@@ -104,7 +105,7 @@ public class Image implements Renderable {
|
||||
/** The colours for each of the corners */
|
||||
protected Color[] corners;
|
||||
/** The OpenGL max filter */
|
||||
private int filter = SGL.GL_LINEAR;
|
||||
private int filter = FILTER_LINEAR;
|
||||
|
||||
/** True if the image should be flipped vertically */
|
||||
private boolean flipped;
|
||||
@@ -562,7 +563,7 @@ public class Image implements Renderable {
|
||||
* @param y The y coordinate to place the image's center at
|
||||
*/
|
||||
public void drawCentered(float x, float y) {
|
||||
draw(x-(getWidth()/2),y-(getHeight()/2));
|
||||
draw(x - (getWidth() / 2f), y - (getHeight() / 2f));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -595,11 +596,22 @@ public class Image implements Renderable {
|
||||
* @param y The y location to draw the image at
|
||||
* @param filter The color to filter with when drawing
|
||||
*/
|
||||
@Override
|
||||
public void draw(float x, float y, Color filter) {
|
||||
init();
|
||||
draw(x,y,width,height, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw this image as part of a collection of images
|
||||
*
|
||||
* @param x The x location to draw the image at
|
||||
* @param y The y location to draw the image at
|
||||
*/
|
||||
public void drawEmbedded(float x,float y) {
|
||||
drawEmbedded(x, y, getWidth(), getHeight());
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw this image as part of a collection of images
|
||||
*
|
||||
@@ -719,6 +731,7 @@ public class Image implements Renderable {
|
||||
* @param height
|
||||
* The height to render the image at
|
||||
*/
|
||||
@Override
|
||||
public void draw(float x,float y,float width,float height) {
|
||||
init();
|
||||
draw(x,y,width,height,Color.white);
|
||||
@@ -797,6 +810,7 @@ public class Image implements Renderable {
|
||||
* @param height The height to render the image at
|
||||
* @param filter The color to filter with while drawing
|
||||
*/
|
||||
@Override
|
||||
public void draw(float x,float y,float width,float height,Color filter) {
|
||||
if (alpha != 1) {
|
||||
if (filter == null) {
|
||||
@@ -1159,6 +1173,83 @@ public class Image implements Renderable {
|
||||
newTextureOffsetY);
|
||||
GL.glVertex3f((x + mywidth),y, 0.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlike the other drawEmbedded methods, this allows for the embedded image
|
||||
* to be rotated. This is done by applying a rotation transform to each
|
||||
* vertex of the image. This ignores getRotation but depends on the
|
||||
* center x/y (scaled accordingly to the new width/height).
|
||||
*
|
||||
* @param x the x to render the image at
|
||||
* @param y the y to render the image at
|
||||
* @param width the new width to render the image
|
||||
* @param height the new height to render the image
|
||||
* @param rotation the rotation to render the image, using getCenterOfRotationX/Y
|
||||
*
|
||||
* @author davedes
|
||||
*/
|
||||
public void drawEmbedded(float x, float y, float width, float height, float rotation) {
|
||||
if (rotation==0) {
|
||||
drawEmbedded(x, y, width, height);
|
||||
return;
|
||||
}
|
||||
init();
|
||||
float scaleX = width/this.width;
|
||||
float scaleY = height/this.height;
|
||||
|
||||
float cx = getCenterOfRotationX()*scaleX;
|
||||
float cy = getCenterOfRotationY()*scaleY;
|
||||
|
||||
float p1x = -cx;
|
||||
float p1y = -cy;
|
||||
float p2x = width - cx;
|
||||
float p2y = -cy;
|
||||
float p3x = width - cx;
|
||||
float p3y = height - cy;
|
||||
float p4x = -cx;
|
||||
float p4y = height - cy;
|
||||
|
||||
double rad = Math.toRadians(rotation);
|
||||
final float cos = (float) FastTrig.cos(rad);
|
||||
final float sin = (float) FastTrig.sin(rad);
|
||||
|
||||
float tx = getTextureOffsetX();
|
||||
float ty = getTextureOffsetY();
|
||||
float tw = getTextureWidth();
|
||||
float th = getTextureHeight();
|
||||
|
||||
float x1 = (cos * p1x - sin * p1y) + cx; // TOP LEFT
|
||||
float y1 = (sin * p1x + cos * p1y) + cy;
|
||||
float x2 = (cos * p4x - sin * p4y) + cx; // BOTTOM LEFT
|
||||
float y2 = (sin * p4x + cos * p4y) + cy;
|
||||
float x3 = (cos * p3x - sin * p3y) + cx; // BOTTOM RIGHT
|
||||
float y3 = (sin * p3x + cos * p3y) + cy;
|
||||
float x4 = (cos * p2x - sin * p2y) + cx; // TOP RIGHT
|
||||
float y4 = (sin * p2x + cos * p2y) + cy;
|
||||
if (corners == null) {
|
||||
GL.glTexCoord2f(tx, ty);
|
||||
GL.glVertex3f(x+x1, y+y1, 0);
|
||||
GL.glTexCoord2f(tx, ty + th);
|
||||
GL.glVertex3f(x+x2, y+y2, 0);
|
||||
GL.glTexCoord2f(tx + tw, ty + th);
|
||||
GL.glVertex3f(x+x3, y+y3, 0);
|
||||
GL.glTexCoord2f(tx + tw, ty);
|
||||
GL.glVertex3f(x+x4, y+y4, 0);
|
||||
} else {
|
||||
corners[TOP_LEFT].bind();
|
||||
GL.glTexCoord2f(tx, ty);
|
||||
GL.glVertex3f(x+x1, y+y1, 0);
|
||||
corners[BOTTOM_LEFT].bind();
|
||||
GL.glTexCoord2f(tx, ty + th);
|
||||
GL.glVertex3f(x+x2, y+y2, 0);
|
||||
corners[BOTTOM_RIGHT].bind();
|
||||
GL.glTexCoord2f(tx + tw, ty + th);
|
||||
GL.glVertex3f(x+x3, y+y3, 0);
|
||||
corners[TOP_RIGHT].bind();
|
||||
GL.glTexCoord2f(tx + tw, ty);
|
||||
GL.glVertex3f(x+x4, y+y4, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the image in a warper rectangle. The effects this can
|
||||
@@ -1457,7 +1548,7 @@ public class Image implements Renderable {
|
||||
if (isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
flushPixelData();
|
||||
destroyed = true;
|
||||
texture.release();
|
||||
GraphicsFactory.releaseGraphicsForImage(this);
|
||||
|
||||
@@ -1078,10 +1078,10 @@ public class Input {
|
||||
throw new SlickException("Unable to create controller - no jinput found - add jinput.jar to your classpath");
|
||||
}
|
||||
throw new SlickException("Unable to create controllers");
|
||||
} catch (NoClassDefFoundError e) {
|
||||
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
||||
// forget it, no jinput availble
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notification from an event handle that an event has been consumed
|
||||
@@ -1233,7 +1233,7 @@ public class Input {
|
||||
}
|
||||
|
||||
while (Mouse.next()) {
|
||||
if (Mouse.getEventButton() >= 0) {
|
||||
if (Mouse.getEventButton() >= 0 && Mouse.getEventButton() < mousePressed.length) {
|
||||
if (Mouse.getEventButtonState()) {
|
||||
consumed = false;
|
||||
mousePressed[Mouse.getEventButton()] = true;
|
||||
|
||||
@@ -76,8 +76,9 @@ public class Mp3InputStream extends InputStream implements AudioInputStream {
|
||||
/**
|
||||
* Create a new stream to decode MP3 data.
|
||||
* @param input the input stream from which to read the MP3 file
|
||||
* @throws IOException failure to read the header from the input stream
|
||||
*/
|
||||
public Mp3InputStream(InputStream input) {
|
||||
public Mp3InputStream(InputStream input) throws IOException {
|
||||
decoder = new Decoder();
|
||||
bitstream = new Bitstream(input);
|
||||
try {
|
||||
@@ -85,6 +86,10 @@ public class Mp3InputStream extends InputStream implements AudioInputStream {
|
||||
} catch (BitstreamException e) {
|
||||
Log.error(e);
|
||||
}
|
||||
if (header == null) {
|
||||
close();
|
||||
throw new IOException("Failed to read header from MP3 input stream.");
|
||||
}
|
||||
|
||||
channels = (header.mode() == Header.SINGLE_CHANNEL) ? 1 : 2;
|
||||
sampleRate = header.frequency();
|
||||
|
||||