Page MenuHomedesp's stash

Koohii.java
No OneTemporary

Koohii.java

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

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

Event Timeline