package me.despawningbone.discordbot.command.games; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.io.BufferedReader; | |
import java.io.IOException; | |
//edited by me for compatibility / bug fixes | |
//TODO clean up koohii to fit my own code style? rn it feels really awkward to use ngl | |
/** | |
* pure java implementation of github.com/Francesco149/oppai-ng . | |
* | |
* <blockquote><pre> | |
* this is meant to be a single file library that's as portable and | |
* easy to set up as possible for java projects that need | |
* pp/difficulty calculation. | |
* | |
* when running the test suite, speed is roughly equivalent to the C | |
* implementation, but peak memory usage is almost 80 times higher. | |
* if you are on a system with limited resources or you don't want | |
* to spend time installing and setting up java, you can use the C | |
* implementation which doesn't depend on any third party software. | |
* ----------------------------------------------------------------- | |
* usage: | |
* put Koohii.java in your project's folder | |
* ----------------------------------------------------------------- | |
* import java.io.BufferedReader; | |
* import java.io.InputStreamReader; | |
* | |
* class Example { | |
* | |
* public static void main(String[] args) throws java.io.IOException | |
* { | |
* BufferedReader stdin = | |
* new BufferedReader(new InputStreamReader(System.in) | |
* ); | |
* | |
* Koohii.Map beatmap = new Koohii.Parser().map(stdin); | |
* Koohii.DiffCalc stars = new Koohii.DiffCalc().calc(beatmap); | |
* System.out.printf("%s stars\n", stars.total); | |
* | |
* Koohii.PPv2 pp = Koohii.PPv2( | |
* stars.aim, stars.speed, beatmap | |
* ); | |
* | |
* System.out.printf("%s pp\n", pp.total); | |
* } | |
* | |
* } | |
* ----------------------------------------------------------------- | |
* javac Example.java | |
* cat /path/to/file.osu | java Example | |
* ----------------------------------------------------------------- | |
* this is free and unencumbered software released into the | |
* public domain. | |
* | |
* refer to the attached UNLICENSE or http://unlicense.org/ | |
* </pre></blockquote> | |
* | |
* @author Franc[e]sco ([email protected]) | |
*/ | |
public final class Koohii { | |
private Koohii() {} | |
public final int VERSION_MAJOR = 2; | |
public final int VERSION_MINOR = 1; | |
public final int VERSION_PATCH = 0; | |
/** prints a message to stderr. */ | |
public static | |
void info(String fmt, Object... args) { | |
System.err.printf(fmt, args); | |
} | |
/* ------------------------------------------------------------- */ | |
/* math */ | |
/** 2D vector with double values */ | |
public static class Vector2 | |
{ | |
public double x = 0.0, y = 0.0; | |
public Vector2() {} | |
public Vector2(Vector2 other) { this(other.x, other.y); } | |
public Vector2(double x, double y) { this.x = x; this.y = y; } | |
public String toString() { | |
return String.format("(%s, %s)", x, y); | |
} | |
/** | |
* this -= other . | |
* @return this | |
*/ | |
public Vector2 sub(Vector2 other) | |
{ | |
x -= other.x; y -= other.y; | |
return this; | |
} | |
/** | |
* this *= value . | |
* @return this | |
*/ | |
public Vector2 mul(double value) | |
{ | |
x *= value; y *= value; | |
return this; | |
} | |
/** length (magnitude) of the vector. */ | |
public double len() { return Math.sqrt(x * x + y * y); } | |
/** dot product between two vectors, correlates with the angle */ | |
public double dot(Vector2 other) { return x * other.x + y * other.y; } | |
} | |
/* ------------------------------------------------------------- */ | |
/* beatmap utils */ | |
public static final int MODE_STD = 0; | |
public static final int MODE_TK = 1; | |
public static class Circle | |
{ | |
public Vector2 pos = new Vector2(); | |
public String toString() { return pos.toString(); } | |
} | |
public static class Slider | |
{ | |
public Vector2 pos = new Vector2(); | |
/** distance travelled by one repetition. */ | |
public double distance = 0.0; | |
/** 1 = no repeats. */ | |
public int repetitions = 1; | |
public String toString() | |
{ | |
return String.format( | |
"{ pos=%s, distance=%s, repetitions=%d }", | |
pos, distance, repetitions | |
); | |
} | |
} | |
public static final int OBJ_CIRCLE = 1<<0; | |
public static final int OBJ_SLIDER = 1<<1; | |
public static final int OBJ_SPINNER = 1<<3; | |
/** strain index for speed */ | |
public final static int DIFF_SPEED = 0; | |
/** strain index for aim */ | |
public final static int DIFF_AIM = 1; | |
public static class HitObject | |
{ | |
/** start time in milliseconds. */ | |
public double time = 0.0; | |
public int type = OBJ_CIRCLE; | |
/** an instance of Circle or Slider or null. */ | |
public Object data = null; | |
public Vector2 normpos = new Vector2(); | |
public double angle = 0.0; | |
public final double[] strains = new double[] { 0.0, 0.0 }; | |
public boolean is_single = false; | |
public double delta_time = 0.0; | |
public double d_distance = 0.0; | |
/** string representation of the type bitmask. */ | |
public String typestr() | |
{ | |
StringBuilder res = new StringBuilder(); | |
if ((type & OBJ_CIRCLE) != 0) res.append("circle | "); | |
if ((type & OBJ_SLIDER) != 0) res.append("slider | "); | |
if ((type & OBJ_SPINNER) != 0) res.append("spinner | "); | |
String result = res.toString(); | |
return result.substring(0, result.length() - 3); | |
} | |
public String toString() | |
{ | |
return String.format( | |
"{ time=%s, type=%s, data=%s, normpos=%s, " + | |
"strains=[ %s, %s ], is_single=%s }", | |
time, typestr(), data, normpos, strains[0], strains[1], | |
is_single | |
); | |
} | |
} | |
public static class Timing | |
{ | |
/** start time in milliseconds. */ | |
public double time = 0.0; | |
public double ms_per_beat = -100.0; | |
/** if false, ms_per_beat is -100 * bpm_multiplier. */ | |
public boolean change = false; | |
} | |
/** | |
* the bare minimum beatmap data for difficulty calculation. | |
* | |
* this object can be reused for multiple beatmaps without | |
* re-allocation by simply calling reset() | |
*/ | |
public static class Map | |
{ | |
public int format_version; | |
public int mode; | |
public String title, title_unicode; | |
public String artist, artist_unicode; | |
/** mapper name. */ | |
public String creator; | |
/** difficulty name. */ | |
public String version; | |
public int ncircles, nsliders, nspinners; | |
public float hp, cs, od, ar; | |
public float sv, tick_rate; | |
public final ArrayList<HitObject> objects = | |
new ArrayList<HitObject>(512); | |
public final ArrayList<Timing> tpoints = | |
new ArrayList<Timing>(32); | |
public Map() { reset(); } | |
/** clears the instance so that it can be reused. */ | |
public void reset() | |
{ | |
title = title_unicode = | |
artist = artist_unicode = | |
creator = | |
version = ""; | |
ncircles = nsliders = nspinners = 0; | |
hp = cs = od = ar = 5.0f; | |
sv = tick_rate = 1.0f; | |
objects.clear(); tpoints.clear(); | |
} | |
public String toString() | |
{ | |
StringBuilder sb = new StringBuilder(); | |
for (HitObject obj : objects) | |
{ | |
sb.append(obj); | |
sb.append(", "); | |
} | |
String objs_str = sb.toString(); | |
sb.setLength(0); | |
for (Timing t : tpoints) | |
{ | |
sb.append(t); | |
sb.append(", "); | |
} | |
String timing_str = sb.toString(); | |
return String.format( | |
"beatmap { mode=%d, title=%s, title_unicode=%s, " + | |
"artist=%s, artist_unicode=%s, creator=%s, " + | |
"version=%s, ncircles=%d, nsliders=%d, nspinners=%d," + | |
" hp=%s, cs=%s, od=%s, ar=%s, sv=%s, tick_rate=%s, " + | |
"tpoints=[ %s ], objects=[ %s ] }", | |
mode, title, title_unicode, artist, artist_unicode, | |
creator, version, ncircles, nsliders, nspinners, hp, | |
cs, od, ar, sv, tick_rate, timing_str, objs_str | |
); | |
} | |
//CUSTOM: added taiko handling method which is way less complicated than this | |
public int max_combo() | |
{ | |
int res = 0; | |
switch(mode) { | |
case MODE_STD: | |
int tindex = -1; | |
double tnext = Double.NEGATIVE_INFINITY; | |
double px_per_beat = 0.0; | |
for (HitObject obj : objects) | |
{ | |
if ((obj.type & OBJ_SLIDER) == 0) | |
{ | |
/* non-sliders add 1 combo */ | |
++res; | |
continue; | |
} | |
/* keep track of the current timing point without | |
looping through all of them for every object */ | |
while (obj.time >= tnext) | |
{ | |
++tindex; | |
if (tpoints.size() > tindex + 1) { | |
tnext = tpoints.get(tindex + 1).time; | |
} else { | |
tnext = Double.POSITIVE_INFINITY; | |
} | |
Timing t = tpoints.get(tindex); | |
double sv_multiplier = 1.0; | |
if (!t.change && t.ms_per_beat < 0) { | |
sv_multiplier = -100.0 / t.ms_per_beat; | |
} | |
px_per_beat = sv * 100.0 * sv_multiplier; | |
if (format_version < 8) { | |
px_per_beat /= sv_multiplier; | |
} | |
} | |
/* slider, we need to calculate slider ticks */ | |
Slider sl = (Slider)obj.data; | |
double num_beats = | |
(sl.distance * sl.repetitions) / px_per_beat; | |
int ticks = (int) | |
Math.ceil( | |
(num_beats - 0.1) / sl.repetitions * tick_rate | |
); | |
--ticks; | |
ticks *= sl.repetitions; | |
ticks += sl.repetitions + 1; | |
res += Math.max(0, ticks); | |
} | |
break; | |
case MODE_TK: | |
res = ncircles; | |
break; | |
default: //should never throw since this object cant get past init if its unsupported | |
throw new UnsupportedOperationException("unsupported gamemode"); | |
} | |
return res; | |
} | |
} | |
/* ------------------------------------------------------------- */ | |
/* beatmap parser */ | |
/* note: I just let parser throw built-in exceptions instead of | |
error checking stuff because it's as good as making my own | |
exception since you can check lastline/lastpos when you catch */ | |
public static class Parser | |
{ | |
/** last line touched. */ | |
public String lastline; | |
/** last line number touched. */ | |
public int nline; | |
/** last token touched. */ | |
public String lastpos; | |
/** true if the parsing completed successfully. */ | |
public boolean done; | |
/** | |
* the parsed beatmap will be stored in this object. | |
* willl persist throughout reset() calls and will be reused by | |
* subsequent parse calls until changed. | |
* @see Parser#reset | |
*/ | |
public Map beatmap = null; | |
private String section; /* current section */ | |
private boolean ar_found = false; | |
public Parser() { reset(); } | |
private void reset() | |
{ | |
lastline = lastpos = section = ""; | |
nline = 0; | |
done = false; | |
if (beatmap != null) { | |
beatmap.reset(); | |
} | |
} | |
public String toString() | |
{ | |
return String.format( | |
"in line %d\n%s\n> %s", nline, lastline, lastpos | |
); | |
} | |
private void warn(String fmt, Object... args) | |
{ | |
info("W: "); | |
info(fmt, args); | |
info("\n%s\n", this); | |
} | |
/** | |
* trims v, sets lastpos to it and returns trimmed v. | |
* should be used to access any string that can make the parser | |
* fail | |
*/ | |
private String setlastpos(String v) | |
{ | |
v = v.trim(); | |
lastpos = v; | |
return v; | |
} | |
private String[] property() | |
{ | |
String[] split = lastline.split(":", 2); | |
split[0] = setlastpos(split[0]); | |
if (split.length > 1) { | |
split[1] = setlastpos(split[1]); | |
} | |
/* why does java have such inconsistent naming? ArrayList | |
length is .size(), normal array length is .length, string | |
length is .length(). why do I have to look up documentation | |
for stuff that should have the same interface? */ | |
return split; | |
} | |
private void metadata() | |
{ | |
String[] p = property(); | |
if (p[0].equals("Title")) { | |
beatmap.title = p[1]; | |
} | |
else if (p[0].equals("TitleUnicode")) { | |
beatmap.title_unicode = p[1]; | |
} | |
else if (p[0].equals("Artist")) { | |
beatmap.artist = p[1]; | |
} | |
else if (p[0].equals("ArtistUnicode")) { | |
beatmap.artist_unicode = p[1]; | |
} | |
else if (p[0].equals("Creator")) { | |
beatmap.creator = p[1]; | |
} | |
else if (p[0].equals("Version")) { | |
beatmap.version = p[1]; | |
} | |
} | |
private void general() | |
{ | |
String[] p = property(); | |
if (p[0].equals("Mode")) | |
{ | |
beatmap.mode = Integer.parseInt(setlastpos(p[1])); | |
if (beatmap.mode != MODE_STD && beatmap.mode != MODE_TK) | |
{ | |
throw new UnsupportedOperationException( | |
"this gamemode is not yet supported" | |
); | |
} | |
} | |
} | |
private void difficulty() | |
{ | |
String[] p = property(); | |
/* what's up with the redundant Float.parseFloat ?_? */ | |
if (p[0].equals("CircleSize")) { | |
beatmap.cs = Float.parseFloat(setlastpos(p[1])); | |
} | |
else if (p[0].equals("OverallDifficulty")) { | |
beatmap.od = Float.parseFloat(setlastpos(p[1])); | |
} | |
else if (p[0].equals("ApproachRate")) { | |
beatmap.ar = Float.parseFloat(setlastpos(p[1])); | |
ar_found = true; | |
} | |
else if (p[0].equals("HPDrainRate")) { | |
beatmap.hp = Float.parseFloat(setlastpos(p[1])); | |
} | |
else if (p[0].equals("SliderMultiplier")) { | |
beatmap.sv = Float.parseFloat(setlastpos(p[1])); | |
} | |
else if (p[0].equals("SliderTickRate")) { | |
beatmap.tick_rate = Float.parseFloat(setlastpos(p[1])); | |
} | |
} | |
private void timing() | |
{ | |
String[] s = lastline.split(","); | |
if (s.length > 8) { | |
warn("timing point with trailing values"); | |
} | |
Timing t = new Timing(); | |
t.time = Double.parseDouble(setlastpos(s[0])); | |
t.ms_per_beat = Double.parseDouble(setlastpos(s[1])); | |
if (s.length >= 7) { | |
t.change = !s[6].trim().equals("0"); | |
} | |
beatmap.tpoints.add(t); | |
} | |
private void objects() | |
{ | |
String[] s = lastline.split(","); | |
if (s.length > 11) { | |
warn("object with trailing values"); | |
} | |
HitObject obj = new HitObject(); | |
obj.time = Double.parseDouble(setlastpos(s[2])); | |
obj.type = Integer.parseInt(setlastpos(s[3])); | |
if ((obj.type & OBJ_CIRCLE) != 0) | |
{ | |
++beatmap.ncircles; | |
Circle c = new Circle(); | |
c.pos.x = Double.parseDouble(setlastpos(s[0])); | |
c.pos.y = Double.parseDouble(setlastpos(s[1])); | |
obj.data = c; | |
} | |
else if ((obj.type & OBJ_SPINNER) != 0) { | |
++beatmap.nspinners; | |
} | |
else if ((obj.type & OBJ_SLIDER) != 0) | |
{ | |
++beatmap.nsliders; | |
Slider sli = new Slider(); | |
sli.pos.x = Double.parseDouble(setlastpos(s[0])); | |
sli.pos.y = Double.parseDouble(setlastpos(s[1])); | |
sli.repetitions = Integer.parseInt(setlastpos(s[6])); | |
sli.distance = Double.parseDouble(setlastpos(s[7])); | |
obj.data = sli; | |
} | |
beatmap.objects.add(obj); | |
} | |
/** | |
* calls reset() on beatmap and parses a osu file into it. | |
* if beatmap is null, it will be initialized to a new Map | |
* @return this.beatmap | |
* @throws IOException | |
*/ | |
public Map map(BufferedReader reader) throws IOException | |
{ | |
String line = null; | |
if (beatmap == null) { | |
beatmap = new Map(); | |
} | |
reset(); | |
while ((line = reader.readLine()) != null) | |
{ | |
lastline = line; | |
++nline; | |
/* comments (according to lazer) */ | |
if (line.startsWith(" ") || line.startsWith("_")) { | |
continue; | |
} | |
line = lastline = line.trim(); | |
if (line.length() <= 0) { | |
continue; | |
} | |
/* c++ style comments */ | |
if (line.startsWith("//")) { | |
continue; | |
} | |
/* [SectionName] */ | |
if (line.startsWith("[")) { | |
section = line.substring(1, line.length() - 1); | |
continue; | |
} | |
try | |
{ | |
if (section.equals("Metadata")) | |
metadata(); | |
else if (section.equals("General")) | |
general(); | |
else if (section.equals("Difficulty")) | |
difficulty(); | |
else if (section.equals("TimingPoints")) | |
timing(); | |
else if (section.equals("HitObjects")) | |
objects(); | |
else { | |
int fmt_index = line.indexOf("file format v"); | |
if (fmt_index < 0) { | |
continue; | |
} | |
beatmap.format_version = Integer.parseInt( | |
line.substring(fmt_index + 13) | |
); | |
} | |
} | |
catch (NumberFormatException e) { | |
warn("ignoring line with bad number"); | |
} catch (ArrayIndexOutOfBoundsException e) { | |
warn("ignoring malformed line"); | |
} | |
} | |
if (!ar_found) { | |
beatmap.ar = beatmap.od; | |
} | |
done = true; | |
return beatmap; | |
} | |
/** | |
* sets beatmap and returns map(reader) | |
* @return this.beatmap | |
* @throws IOException | |
*/ | |
public Map map(BufferedReader reader, Map beatmap) | |
throws IOException | |
{ | |
this.beatmap = beatmap; | |
return map(reader); | |
} | |
} | |
/* ------------------------------------------------------------- */ | |
/* mods utils */ | |
public static final int MODS_NOMOD = 0; | |
public static final int MODS_NF = 1<<0; | |
public static final int MODS_EZ = 1<<1; | |
public static final int MODS_TOUCH_DEVICE = 1<<2; | |
public static final int MODS_TD = MODS_TOUCH_DEVICE; | |
public static final int MODS_HD = 1<<3; | |
public static final int MODS_HR = 1<<4; | |
public static final int MODS_DT = 1<<6; | |
public static final int MODS_HT = 1<<8; | |
public static final int MODS_NC = 1<<9; | |
public static final int MODS_FL = 1<<10; | |
public static final int MODS_SO = 1<<12; | |
public static final int MODS_SPEED_CHANGING = | |
MODS_DT | MODS_HT | MODS_NC; | |
public static final int MODS_MAP_CHANGING = | |
MODS_HR | MODS_EZ | MODS_SPEED_CHANGING; | |
/** @return a string representation of the mods, such as HDDT */ | |
public static | |
String mods_str(int mods) | |
{ | |
StringBuilder sb = new StringBuilder(); | |
if ((mods & MODS_NF) != 0) { | |
sb.append("NF"); | |
} | |
if ((mods & MODS_EZ) != 0) { | |
sb.append("EZ"); | |
} | |
if ((mods & MODS_TOUCH_DEVICE) != 0) { | |
sb.append("TD"); | |
} | |
if ((mods & MODS_HD) != 0) { | |
sb.append("HD"); | |
} | |
if ((mods & MODS_HR) != 0) { | |
sb.append("HR"); | |
} | |
if ((mods & MODS_NC) != 0) { | |
sb.append("NC"); | |
} else if ((mods & MODS_DT) != 0) { | |
sb.append("DT"); | |
} | |
if ((mods & MODS_HT) != 0) { | |
sb.append("HT"); | |
} | |
if ((mods & MODS_FL) != 0) { | |
sb.append("FL"); | |
} | |
if ((mods & MODS_SO) != 0) { | |
sb.append("SO"); | |
} | |
return sb.toString(); | |
} | |
/** @return mod bitmask from the string representation */ | |
public static | |
int mods_from_str(String str) | |
{ | |
int mask = 0; | |
while (str.length() > 0) | |
{ | |
if (str.startsWith("NF")) mask |= MODS_NF; | |
else if (str.startsWith("EZ")) mask |= MODS_EZ; | |
else if (str.startsWith("TD")) mask |= MODS_TOUCH_DEVICE; | |
else if (str.startsWith("HD")) mask |= MODS_HD; | |
else if (str.startsWith("HR")) mask |= MODS_HR; | |
else if (str.startsWith("DT")) mask |= MODS_DT; | |
else if (str.startsWith("HT")) mask |= MODS_HT; | |
else if (str.startsWith("NC")) mask |= MODS_NC; | |
else if (str.startsWith("FL")) mask |= MODS_FL; | |
else if (str.startsWith("SO")) mask |= MODS_SO; | |
else { | |
str = str.substring(1); | |
continue; | |
} | |
str = str.substring(2); | |
} | |
return mask; | |
} | |
/** | |
* beatmap stats with mods applied. | |
* should be populated with the base beatmap stats and passed to | |
* mods_apply which will modify the stats for the given mods | |
*/ | |
public static class MapStats | |
{ | |
float ar, od, cs, hp; | |
public MapStats() { | |
} | |
public MapStats(Map map) { | |
ar = map.ar; | |
od = map.od; | |
cs = map.cs; | |
hp = map.hp; | |
} | |
/** | |
* speed multiplier / music rate. | |
* this doesn't need to be initialized before calling mods_apply | |
*/ | |
float speed = 1.0f; | |
} | |
private static final double OD0_MS = 80; | |
private static final double OD10_MS = 20; | |
private static final double AR0_MS = 1800.0; | |
private static final double AR5_MS = 1200.0; | |
private static final double AR10_MS = 450.0; | |
private static final double OD_MS_STEP = (OD0_MS - OD10_MS) / 10.0; | |
private static final double AR_MS_STEP1 = (AR0_MS - AR5_MS) / 5.0; | |
private static final double AR_MS_STEP2 = (AR5_MS - AR10_MS) / 5.0; | |
private static final int APPLY_AR = 1<<0; | |
private static final int APPLY_OD = 1<<1; | |
private static final int APPLY_CS = 1<<2; | |
private static final int APPLY_HP = 1<<3; | |
/** | |
* applies mods to mapstats. | |
* | |
* <blockquote><pre> | |
* Koohii.MapStats mapstats = new Koohii.MapStats(); | |
* mapstats.ar = 9; | |
* Koohii.mods_apply(Koohii.MODS_DT, mapstats, Koohii.APPLY_AR); | |
* // mapstats.ar is now 10.33, mapstats.speed is 1.5 | |
* </pre></blockquote> | |
* | |
* @param mapstats the base beatmap stats | |
* @param flags bitmask that specifies which stats to modify. only | |
* the stats specified here need to be initialized in | |
* mapstats. | |
* @return mapstats | |
* @see MapStats | |
*/ | |
public static | |
MapStats mods_apply(int mods, MapStats mapstats, int flags) | |
{ | |
mapstats.speed = 1.0f; | |
if ((mods & MODS_MAP_CHANGING) == 0) { | |
return mapstats; | |
} | |
if ((mods & (MODS_DT | MODS_NC)) != 0) { | |
mapstats.speed = 1.5f; | |
} | |
if ((mods & MODS_HT) != 0) { | |
mapstats.speed *= 0.75f; | |
} | |
float od_ar_hp_multiplier = 1.0f; | |
if ((mods & MODS_HR) != 0) { | |
od_ar_hp_multiplier = 1.4f; | |
} | |
if ((mods & MODS_EZ) != 0) { | |
od_ar_hp_multiplier *= 0.5f; | |
} | |
if ((flags & APPLY_AR) != 0) | |
{ | |
mapstats.ar *= od_ar_hp_multiplier; | |
/* convert AR into milliseconds window */ | |
double arms = mapstats.ar < 5.0f ? | |
AR0_MS - AR_MS_STEP1 * mapstats.ar | |
: AR5_MS - AR_MS_STEP2 * (mapstats.ar - 5.0f); | |
/* stats must be capped to 0-10 before HT/DT which brings | |
them to a range of -4.42->11.08 for OD and -5->11 for AR */ | |
arms = Math.min(AR0_MS, Math.max(AR10_MS, arms)); | |
arms /= mapstats.speed; | |
mapstats.ar = (float)( | |
arms > AR5_MS ? | |
(AR0_MS - arms) / AR_MS_STEP1 | |
: 5.0 + (AR5_MS - arms) / AR_MS_STEP2 | |
); | |
} | |
if ((flags & APPLY_OD) != 0) | |
{ | |
mapstats.od *= od_ar_hp_multiplier; | |
double odms = OD0_MS - Math.ceil(OD_MS_STEP * mapstats.od); | |
odms = Math.min(OD0_MS, Math.max(OD10_MS, odms)); | |
odms /= mapstats.speed; | |
mapstats.od = (float)((OD0_MS - odms) / OD_MS_STEP); | |
} | |
if ((flags & APPLY_CS) != 0) | |
{ | |
if ((mods & MODS_HR) != 0) { | |
mapstats.cs *= 1.3f; | |
} | |
if ((mods & MODS_EZ) != 0) { | |
mapstats.cs *= 0.5f; | |
} | |
mapstats.cs = Math.min(10.0f, mapstats.cs); | |
} | |
if ((flags & APPLY_HP) != 0) | |
{ | |
mapstats.hp = | |
Math.min(10.0f, mapstats.hp * od_ar_hp_multiplier); | |
} | |
return mapstats; | |
} | |
/* ------------------------------------------------------------- */ | |
/* difficulty calculator */ | |
/** | |
* arbitrary thresholds to determine when a stream is spaced | |
* enough that it becomes hard to alternate. | |
*/ | |
private final static double SINGLE_SPACING = 125.0; | |
/** strain decay per interval. */ | |
private final static double[] DECAY_BASE = { 0.3, 0.15 }; | |
/** balances speed and aim. */ | |
private final static double[] WEIGHT_SCALING = { 1400.0, 26.25 }; | |
/** | |
* max strains are weighted from highest to lowest, this is how | |
* much the weight decays. | |
*/ | |
private final static double DECAY_WEIGHT = 0.9; | |
/** | |
* strains are calculated by analyzing the map in chunks and taking | |
* the peak strains in each chunk. this is the length of a strain | |
* interval in milliseconds | |
*/ | |
private final static double STRAIN_STEP = 400.0; | |
/** non-normalized diameter where the small circle buff starts. */ | |
private final static double CIRCLESIZE_BUFF_THRESHOLD = 30.0; | |
/** global stars multiplier. */ | |
private final static double STAR_SCALING_FACTOR = 0.0675; | |
/** in osu! pixels */ | |
private final static double PLAYFIELD_WIDTH = 512.0, | |
PLAYFIELD_HEIGHT = 384.0; | |
private final static Vector2 PLAYFIELD_CENTER = new Vector2( | |
PLAYFIELD_WIDTH / 2.0, PLAYFIELD_HEIGHT / 2.0 | |
); | |
/** | |
* 50% of the difference between aim and speed is added to total | |
* star rating to compensate for aim/speed only maps | |
*/ | |
private final static double EXTREME_SCALING_FACTOR = 0.5; | |
private final static double MIN_SPEED_BONUS = 75.0; | |
private final static double MAX_SPEED_BONUS = 45.0; | |
private final static double ANGLE_BONUS_SCALE = 90.0; | |
private final static double AIM_TIMING_THRESHOLD = 107; | |
private final static double SPEED_ANGLE_BONUS_BEGIN = 5 * Math.PI / 6; | |
private final static double AIM_ANGLE_BONUS_BEGIN = Math.PI / 3; | |
private static | |
double d_spacing_weight(int type, double distance, double delta_time, | |
double prev_distance, double prev_delta_time, double angle) | |
{ | |
double strain_time = Math.max(delta_time, 50.0); | |
double prev_strain_time = Math.max(prev_delta_time, 50.0); | |
double angle_bonus; | |
switch (type) | |
{ | |
case DIFF_AIM: { | |
double result = 0.0; | |
if (!Double.isNaN(angle) && angle > AIM_ANGLE_BONUS_BEGIN) { | |
angle_bonus = Math.sqrt( | |
Math.max(prev_distance - ANGLE_BONUS_SCALE, 0.0) * | |
Math.pow(Math.sin(angle - AIM_ANGLE_BONUS_BEGIN), 2.0) * | |
Math.max(distance - ANGLE_BONUS_SCALE, 0.0) | |
); | |
result = ( | |
1.5 * Math.pow(Math.max(0.0, angle_bonus), 0.99) / | |
Math.max(AIM_TIMING_THRESHOLD, prev_strain_time) | |
); | |
} | |
double weighted_distance = Math.pow(distance, 0.99); | |
return Math.max(result + | |
weighted_distance / | |
Math.max(AIM_TIMING_THRESHOLD, strain_time), | |
weighted_distance / strain_time); | |
} | |
case DIFF_SPEED: { | |
distance = Math.min(distance, SINGLE_SPACING); | |
delta_time = Math.max(delta_time, MAX_SPEED_BONUS); | |
double speed_bonus = 1.0; | |
if (delta_time < MIN_SPEED_BONUS) { | |
speed_bonus += | |
Math.pow((MIN_SPEED_BONUS - delta_time) / 40.0, 2); | |
} | |
angle_bonus = 1.0; | |
if (!Double.isNaN(angle) && angle < SPEED_ANGLE_BONUS_BEGIN) { | |
double s = Math.sin(1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)); | |
angle_bonus += Math.pow(s, 2) / 3.57; | |
if (angle < Math.PI / 2.0) { | |
angle_bonus = 1.28; | |
if (distance < ANGLE_BONUS_SCALE && angle < Math.PI / 4.0) { | |
angle_bonus += (1.0 - angle_bonus) * | |
Math.min((ANGLE_BONUS_SCALE - distance) / 10.0, 1.0); | |
} else if (distance < ANGLE_BONUS_SCALE) { | |
angle_bonus += (1.0 - angle_bonus) * | |
Math.min((ANGLE_BONUS_SCALE - distance) / 10.0, 1.0) * | |
Math.sin((Math.PI / 2.0 - angle) * 4.0 / Math.PI); | |
} | |
} | |
} | |
return ( | |
(1 + (speed_bonus - 1) * 0.75) * angle_bonus * | |
(0.95 + speed_bonus * Math.pow(distance / SINGLE_SPACING, 3.5)) | |
) / strain_time; | |
} | |
} | |
throw new UnsupportedOperationException( | |
"this difficulty type does not exist" | |
); | |
} | |
/** | |
* calculates the strain for one difficulty type and stores it in | |
* obj. this assumes that normpos is already computed. | |
* this also sets is_single if type is DIFF_SPEED | |
*/ | |
private static | |
void d_strain(int type, HitObject obj, HitObject prev, | |
double speed_mul) | |
{ | |
double value = 0.0; | |
double time_elapsed = (obj.time - prev.time) / speed_mul; | |
double decay = | |
Math.pow(DECAY_BASE[type], time_elapsed / 1000.0); | |
obj.delta_time = time_elapsed; | |
/* this implementation doesn't account for sliders */ | |
if ((obj.type & (OBJ_SLIDER | OBJ_CIRCLE)) != 0) | |
{ | |
double distance = | |
new Vector2(obj.normpos).sub(prev.normpos).len(); | |
obj.d_distance = distance; | |
if (type == DIFF_SPEED) { | |
obj.is_single = distance > SINGLE_SPACING; | |
} | |
value = d_spacing_weight(type, distance, time_elapsed, | |
prev.d_distance, prev.delta_time, obj.angle); | |
value *= WEIGHT_SCALING[type]; | |
} | |
obj.strains[type] = prev.strains[type] * decay + value; | |
} | |
/** | |
* difficulty calculator, can be reused in subsequent calc() calls. | |
*/ | |
public static class DiffCalc | |
{ | |
/** star rating. */ | |
public double total; | |
/** aim stars. */ | |
public double aim; | |
/** aim difficulty (used to calc length bonus) */ | |
public double aim_difficulty; | |
/** aim length bonus (unused at the moment) */ | |
public double aim_length_bonus; | |
/** speed stars. */ | |
public double speed; | |
/** speed difficulty (used to calc length bonus) */ | |
public double speed_difficulty; | |
/** speed length bonus (unused at the moment) */ | |
public double speed_length_bonus; | |
/** | |
* number of notes that are considered singletaps by the | |
* difficulty calculator. | |
*/ | |
public int nsingles; | |
/** | |
* number of taps slower or equal to the singletap threshold | |
* value. | |
*/ | |
public int nsingles_threshold; | |
/** | |
* the beatmap we want to calculate the difficulty for. | |
* must be set or passed to calc() explicitly. | |
* persists across calc() calls unless it's changed or explicity | |
* passed to calc() | |
* @see DiffCalc#calc(Koohii.Map, int, double) | |
* @see DiffCalc#calc(Koohii.Map, int) | |
* @see DiffCalc#calc(Koohii.Map) | |
*/ | |
public Map beatmap = null; | |
private double speed_mul; | |
private final ArrayList<Double> strains = | |
new ArrayList<Double>(512); | |
public DiffCalc() { reset(); } | |
/** sets up the instance for re-use by resetting fields. */ | |
private void reset() | |
{ | |
total = aim = speed = 0.0; | |
nsingles = nsingles_threshold = 0; | |
speed_mul = 1.0; | |
} | |
public String toString() | |
{ | |
return String.format("%s stars (%s aim, %s speed)", | |
total, aim, speed); | |
} | |
private static double length_bonus(double stars, double difficulty) { | |
return ( | |
0.32 + 0.5 * | |
(Math.log10(difficulty + stars) - Math.log10(stars)) | |
); | |
} | |
private class DiffValues | |
{ | |
public double difficulty, total; | |
public DiffValues(double difficulty, double total) { | |
this.difficulty = difficulty; | |
this.total = total; | |
} | |
}; | |
private DiffValues calc_individual(int type) | |
{ | |
strains.clear(); | |
double strain_step = STRAIN_STEP * speed_mul; | |
/* the first object doesn't generate a strain | |
* so we begin with an incremented interval end */ | |
double interval_end = ( | |
Math.ceil(beatmap.objects.get(0).time / strain_step) | |
* strain_step | |
); | |
double max_strain = 0.0; | |
/* calculate all strains */ | |
for (int i = 0; i < beatmap.objects.size(); ++i) | |
{ | |
HitObject obj = beatmap.objects.get(i); | |
HitObject prev = i > 0 ? | |
beatmap.objects.get(i - 1) : null; | |
if (prev != null) { | |
d_strain(type, obj, prev, speed_mul); | |
} | |
while (obj.time > interval_end) | |
{ | |
/* add max strain for this interval */ | |
strains.add(max_strain); | |
if (prev != null) | |
{ | |
/* decay last object's strains until the next | |
interval and use that as the initial max | |
strain */ | |
double decay = Math.pow(DECAY_BASE[type], | |
(interval_end - prev.time) / 1000.0); | |
max_strain = prev.strains[type] * decay; | |
} else { | |
max_strain = 0.0; | |
} | |
interval_end += strain_step; | |
} | |
max_strain = Math.max(max_strain, obj.strains[type]); | |
} | |
/* don't forget to add the last strain interval */ | |
strains.add(max_strain); | |
/* weigh the top strains sorted from highest to lowest */ | |
double weight = 1.0; | |
double total = 0.0; | |
double difficulty = 0.0; | |
Collections.sort(strains, Collections.reverseOrder()); | |
for (Double strain : strains) | |
{ | |
total += Math.pow(strain, 1.2); | |
difficulty += strain * weight; | |
weight *= DECAY_WEIGHT; | |
} | |
return new DiffValues(difficulty, total); | |
} | |
/** | |
* default value for singletap_threshold. | |
* @see DiffCalc#calc | |
*/ | |
public final static double DEFAULT_SINGLETAP_THRESHOLD = 125.0; | |
/** | |
* calculates beatmap difficulty and stores it in total, aim, | |
* speed, nsingles, nsingles_speed fields. | |
* @param singletap_threshold the smallest milliseconds interval | |
* that will be considered singletappable. for example, | |
* 125ms is 240 1/2 singletaps ((60000 / 240) / 2) | |
* @return self | |
*/ | |
public DiffCalc calc(int mods, double singletap_threshold) | |
{ | |
reset(); | |
MapStats mapstats = new MapStats(); | |
mapstats.cs = beatmap.cs; | |
mods_apply(mods, mapstats, APPLY_CS); | |
speed_mul = mapstats.speed; | |
double radius = (PLAYFIELD_WIDTH / 16.0) * | |
(1.0 - 0.7 * (mapstats.cs - 5.0) / 5.0); | |
/* positions are normalized on circle radius so that we can | |
calc as if everything was the same circlesize */ | |
double scaling_factor = 52.0 / radius; | |
if (radius < CIRCLESIZE_BUFF_THRESHOLD) | |
{ | |
scaling_factor *= 1.0 + | |
Math.min(CIRCLESIZE_BUFF_THRESHOLD - radius, 5.0) / 50.0; | |
} | |
Vector2 normalized_center = | |
new Vector2(PLAYFIELD_CENTER).mul(scaling_factor); | |
HitObject prev1 = null; | |
HitObject prev2 = null; | |
int i = 0; | |
/* calculate normalized positions */ | |
for (HitObject obj : beatmap.objects) | |
{ | |
if ((obj.type & OBJ_SPINNER) != 0) { | |
obj.normpos = new Vector2(normalized_center); | |
} | |
else | |
{ | |
Vector2 pos; | |
if ((obj.type & OBJ_SLIDER) != 0) { | |
pos = ((Slider)obj.data).pos; | |
} | |
else if ((obj.type & OBJ_CIRCLE) != 0) { | |
pos = ((Circle)obj.data).pos; | |
} | |
else | |
{ | |
info( | |
"W: unknown object type %08X\n", | |
obj.type | |
); | |
pos = new Vector2(); | |
} | |
obj.normpos = new Vector2(pos).mul(scaling_factor); | |
} | |
if (i >= 2) { | |
Vector2 v1 = new Vector2(prev2.normpos).sub(prev1.normpos); | |
Vector2 v2 = new Vector2(obj.normpos).sub(prev1.normpos); | |
double dot = v1.dot(v2); | |
double det = v1.x * v2.y - v1.y * v2.x; | |
obj.angle = Math.abs(Math.atan2(det, dot)); | |
} else { | |
obj.angle = Double.NaN; | |
} | |
prev2 = prev1; | |
prev1 = obj; | |
++i; | |
} | |
/* speed and aim stars */ | |
DiffValues aimvals = calc_individual(DIFF_AIM); | |
aim = aimvals.difficulty; | |
aim_difficulty = aimvals.total; | |
aim_length_bonus = length_bonus(aim, aim_difficulty); | |
DiffValues speedvals = calc_individual(DIFF_SPEED); | |
speed = speedvals.difficulty; | |
speed_difficulty = speedvals.total; | |
speed_length_bonus = length_bonus(speed, speed_difficulty); | |
aim = Math.sqrt(aim) * STAR_SCALING_FACTOR; | |
speed = Math.sqrt(speed) * STAR_SCALING_FACTOR; | |
if ((mods & MODS_TOUCH_DEVICE) != 0) { | |
aim = Math.pow(aim, 0.8); | |
} | |
/* total stars */ | |
total = aim + speed + | |
Math.abs(speed - aim) * EXTREME_SCALING_FACTOR; | |
/* singletap stats */ | |
for (i = 1; i < beatmap.objects.size(); ++i) | |
{ | |
HitObject prev = beatmap.objects.get(i - 1); | |
HitObject obj = beatmap.objects.get(i); | |
if (obj.is_single) { | |
++nsingles; | |
} | |
if ((obj.type & (OBJ_CIRCLE | OBJ_SLIDER)) == 0) { | |
continue; | |
} | |
double interval = (obj.time - prev.time) / speed_mul; | |
if (interval >= singletap_threshold) { | |
++nsingles_threshold; | |
} | |
} | |
return this; | |
} | |
/** | |
* @return calc(mods, DEFAULT_SINGLETAP_THRESHOLD) | |
* @see DiffCalc#calc(int, double) | |
* @see DiffCalc#DEFAULT_SINGLETAP_THRESHOLD | |
*/ | |
public DiffCalc calc(int mods) { | |
return calc(mods, DEFAULT_SINGLETAP_THRESHOLD); | |
} | |
/** | |
* @return calc(MODS_NOMOD, DEFAULT_SINGLETAP_THRESHOLD) | |
* @see DiffCalc#calc(int, double) | |
* @see DiffCalc#DEFAULT_SINGLETAP_THRESHOLD | |
*/ | |
public DiffCalc calc() { | |
return calc(MODS_NOMOD, DEFAULT_SINGLETAP_THRESHOLD); | |
} | |
/** | |
* sets beatmap field and calls | |
* calc(mods, singletap_threshold). | |
* @see DiffCalc#calc(int, double) | |
*/ | |
public DiffCalc calc(Map beatmap, int mods, | |
double singletap_threshold) | |
{ | |
this.beatmap = beatmap; | |
return calc(mods, singletap_threshold); | |
} | |
/** | |
* sets beatmap field and calls | |
* calc(mods, DEFAULT_SINGLETAP_THRESHOLD). | |
* @see DiffCalc#calc(int, double) | |
* @see DiffCalc#DEFAULT_SINGLETAP_THRESHOLD | |
*/ | |
public DiffCalc calc(Map beatmap, int mods) { | |
return calc(beatmap, mods, DEFAULT_SINGLETAP_THRESHOLD); | |
} | |
/** | |
* sets beatmap field and calls | |
* calc(MODS_NOMOD, DEFAULT_SINGLETAP_THRESHOLD). | |
* @see DiffCalc#calc(int, double) | |
* @see DiffCalc#DEFAULT_SINGLETAP_THRESHOLD | |
*/ | |
public DiffCalc calc(Map beatmap) | |
{ | |
return calc(beatmap, MODS_NOMOD, | |
DEFAULT_SINGLETAP_THRESHOLD); | |
} | |
} | |
/* ------------------------------------------------------------- */ | |
/* acc calc */ | |
public static class Accuracy | |
{ | |
public int n300 = 0, n100 = 0, n50 = 0, nmisses = 0; | |
public Accuracy() {} | |
/** | |
* @param n300 the number of 300s, if -1 it will be calculated | |
* from the object count in Accuracy#value(int). | |
*/ | |
public Accuracy(int n300, int n100, int n50, int nmisses) | |
{ | |
this.n300 = n300; | |
this.n100 = n100; | |
this.n50 = n50; | |
this.nmisses = nmisses; | |
} | |
/** | |
* calls Accuracy(-1, n100, n50, nmisses) . | |
* @see Accuracy#Accuracy(int, int, int, int) | |
*/ | |
public Accuracy(int n100, int n50, int nmisses) { | |
this(-1, n100, n50, nmisses); | |
} | |
/** | |
* calls Accuracy(-1, n100, n50, 0) . | |
* @see Accuracy#Accuracy(int, int, int, int) | |
*/ | |
public Accuracy(int n100, int n50) { | |
this(-1, n100, n50, 0); | |
} | |
/** | |
* calls Accuracy(-1, n100, 0, 0) . | |
* @see Accuracy#Accuracy(int, int, int, int) | |
*/ | |
public Accuracy(int n100) { | |
this(-1, n100, 0, 0); | |
} | |
/** | |
* rounds to the closest amount of 300s, 100s, 50s for a given | |
* accuracy percentage. | |
* @param nobjects the total number of hits (n300 + n100 + n50 + | |
* nmisses) | |
*/ | |
public Accuracy(double acc_percent, int nmisses, Map beatmap) | |
{ | |
int nobjects = beatmap.objects.size(); | |
nmisses = Math.min(nobjects, nmisses); | |
int max300 = nobjects - nmisses; | |
double maxacc = | |
new Accuracy(max300, 0, 0, nmisses).value(beatmap.mode) * 100.0; | |
acc_percent = Math.max(0.0, Math.min(maxacc, acc_percent)); | |
/* | |
* CUSTOM: new logic below for computing n100 and n50 since vanilla koohii is kinda inaccurate | |
* also changed params for cross mode acc derivation | |
*/ | |
switch(beatmap.mode) { | |
case 0: //osu | |
n100 = (int) Math.min(max300, //guard against invalid n100s | |
Math.ceil( //instead of round to get highest n100 | |
-3.0 * | |
((acc_percent * 0.01 - 1.0) * nobjects + nmisses) * | |
0.5 | |
)); | |
//solve x (y = (x*100 + (n - x - m)*300)/(n*300)) where y = acc, n = nobjects, x = n100, m = nmisses | |
//then use n50 to fill in the gaps; need round | |
n50 = (int) Math.round(-2/5.0 * (3 * nmisses + 3 * nobjects * (acc_percent * 0.01 - 1) + 2 * n100)); | |
//solve z (y = (z * 50 + x*100 + (n - z - x - m)*300)/(n*300)) where everything is same as above and z = n50 | |
//use lowest discrepancy with n100-- until no more or until < epsilon = 0.002% | |
double lowestDiscrepancy = 1; | |
int n50_ = n50, n100_ = n100; | |
while((lowestDiscrepancy > 0.00002 || (n100_ + n50_) > max300) && n100_ > 0) { | |
//get current discrepancy | |
double discrepancy = Math.abs(((acc_percent/100.0) - (n50_*50.0 + n100_*100.0 + (nobjects - n100_ - n50_ - nmisses)*300.0)/(nobjects*300.0))); | |
//only update if better and valid | |
if(discrepancy < lowestDiscrepancy && (n100_ + n50_) <= max300 && n50_ >= 0) { | |
lowestDiscrepancy = discrepancy; | |
n50 = n50_; n100 = n100_; | |
} | |
//tick n100 down by 1 and calc new n50 | |
n50_ = (int) Math.round(-2/5.0 * (3 * nmisses + 3 * nobjects * (acc_percent * 0.01 - 1) + 2 * --n100_)); | |
} | |
n300 = nobjects - n100 - n50 - nmisses; | |
break; | |
case 1: //taiko | |
n50 = 0; //taiko has no n50 | |
n100 = (int) Math.min(max300, //guard just like above | |
Math.round((1 - ((double) nmisses)/nobjects - acc_percent/100) * 2 * nobjects)); | |
n300 = beatmap.ncircles - n100 - nmisses; //taiko only cares about circles | |
break; | |
default: | |
throw new IllegalArgumentException("This gamemode is not yet supported."); | |
} | |
if(n300 < 0) | |
throw new IllegalArgumentException("Impossible accuracy inputted. Did you forget to input misses?"); | |
} | |
/** | |
* @param nobjects the total number of hits (n300 + n100 + n50 + | |
* nmiss). if -1, n300 must have been set and | |
* will be used to deduce this value. | |
* @return the accuracy value (0.0-1.0) | |
*/ | |
public double value(int nobjects, int mode) | |
{ | |
if (nobjects < 0 && n300 < 0) | |
{ | |
throw new IllegalArgumentException( | |
"either nobjects or n300 must be specified" | |
); | |
} | |
int n300_ = n300 >= 0 ? n300 : //CUSTOM: the guard is <, yet this checks > which makes no sense; changed to >= | |
nobjects - n100 - n50 - nmisses; | |
if (nobjects < 0) { | |
nobjects = n300_ + n100 + n50 + nmisses; | |
} | |
/* | |
* CUSTOM: new logic below for cross mode acc derivation with mode param | |
*/ | |
double res; | |
switch(mode) { | |
case 0: //osu | |
res = (n50 * 50.0 + n100 * 100.0 + n300_ * 300.0) / (nobjects * 300.0); | |
break; | |
case 1: //taiko | |
res = (n100 * 0.5 + n300) / (double) (nmisses + n100 + n300_); | |
break; | |
default: | |
throw new IllegalArgumentException("This gamemode is not yet supported."); | |
} | |
return Math.max(0, Math.min(res, 1.0)); | |
} | |
/** | |
* calls value(-1) . | |
* @see Accuracy#value(int) | |
*/ | |
public double value(int mode) { | |
return value(-1, mode); | |
} | |
} | |
/* ------------------------------------------------------------- */ | |
/* pp calc */ | |
/* base pp value for stars, used internally by ppv2 */ | |
private static | |
double pp_base(double stars) | |
{ | |
return Math.pow(5.0 * Math.max(1.0, stars / 0.0675) - 4.0, 3.0) | |
/ 100000.0; | |
} | |
/** | |
* parameters to be passed to PPv2. | |
* aim_stars, speed_stars, max_combo, nsliders, ncircles, nobjects, | |
* base_ar, base_od are required. | |
* @see PPv2#PPv2(Koohii.PlayParameters) | |
* | |
* CUSTOM: no longer just used by PPv2, renamed to playparameters instead - it records the play info for pp calculation | |
*/ | |
public static class PlayParameters | |
{ | |
/** | |
* if not null, max_combo, nsliders, ncircles, nobjects, | |
* base_ar, base_od will be obtained from this beatmap. | |
*/ | |
public Map beatmap = null; | |
public double aim_stars = 0.0; | |
public double speed_stars = 0.0; | |
public int max_combo = 0; | |
public int nsliders = 0, ncircles = 0, nobjects = 0; | |
/** the base AR (before applying mods). */ | |
public float base_ar = 5.0f; | |
/** the base OD (before applying mods). */ | |
public float base_od = 5.0f; | |
/** gamemode. */ | |
public int mode = MODE_STD; | |
/** the mods bitmask, same as osu! api, see MODS_* constants */ | |
public int mods = MODS_NOMOD; | |
/** | |
* the maximum combo achieved, if -1 it will default to | |
* max_combo - nmiss . | |
*/ | |
public int combo = -1; | |
/** | |
* number of 300s, if -1 it will default to | |
* nobjects - n100 - n50 - nmiss . | |
*/ | |
public int n300 = -1; | |
public int n100 = 0, n50 = 0, nmiss = 0; | |
/** scorev1 (1) or scorev2 (2). */ | |
public int score_version = 1; | |
} | |
public static class PPv2 | |
{ | |
public double total, aim, speed, acc; | |
public Accuracy computed_accuracy; | |
/** | |
* calculates ppv2, results are stored in total, aim, speed, | |
* acc, acc_percent. | |
* @see PlayParameters | |
*/ | |
private PPv2(double aim_stars, double speed_stars, | |
int max_combo, int nsliders, int ncircles, int nobjects, | |
float base_ar, float base_od, int mode, int mods, | |
int combo, int n300, int n100, int n50, int nmiss, | |
int score_version, Map beatmap) | |
{ | |
if (beatmap != null) | |
{ | |
mode = beatmap.mode; | |
base_ar = beatmap.ar; | |
base_od = beatmap.od; | |
max_combo = beatmap.max_combo(); | |
nsliders = beatmap.nsliders; | |
ncircles = beatmap.ncircles; | |
nobjects = beatmap.objects.size(); | |
} | |
if (mode != MODE_STD) | |
{ | |
throw new UnsupportedOperationException( | |
"this gamemode is not yet supported" | |
); | |
} | |
if (max_combo <= 0) | |
{ | |
info("W: max_combo <= 0, changing to 1\n"); | |
max_combo = 1; | |
} | |
if (combo < 0) { | |
combo = max_combo - nmiss; | |
} | |
if (n300 < 0) { | |
n300 = nobjects - n100 - n50 - nmiss; | |
} | |
/* accuracy -------------------------------------------- */ | |
computed_accuracy = new Accuracy(n300, n100, n50, nmiss); | |
double accuracy = computed_accuracy.value(0); | |
double real_acc = accuracy; | |
/* scorev1 ignores sliders since they are free 300s | |
and for some reason also ignores spinners */ | |
int nspinners = nobjects - nsliders - ncircles; | |
switch (score_version) | |
{ | |
case 1: | |
real_acc = new Accuracy(Math.max(0, n300 - nsliders - nspinners), //CUSTOM: guard against going below zero | |
n100, n50, nmiss).value(0); | |
real_acc = Math.max(0.0, real_acc); | |
break; | |
case 2: | |
ncircles = nobjects; | |
break; | |
default: | |
throw new UnsupportedOperationException( | |
String.format("unsupported scorev%d", | |
score_version) | |
); | |
} | |
/* global values --------------------------------------- */ | |
double nobjects_over_2k = nobjects / 2000.0; | |
double length_bonus = 0.95 + 0.4 * | |
Math.min(1.0, nobjects_over_2k); | |
if (nobjects > 2000) { | |
length_bonus += Math.log10(nobjects_over_2k) * 0.5; | |
} | |
double miss_penality_aim = | |
0.97 * Math.pow(1 - Math.pow((double)nmiss / nobjects, 0.775), nmiss); | |
double miss_penality_speed = | |
0.97 * Math.pow(1 - Math.pow((double)nmiss / nobjects, 0.775), Math.pow(nmiss, 0.875)); | |
double combo_break = Math.pow(combo, 0.8) / | |
Math.pow(max_combo, 0.8); | |
/* calculate stats with mods */ | |
MapStats mapstats = new MapStats(); | |
mapstats.ar = base_ar; | |
mapstats.od = base_od; | |
mods_apply(mods, mapstats, APPLY_AR | APPLY_OD); | |
/* ar bonus -------------------------------------------- */ | |
double ar_bonus = 0.0; | |
if (mapstats.ar > 10.33) { | |
ar_bonus += 0.4 * (mapstats.ar - 10.33); | |
} | |
else if (mapstats.ar < 8.0) { | |
ar_bonus += 0.01 * (8.0 - mapstats.ar); | |
} | |
/* aim pp ---------------------------------------------- */ | |
aim = pp_base(aim_stars); | |
aim *= length_bonus; | |
if (nmiss > 0) { | |
aim *= miss_penality_aim; | |
} | |
aim *= combo_break; | |
aim *= 1.0 + Math.min(ar_bonus, ar_bonus * (nobjects / 1000.0)); | |
double hd_bonus = 1.0; | |
if ((mods & MODS_HD) != 0) { | |
hd_bonus *= 1.0 + 0.04 * (12.0 - mapstats.ar); | |
} | |
aim *= hd_bonus; | |
if ((mods & MODS_FL) != 0) { | |
double fl_bonus = 1.0 + 0.35 * Math.min(1.0, nobjects / 200.0); | |
if (nobjects > 200) { | |
fl_bonus += 0.3 * Math.min(1.0, (nobjects - 200) / 300.0); | |
} | |
if (nobjects > 500) { | |
fl_bonus += (nobjects - 500) / 1200.0; | |
} | |
aim *= fl_bonus; | |
} | |
double acc_bonus = 0.5 + accuracy / 2.0; | |
double od_squared = mapstats.od * mapstats.od; | |
double od_bonus = 0.98 + od_squared / 2500.0; | |
aim *= acc_bonus; | |
aim *= od_bonus; | |
/* speed pp -------------------------------------------- */ | |
speed = pp_base(speed_stars); | |
speed *= length_bonus; | |
if (nmiss > 0) { | |
speed *= miss_penality_speed; | |
} | |
speed *= combo_break; | |
if (mapstats.ar > 10.33) { | |
speed *= 1.0 + Math.min(ar_bonus, ar_bonus * (nobjects / 1000.0)); | |
} | |
speed *= hd_bonus; | |
/* similar to aim acc and od bonus */ | |
speed *= (0.95 + od_squared / 750.0) * | |
Math.pow(accuracy, (14.5 - Math.max(mapstats.od, 8)) / 2); | |
speed *= Math.pow(0.98, n50 < nobjects / 500.0 ? 0.00 : n50 - nobjects / 500.0); | |
/* acc pp ---------------------------------------------- */ | |
acc = Math.pow(1.52163, mapstats.od) * | |
Math.pow(real_acc, 24.0) * 2.83; | |
acc *= Math.min(1.15, Math.pow(ncircles / 1000.0, 0.3)); | |
if ((mods & MODS_HD) != 0) { | |
acc *= 1.08; | |
} | |
if ((mods & MODS_FL) != 0) { | |
acc *= 1.02; | |
} | |
/* total pp -------------------------------------------- */ | |
double final_multiplier = 1.12; | |
if ((mods & MODS_NF) != 0) { | |
final_multiplier *= Math.max(0.9, 1.0 - 0.2 * nmiss); | |
} | |
if ((mods & MODS_SO) != 0) { | |
final_multiplier *= 1.0 - Math.pow((double)nspinners / nobjects, 0.85); | |
} | |
total = Math.pow( | |
Math.pow(aim, 1.1) + Math.pow(speed, 1.1) + | |
Math.pow(acc, 1.1), | |
1.0 / 1.1 | |
) * final_multiplier; | |
} | |
/** @see PlayParameters */ | |
public PPv2(PlayParameters p) | |
{ | |
this(p.aim_stars, p.speed_stars, p.max_combo, p.nsliders, | |
p.ncircles, p.nobjects, p.base_ar, p.base_od, p.mode, | |
p.mods, p.combo, p.n300, p.n100, p.n50, p.nmiss, | |
p.score_version, p.beatmap); | |
} | |
/** | |
* simplest possible call, calculates ppv2 for SS scorev1. | |
* @see PPv2#PPv2(Koohii.PlayParameters) | |
*/ | |
public PPv2(double aim_stars, double speed_stars, Map b) | |
{ | |
this(aim_stars, speed_stars, -1, b.nsliders, b.ncircles, | |
b.objects.size(), b.ar, b.od, b.mode, MODS_NOMOD, -1, | |
-1, 0, 0, 0, 1, b); | |
} | |
} | |
/** | |
* CUSTOM: Taiko support added by me | |
* Heavily modified from https://mon.im/taikopp/ to fit Koohii | |
*/ | |
public static class TaikoPP { | |
public Map map; | |
public double stars; | |
public int totalHits; | |
public int misses; | |
public int combo; | |
public int mods; | |
public double acc; | |
public double computedAcc; | |
public double rawod; | |
public double strain; | |
public double pp; | |
//technically taiko dont have aim stars and the only other star val is speed stars, so imma just borrow that variable | |
public TaikoPP(PlayParameters p) { | |
this(p.speed_stars, p.combo, p.mods, new Accuracy(p.n300, p.n100, p.n50, p.nmiss).value(1), p.nmiss, p.beatmap); | |
} | |
public TaikoPP(double stars, int combo, int mods, double acc, int misses, Map beatmap) { | |
this.map = beatmap; | |
this.stars = stars; | |
this.totalHits = beatmap.ncircles; | |
this.misses = misses; | |
this.combo = combo; | |
this.mods = mods; | |
this.acc = acc; | |
this.rawod = beatmap.od; | |
if(this.acc > 1) { | |
this.acc /= 100; | |
} | |
this.strain = computeStrain(this.combo, this.misses, this.acc); | |
this.computedAcc = computeAcc(this.acc); | |
this.pp = computeTotal(this.strain, this.computedAcc); | |
} | |
private double computeTotal(double strain, double acc) { | |
double multiplier = 1.1; | |
if ((mods & MODS_NF) != 0) multiplier *= 0.9; | |
if ((mods & MODS_HD) != 0) multiplier *= 1.1; | |
return Math.pow(Math.pow(strain, 1.1) + Math.pow(acc, 1.1), 1.0 / 1.1) * multiplier; | |
} | |
private double computeStrain(int combo, int misses, double acc) { | |
double strainValue = Math.pow(5 * Math.max(1, this.stars / 0.0075) - 4, 2) / 100000; | |
double lengthBonus = 1 + 0.1 * Math.min(1, (this.totalHits / 1500.0)); | |
strainValue *= lengthBonus * Math.pow(0.985, misses); | |
//if(combo > 0) strainValue *= Math.min(Math.pow(this.totalHits, 0.5) / Math.pow(combo, 0.5), 1); | |
if ((mods & MODS_HD) != 0) strainValue *= 1.025; | |
if ((mods & MODS_FL) != 0) strainValue *= (1.05 * lengthBonus); | |
strainValue *= acc; | |
return strainValue; | |
} | |
private double computeAcc(double acc) { | |
double hitWindow300 = hitWindow(); | |
if(hitWindow300 <= 0) return -1; | |
return Math.pow(150 / hitWindow300, 1.1) * Math.pow(acc, 15) * 22 * Math.min(Math.pow(this.totalHits / 1500.0, 0.3), 1.15); | |
} | |
private double hitWindow() { | |
double od = scaleOD(); | |
double max = 20, min = 50; | |
double result = Math.floor(min + (max - min) * od / 10) - 0.5; | |
if ((mods & MODS_HT) != 0) result /= 0.75; | |
if ((mods & (MODS_DT | MODS_NC)) != 0) result /= 1.5; | |
return Math.round(result * 100) / 100.0; | |
} | |
private double scaleOD() { | |
double od = this.rawod; | |
if ((mods & MODS_EZ) != 0) od /= 2; | |
if ((mods & MODS_HR) != 0) od *= 1.4; | |
od = Math.max(Math.min(od, 10), 0); | |
return od; | |
} | |
//just realized how weird this method was implemented with how it modifies stats similar to pass by ref and still returns that object | |
//but hey imma stick with it for consistency's sake | |
public MapStats taiko_mods_apply(int mods, MapStats stats) { | |
stats.od = (float) scaleOD(); | |
mods_apply(mods, stats, APPLY_HP); //hp scaling works the same in taiko, just reuse | |
return stats; | |
} | |
} | |
} /* public final class Koohii */ |
File Metadata
File Metadata
- Mime Type
- text/x-c
- Expires
- Sun, Jul 6, 3:45 AM (21 h, 20 m)
- Storage Engine
- local-disk
- Storage Format
- Raw Data
- Storage Handle
- 7a/96/d62203d4c9231b09d4f0253d1294