diff --git a/src/me/despawningbone/discordbot/command/games/Koohii.java b/src/me/despawningbone/discordbot/command/games/Koohii.java
index 78acb94..b320d72 100644
--- a/src/me/despawningbone/discordbot/command/games/Koohii.java
+++ b/src/me/despawningbone/discordbot/command/games/Koohii.java
@@ -1,1874 +1,1934 @@
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 .
*
*
* 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/
*
*
* @author Franc[e]sco (lolisamurai@tfwno.gf)
*/
public final class Koohii {
private Koohii() {}
-public final int VERSION_MAJOR = 1;
-public final int VERSION_MINOR = 2;
+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 objects =
new ArrayList(512);
public final ArrayList tpoints =
new ArrayList(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;
- int tindex = -1;
- double tnext = Double.NEGATIVE_INFINITY;
- double px_per_beat = 0.0;
-
- for (HitObject obj : objects)
- {
- if ((obj.type & OBJ_SLIDER) == 0)
+ 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)
{
- /* non-sliders add 1 combo */
- ++res;
- continue;
- }
+ 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;
+ /* 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;
- }
+ if (tpoints.size() > tindex + 1) {
+ tnext = tpoints.get(tindex + 1).time;
+ } else {
+ tnext = Double.POSITIVE_INFINITY;
+ }
- Timing t = tpoints.get(tindex);
+ Timing t = tpoints.get(tindex);
- double sv_multiplier = 1.0;
+ double sv_multiplier = 1.0;
- if (!t.change && t.ms_per_beat < 0) {
- sv_multiplier = -100.0 / t.ms_per_beat;
- }
+ 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;
+ 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;
+ /* slider, we need to calculate slider ticks */
+ Slider sl = (Slider)obj.data;
- double num_beats =
- (sl.distance * sl.repetitions) / px_per_beat;
+ double num_beats =
+ (sl.distance * sl.repetitions) / px_per_beat;
- int ticks = (int)
- Math.ceil(
- (num_beats - 0.1) / sl.repetitions * tick_rate
- );
+ int ticks = (int)
+ Math.ceil(
+ (num_beats - 0.1) / sl.repetitions * tick_rate
+ );
- --ticks;
- ticks *= sl.repetitions;
- ticks += sl.repetitions + 1;
+ --ticks;
+ ticks *= sl.repetitions;
+ ticks += sl.repetitions + 1;
- res += Math.max(0, ticks);
- }
+ 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"
- );
+ 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_SD = 1<<5;
-public static final int MODS_PF = 1<<14;
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");
}
-
- if ((mods & MODS_PF) != 0) {
- sb.append("PF");
- } else if ((mods & MODS_SD) != 0) {
- sb.append("SD");
- }
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.
*
*
* 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
*
*
* @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);
}
- } 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 strains =
new ArrayList(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;
- double interval_end = strain_step;
+ /* 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 nobjects, int 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() * 100.0;
+ new Accuracy(max300, 0, 0, nmisses).value(beatmap.mode) * 100.0;
acc_percent = Math.max(0.0, Math.min(maxacc, acc_percent));
- /* just some black magic maths from wolfram alpha */
- n100 = (int)
- Math.round(
- -3.0 *
- ((acc_percent * 0.01 - 1.0) * nobjects + nmisses) *
- 0.5
- );
-
- if (n100 > max300)
- {
- /* acc lower than all 100s, use 50s */
- n100 = 0;
-
- n50 = (int)
- Math.round(
- -6.0 *
- ((acc_percent * 0.01 - 1.0) * nobjects +
- nmisses) * 0.5
- );
-
- n50 = Math.min(max300, n50);
+ /*
+ * 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.");
}
-
- n300 = nobjects - n100 - n50 - nmisses;
+
+ 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)
+ 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 :
+ 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;
}
- double res = (n50 * 50.0 + n100 * 100.0 + n300_ * 300.0) /
- (nobjects * 300.0);
+ /*
+ * 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() {
- return value(-1);
+ 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.PPv2Parameters)
+* @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 PPv2Parameters
+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;
- public double real_acc;
/**
* calculates ppv2, results are stored in total, aim, speed,
* acc, acc_percent.
- * @see PPv2Parameters
+ * @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;
- if(nobjects == 0) {
- nobjects = beatmap.objects.size();
- }
+ 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(nobjects);
- real_acc = accuracy;
+ 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:
- /* scorev1 ignores sliders since they are free 300s
- and for some reason also ignores spinners */
- int nspinners = nobjects - nsliders - ncircles;
-
- try {
- real_acc = new Accuracy(n300 - nsliders - nspinners,
- n100, n50, nmiss).value();
- } catch (IllegalArgumentException e) {
- info(" Invalid values, using computed accuracy...\n");
- };
+ 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 = Math.pow(0.97, nmiss);
+ 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 = 1.0;
+ double ar_bonus = 0.0;
if (mapstats.ar > 10.33) {
- ar_bonus += 0.3 * (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;
- aim *= miss_penality;
+ if (nmiss > 0) {
+ aim *= miss_penality_aim;
+ }
aim *= combo_break;
- aim *= ar_bonus;
+ 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;
- speed *= miss_penality;
+ if (nmiss > 0) {
+ speed *= miss_penality_speed;
+ }
speed *= combo_break;
if (mapstats.ar > 10.33) {
- speed *= ar_bonus;
+ speed *= 1.0 + Math.min(ar_bonus, ar_bonus * (nobjects / 1000.0));
}
speed *= hd_bonus;
/* similar to aim acc and od bonus */
- speed *= 0.02 + accuracy;
- speed *= 0.96 + od_squared / 1600.0;
+ 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 *= 0.90;
+ final_multiplier *= Math.max(0.9, 1.0 - 0.2 * nmiss);
}
if ((mods & MODS_SO) != 0) {
- final_multiplier *= 0.95;
+ 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 PPv2Parameters */
- public PPv2(PPv2Parameters p)
+ /** @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.PPv2Parameters)
+ * @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;
- public TaikoPP(double stars, double acc, PPv2Parameters p) {
- this(stars, p.beatmap.ncircles + p.beatmap.nsliders, p.mods, acc, p.nmiss, p.beatmap);
- if(p.combo == p.max_combo) p.combo = this.totalHits;
- p.nobjects = p.max_combo = this.totalHits;
- p.n50 = 0;
- p.n100 = (int) Math.round((1 - ((double) misses/p.nobjects) - acc/100) * 2 * p.nobjects);
- if(p.n100 < 0) {
- p.n100 = 0;
- this.acc = (p.nobjects - p.nmiss) / (double) (p.nobjects);
- }
- p.n300 = p.nobjects - p.n100 - p.nmiss;
- }
-
- public TaikoPP(double stars, PPv2Parameters p) { //DO NOT USE THIS WITH DIFFCALC
- this(stars, p.beatmap.ncircles + p.beatmap.nsliders, p.mods, (p.n100 * 0.5 + p.n300) / (double) (p.nmiss + p.n100 + p.n300), p.nmiss, p.beatmap);
+ //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 + beatmap.nsliders;
+ 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(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) != 0) result /= 1.5;
+ 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 */
diff --git a/src/me/despawningbone/discordbot/command/games/Osu.java b/src/me/despawningbone/discordbot/command/games/Osu.java
index ee9fc26..a640424 100644
--- a/src/me/despawningbone/discordbot/command/games/Osu.java
+++ b/src/me/despawningbone/discordbot/command/games/Osu.java
@@ -1,1174 +1,1159 @@
package me.despawningbone.discordbot.command.games;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
-import java.util.HashMap;
import java.util.List;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.knowm.xchart.BitmapEncoder;
import org.knowm.xchart.XYChart;
import org.knowm.xchart.XYChartBuilder;
import org.knowm.xchart.BitmapEncoder.BitmapFormat;
import org.knowm.xchart.XYSeries.XYSeriesRenderStyle;
import org.knowm.xchart.style.Styler.LegendPosition;
import me.despawningbone.discordbot.DiscordBot;
import me.despawningbone.discordbot.command.Command;
import me.despawningbone.discordbot.command.CommandResult;
import me.despawningbone.discordbot.command.CommandResult.CommandResultType;
import me.despawningbone.discordbot.command.games.Koohii.Accuracy;
import me.despawningbone.discordbot.command.games.Koohii.DiffCalc;
import me.despawningbone.discordbot.command.games.Koohii.Map;
import me.despawningbone.discordbot.command.games.Koohii.PPv2;
-import me.despawningbone.discordbot.command.games.Koohii.PPv2Parameters;
+import me.despawningbone.discordbot.command.games.Koohii.PlayParameters;
import me.despawningbone.discordbot.command.games.Koohii.MapStats;
import me.despawningbone.discordbot.command.games.Koohii.TaikoPP;
import me.despawningbone.discordbot.utils.MiscUtils;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.User;
public class Osu extends Command {
- private final static String osuAPI = DiscordBot.tokens.getProperty("osu");
+ private static final String osuAPI = DiscordBot.tokens.getProperty("osu");
- private static DecimalFormat df = new DecimalFormat("#.##");
+ private static final DecimalFormat df = new DecimalFormat("#.##");
- private final static HashMap modes = new HashMap<>();
- static {
- modes.put(0, "osu!");
- modes.put(1, "osu!taiko");
- modes.put(2, "osu!catch");
- modes.put(3, "osu!mania");
- }
+ private static final String[] modes = new String[] {"osu!", "osu!taiko", "osu!catch", "osu!mania"};
public Osu() { //TODO add back the todos to respective sub command, automate desc for subcmds, add back typing; CLOSE ALL STREAMS
this.desc = "All the info you need with osu!";
this.usage = "";
//TODO add a command to parse replays?
registerSubCommand("pp", Arrays.asList("map"), (channel, user, msg, words) -> {
- //OffsetDateTime timesent = OffsetDateTime.now();
List amend = new ArrayList(Arrays.asList(words));
- int temp = amend.indexOf("-w");
+ int wParamIndex = amend.indexOf("-w");
+
+ //parse weight param and uid; doesnt work if -w is immediately followed by pp params like 100x etc
String uid = null;
- boolean weight = temp != -1;
+ boolean weight = wParamIndex != -1;
if(weight) {
try {
- uid = amend.get(temp + 1);
- if(!uid.contains("osu.ppy.sh/u") && (uid.startsWith("http") && uid.contains("://"))) {
+ int wParamLength = 1;
+
+ uid = amend.get(wParamIndex + 1);
+ if(!uid.contains("osu.ppy.sh/u") && (uid.startsWith("http") && uid.contains("://"))) { //url thats not user url means its most likely a beatmap, aka no username param
uid = getPlayer(user);
+ } else {
+ wParamLength = 2; //has username
}
- amend.subList(temp, temp + 1).clear();
+ amend.subList(wParamIndex, wParamIndex + wParamLength).clear(); //remove
} catch (IndexOutOfBoundsException e) {
uid = getPlayer(user);
}
}
+
String initmap;
try {
initmap = amend.get(0);
} catch (IndexOutOfBoundsException e) {
- initmap = "null";
+ initmap = "null"; //dud
}
- List mid = new ArrayList();
- if (!initmap.startsWith("https://") && !initmap.startsWith("http://")) { //check if no map input, use discord rich presence
+
+ CompletableFuture msgId = new CompletableFuture<>();
+
+ //check if no map input, use discord rich presence
+ if (!initmap.startsWith("https://") && !initmap.startsWith("http://")) {
+
+ //get map name from status
String details = null;
try (Connection con = DiscordBot.db.getConnection()) {
ResultSet rs = con.createStatement().executeQuery("SELECT game FROM users WHERE id = " + user.getId() + ";");
if(rs.next()) {
details = rs.getString(1).substring(rs.getString(1).indexOf("||"));
}
rs.close();
} catch (SQLException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
- channel.sendMessage("Trying to retrieve map from discord status...").queue(m -> mid.add(m.getId())); //DONE custom status masks presence now, will not be as effective; any way to get manually? //JDA 4 API change fixed this
+
+ channel.sendMessage("Trying to retrieve map from discord status...").queue(m -> msgId.complete(m.getId())); //DONE custom status masks presence now, will not be as effective; any way to get manually? //JDA 4 API change fixed this
channel.sendTyping().queue();
- /*OffsetDateTime timeReceived = OffsetDateTime.now();
- long ms = Math.abs(timesent.until(timeReceived, ChronoUnit.MILLIS));
- System.out.println("Time taken: " + ms + "ms");*/
- if (details != null) { //TODO if name is sth like `Feryquitous - (S).0ngs//---::compilation.[TQR-f3] [-[//mission:#FC.0011-excindell.defer.abferibus]-]` it breaks (but reasonable break tbh)
- //if(game.getName().equals("osu!") && game.isRich()) {
+
+ //parse map name and search
+ if (details != null) { //TODO if name is sth like `Feryquitous - (S).0ngs//---::compilation.[TQR-f3] [-[//mission:#FC.0011-excindell.defer.abferibus]-]` it breaks (but reasonable break tbh); also breaks if difficulty has " in it
try {
String title = URLEncoder.encode(details.substring(details.indexOf(" - ") + 3, details.lastIndexOf("[")).trim(), "UTF-8");
String diff = URLEncoder.encode(details.substring(details.lastIndexOf("[") + 1, details.lastIndexOf("]")).trim(), "UTF-8");
String url = "https://osusearch.com/query/?title=" + title + "&diff_name=" + diff + "&query_order=play_count&offset=0";
URLConnection stream = new URL(url).openConnection();
stream.addRequestProperty("User-Agent", "Mozilla/4.0");
JSONTokener tokener = new JSONTokener(stream.getInputStream());
initmap = "https://osu.ppy.sh/beatmaps/" + new JSONObject(tokener).getJSONArray("beatmaps").getJSONObject(0).getInt("beatmap_id");
} catch (IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
} catch (JSONException e) {
e.printStackTrace();
System.out.println(details);
return new CommandResult(CommandResultType.NORESULT); //returns if osusearch isnt updated fast enough
}
-
- /*SELENIUM DEPRECATED*/
-
- /*String url = "http://osusearch.com/search/?title=" + title + "&diff_name=" + diff
- + "&query_order=play_count";
- System.out.println(url);
- //MsgListener.driver.manage().timeouts().implicitlyWait(2000, TimeUnit.MILLISECONDS);
- DiscordBot.driver.get(url);
- WebDriverWait wait = new WebDriverWait(DiscordBot.driver, 10);
- wait.until(ExpectedConditions
- .elementToBeClickable(By.cssSelector("div[class~=beatmap-list]")));
- Document document = Jsoup.parse(DiscordBot.driver.getPageSource());
- //System.out.println(document.toString());
- initmap = document.body()
- .select("div[class~=beatmap-list]")
- .first()
- .child(0)
- .select("div.truncate.beatmap-title a").get(0).attr("href");*/
} else {
return new CommandResult(CommandResultType.FAILURE, "There is no account of your rich presence, therefore I cannot get the beatmap from your status.");
}
- /*
- * } else { throw new
- * IllegalArgumentException("Rich presence is not an instance of osu!, therefore I cannot get the beatmap from your status."
- * ); }
- */
}
+
+ //if still dud aka no presence nor url
if(initmap.equals("null")) { //shouldnt throw at all
return new CommandResult(CommandResultType.FAILURE, "You haven't played any maps I can recognize yet!");
}
- List params = Arrays.asList(words);
- double dacc = 100;
- int combo = -1, mods = 0, miss = 0;
- PPv2Parameters p = new Koohii.PPv2Parameters();
+
+ //parse beatmap
+ Map beatmap = null;
try {
- p.beatmap = new Koohii.Parser()
+ beatmap = new Koohii.Parser()
.map(new BufferedReader(new InputStreamReader(getMap(initmap), "UTF-8")));
} catch (IOException e1) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
} catch (UnsupportedOperationException e1) {
return new CommandResult(CommandResultType.FAILURE, "This gamemode is not yet supported.");
} catch (IllegalArgumentException e1) {
return new CommandResult(CommandResultType.INVALIDARGS, e1.getMessage());
}
- if (p.beatmap.title.isEmpty()) {
+
+ if (beatmap.title.isEmpty()) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap.");
}
+
+ //parse pp params
+ double dacc = 100;
+ int combo = beatmap.max_combo(), mods = 0, miss = 0;
try {
- for (int i = 0; i < params.size(); i++) {
- String param = params.get(i);
+ for (int i = 0; i < amend.size(); i++) {
+ String param = amend.get(i);
if (param.startsWith("+")) {
mods = Koohii.mods_from_str(param.substring(1).toUpperCase());
+ //invalid mods just get ignored
+
} else if (param.toLowerCase().endsWith("m")) {
miss = Integer.parseInt(param.substring(0, param.length() - 1));
+ if(miss < 0 || miss > beatmap.objects.size())
+ return new CommandResult(CommandResultType.INVALIDARGS, "Invalid miss count specified.");
+
} else if (param.endsWith("%")) {
dacc = Double.parseDouble(param.substring(0, param.length() - 1));
+ if(dacc < 0)
+ return new CommandResult(CommandResultType.INVALIDARGS, "Invalid accuracy specified.");
+
} else if (param.toLowerCase().endsWith("x")) {
combo = Integer.parseInt(param.substring(0, param.length() - 1));
+ if(combo < 0 || combo > beatmap.max_combo())
+ return new CommandResult(CommandResultType.INVALIDARGS, "Invalid combo specified.");
}
}
} catch (NumberFormatException e) {
- return new CommandResult(CommandResultType.INVALIDARGS, "Invalid value inputted.");
+ return new CommandResult(CommandResultType.INVALIDARGS, "Invalid params specified.");
}
- DiffCalc stars = new Koohii.DiffCalc().calc(p.beatmap, mods);
- Accuracy acc = new Accuracy(dacc, p.beatmap.objects.size(), miss);
- p.n50 = acc.n50;
- p.n100 = acc.n100;
- p.n300 = acc.n300;
- p.nmiss = miss;
- p.aim_stars = stars.aim;
- p.speed_stars = stars.speed;
- p.mods = mods;
- if (combo > 0) {
- p.combo = combo;
- } else {
- p.combo = p.beatmap.max_combo();
- }
- EmbedBuilder eb = new EmbedBuilder();
- JSONTokener result = null;
- InputStream stream = null;
- URL url = null;
- try {
- String addparam = "";
- if(p.beatmap.mode == 1) {
- if((mods & 1<<8) != 0) addparam = "&mods=256";
- if((mods & 1<<6) != 0) if(addparam.isEmpty()) addparam = "&mods=64";
- else addparam = "";
- }
- url = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + initmap.substring(initmap.lastIndexOf("/") + 1).split("&")[0] + addparam);
- stream = url.openStream();
- } catch (IOException ex) {
- ex.printStackTrace();
- }
- result = new JSONTokener(stream);
- JSONObject jbm = new JSONArray(result).getJSONObject(0);
- String setid = jbm.getString("beatmapset_id");
- double accVal, ppVal, starsVal;
- MapStats stats = new MapStats(p.beatmap);
+
+ //compute pp
+ Accuracy acc = null;
+ JSONObject jbm = null;
try {
- PPv2 pp = new PPv2(p);
- ppVal = pp.total;
- accVal = acc.value(); //somehow real_acc aint correct, pretty sure i screwed sth up
- starsVal = stars.total;
- Koohii.mods_apply(mods, stats, 1|2|4|8);
- } catch (UnsupportedOperationException e) { //should always only be taiko
- starsVal = jbm.getDouble("difficultyrating");
- TaikoPP pp = new TaikoPP(starsVal, dacc, p);
- ppVal = pp.pp;
- accVal = pp.acc;
- Koohii.mods_apply(mods, stats, 0); //want nothing but speed to be changed
+ acc = new Accuracy(dacc, miss, beatmap);
+ jbm = computePP(beatmap, initmap.substring(initmap.lastIndexOf("/") + 1).split("&")[0],
+ mods, acc.n50, acc.n100, acc.n300, miss, combo);
+ } catch (IOException e) {
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
+ } catch (IllegalArgumentException e) {
+ return new CommandResult(CommandResultType.INVALIDARGS, e.getMessage());
}
- String totaldur = MiscUtils.convertMillis(TimeUnit.SECONDS.toMillis((int)(jbm.getInt("total_length") / stats.speed)));
- String draindur = MiscUtils.convertMillis(TimeUnit.SECONDS.toMillis((int)(jbm.getInt("hit_length") / stats.speed)));
+
+ //build embed
+ EmbedBuilder eb = new EmbedBuilder();
if(jbm.has("source") && !jbm.getString("source").isEmpty()) eb.setTitle("Source: " + jbm.getString("source"));
- eb.setDescription("Mode: " + modes.get(p.beatmap.mode));
- eb.setAuthor("PP information for " + (p.beatmap.title_unicode.isEmpty() ? p.beatmap.title : p.beatmap.title_unicode),
- initmap, "https://b.ppy.sh/thumb/" + setid + ".jpg");
- eb.addField("Artist", (p.beatmap.artist_unicode.isEmpty() ? p.beatmap.artist : p.beatmap.artist_unicode),
- true);
- eb.addField("Created by", p.beatmap.creator, true);
+ eb.setDescription("Mode: " + modes[beatmap.mode]);
+ eb.setAuthor("PP information for " + (beatmap.title_unicode.isEmpty() ? beatmap.title : beatmap.title_unicode),
+ initmap, "https://b.ppy.sh/thumb/" + jbm.getString("beatmapset_id") + ".jpg");
+
+ eb.addField("Artist", (beatmap.artist_unicode.isEmpty() ? beatmap.artist : beatmap.artist_unicode), true);
+ eb.addField("Created by", beatmap.creator, true);
+
+ String totaldur = MiscUtils.convertMillis(TimeUnit.SECONDS.toMillis((int)(jbm.getInt("total_length") / jbm.getDouble("speed"))));
+ String draindur = MiscUtils.convertMillis(TimeUnit.SECONDS.toMillis((int)(jbm.getInt("hit_length") / jbm.getDouble("speed"))));
eb.addField("Duration", totaldur + " | Drain " + draindur, true);
- eb.addField("Difficulty", p.beatmap.version, true);
- eb.addField("Mods", (p.mods == 0 ? "None" : Koohii.mods_str(p.mods)), true);
- eb.addField("BPM", df.format(jbm.getDouble("bpm") * stats.speed), true);
- eb.addField("Accuracy", df.format(accVal * 100), true);
- eb.addField("Combo", String.valueOf(p.combo), true);
- eb.addField("Misses", String.valueOf(p.nmiss), true);
- eb.addField("300", String.valueOf(p.n300), true);
- eb.addField("100", String.valueOf(p.n100), true);
- eb.addField("50", String.valueOf(p.n50), true);
- eb.addField("PP", df.format(ppVal), true);
+
+ eb.addField("Difficulty", beatmap.version, true);
+ eb.addField("Mods", (mods == 0 ? "None" : Koohii.mods_str(mods)), true);
+ eb.addField("BPM", df.format(jbm.getDouble("bpm") * jbm.getDouble("speed")), true);
+
+ eb.addField("Accuracy", df.format(jbm.getDouble("accVal") * 100) + "%", true);
+ eb.addField("Combo", String.valueOf(combo) + "x", true);
+ eb.addField("Misses", String.valueOf(miss), true);
+ eb.addField("300", String.valueOf(acc.n300), true);
+ eb.addField("100", String.valueOf(acc.n100), true);
+ eb.addField("50", String.valueOf(acc.n50), true);
+
+ eb.addField("PP", df.format(jbm.getDouble("ppVal")), true);
+
if(weight) {
try {
- String gain = df.format(weightForPP(ppVal, jbm.getString("beatmap_id"), getUserBest(uid, p.beatmap.mode)));
- eb.addField("Actual pp gain for " + uid, (gain.equals("-1") ? "User has a better score than this" : gain + "pp"), true);
- } catch (IllegalArgumentException e1) {
- return new CommandResult(CommandResultType.INVALIDARGS, e1.getMessage());
+ JSONObject res = getPlayerData("get_user_best", uid, beatmap.mode, user);
+ if(res.has("result")) {
+ String gain = df.format(weightForPP(jbm.getDouble("ppVal"), jbm.getString("beatmap_id"), res.getJSONArray("result")));
+ eb.addField("Actual pp gain for " + uid, (gain.equals("-1") ? "User has a better score than this" : gain + "pp"), true);
+ } else {
+ channel.sendMessage("Unknown user `" + res.getString("search") + "` specified for pp checking, ignoring...").queue();
+ }
+ } catch (IOException e) {
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
}
- eb.setFooter("Stars: " + df.format(starsVal) + " | CS: " + df.format(stats.cs) + " | HP: " + df.format(stats.hp)
- + " | AR: " + df.format(stats.ar) + " | OD: " + df.format(stats.od), null);
- //e.setColor(new Color(248, 124, 248));
+
+ eb.setFooter("Stars: " + df.format(jbm.getDouble("starsVal")) + " | CS: " + df.format(jbm.getDouble("cs")) + " | HP: " + df.format(jbm.getDouble("hp"))
+ + " | AR: " + df.format(jbm.getDouble("ar")) + " | OD: " + df.format(jbm.getDouble("od")), null);
eb.setColor(new Color(239, 109, 167));
- if(!mid.isEmpty()) {
- channel.deleteMessageById(mid.get(0)).queue();
+
+ //remove retrieve msg if sent
+ if(msgId.isDone()) {
+ msgId.thenAccept(m -> channel.deleteMessageById(m).queue());
}
- //channel.sendMessage(e.build()).queue();
+
channel.sendMessage(eb.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
}, "[beatmap URL] [+|%|x|m] [-w [username]]", Arrays.asList("-w", "+DT", "https://osu.ppy.sh/b/1817768 93.43% -w", "https://osu.ppy.sh/beatmaps/1154509 +HDDT 96.05% 147x 2m -w despawningbone"),
"Check PP information about that beatmap!", Arrays.asList(
" * Available parameters: +[mod alias], [accuracy]%, [combo]x, [misses]m",
" * specify the `-w` parameter with a username (or none for your own) to check how many actual pp the play will get them!",
" * You can also not input the URL, if you are currently playing osu! and discord sensed it."));
registerSubCommand("weight", Arrays.asList("w"), (channel, user, msg, words) -> {
- //System.out.println(weightForPP(95.91, null, null));
- //String debugMax = "", debugMin = "", debugData = "", debugAll = "";
List params = new ArrayList<>(Arrays.asList(words));
int modeId = getMode(params);
+
final double precision = 1e-4, iterations = 500;
+
+ //get user
String uid;
int index = params.size() < 2 ? 0 : 1;
try {
uid = params.get(index - 1);
if(index == 0 || (!uid.contains("osu.ppy.sh/u") && (uid.startsWith("http") && uid.contains("://")))) {
uid = getPlayer(user);
}
} catch (IndexOutOfBoundsException e) {
uid = getPlayer(user);
}
+
+ //get user top
JSONArray arr = null;
try {
- arr = getUserBest(uid, modeId);
- } catch (IllegalArgumentException e) {
- return new CommandResult(CommandResultType.INVALIDARGS, e.getMessage());
+ JSONObject res = getPlayerData("get_user_best", uid.trim(), modeId, user);
+ if(!res.has("result"))
+ return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + res.getString("search") + "`.");
+
+ arr = res.getJSONArray("result");
+ } catch (IOException e) {
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
- double maxPP = arr.getJSONObject(0).getDouble("pp") + 100; //fricc those who wanna get pp over 100pp more than they can lmao
+
+ double limit = arr.getJSONObject(0).getDouble("pp") + 100; //fricc those who wanna get pp over 100pp more than they can lmao
+
+ //perform bisection search
try {
- //double basePP = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + uid).openStream())).getJSONObject(0).getDouble("pp_raw");
double targetPP = params.size() > index ? Double.parseDouble(params.get(index)) : 1;
- int i = 0; double minPP = 0, mid = 0;
- /*for(i = 0; i < 600; i++) {
- //debugData += weightForPP(i, null, arr) + "\n";
- debugData += i + " " + weightForPP(i, null, arr) + "\n";
- }*/
- //System.out.println(debugData);
- i = 0;
+ int i = 0; double minPP = 0, maxPP = limit, mid = 0;
while(i < iterations && Math.abs(maxPP - minPP) > precision) {
mid = (maxPP + minPP) / 2;
double temp = weightForPP(mid, null, arr);
- //System.out.println(i + ", " + mid + ", " + temp);
if(temp > targetPP) {
maxPP = mid;
} else {
minPP = mid;
}
i++;
- //System.out.println(maxPP + " " + minPP);
- //debugAll += maxPP + " " + minPP + "\n";
- //debugMax += maxPP + "";
- //debugMin += minPP + " ";
}
- //System.out.println(debugAll);
- //System.out.println(debugMax);
- //System.out.println(debugMin);
- if(!df.format(mid).equals(df.format(arr.getJSONObject(0).getDouble("pp") + 100))) {
- channel.sendMessage("For " + uid + " to actually gain " + df.format(targetPP) + "pp in " + modes.get(modeId) + ", they have to play a map worth approximately **" + df.format(mid) + "pp** raw.").queue();
+
+ if(!df.format(mid).equals(df.format(limit))) {
+ channel.sendMessage("For " + uid + " to actually gain " + df.format(targetPP) + "pp in " + modes[modeId] + ", they have to play a map worth approximately **" + df.format(mid) + "pp** raw.").queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.INVALIDARGS, "You can't really achieve such large pp jumps without people thinking you are hacking :P");
}
} catch (NumberFormatException e) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid target pp specified.");
}
- //return "";
}, "[username] [wished pp gain]", Arrays.asList("", "10", "despawningbone 20"),
"Estimate how much raw pp a map needs to have in order to gain you pp!",
Arrays.asList(" * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
- //TODO NEED EXTREME TIDYING UP
- //TODO merge parts of the code with getRecent() and getPP();
+ //DONE NEED EXTREME TIDYING UP
+ //DONE merge parts of the code with getRecent() and getPP();
registerSubCommand("recent", Arrays.asList("r"), (channel, user, msg, words) -> {
List params = new ArrayList<>(Arrays.asList(words));
int modeId = getMode(params);
- if(modeId > 1) modeId = 0;
+ if(modeId > 1) modeId = 0; //ignore unsupported modes and default to standard
+
boolean passOnly = params.removeAll(Collections.singleton("-p")); //no need space, as spaced ones are split (if a player has a name with spaced -p it might be a problem)
+
+ //parse params
String[] split = String.join(" ", params).split("\\|");
String search = split[0].trim();
int nrecent = 0;
try {
nrecent = Integer.parseInt(split[1].trim()) - 1;
} catch (NumberFormatException e) {
//print to channel?
} catch (ArrayIndexOutOfBoundsException e) {
; //nothing special about this
}
if(nrecent > 50) {
return new CommandResult(CommandResultType.FAILURE, "Only the 50 most recent plays can be recalled!");
}
- String id = "";
- boolean noturl = false;
- try {
- new URL(search);
- id = search.substring(search.lastIndexOf("/") + 1);
- } catch (MalformedURLException e) {
- noturl = true;
- id = search;
- }
- if(id.isEmpty()) {
- id = getPlayer(user);
- }
- JSONTokener result = null;
- InputStream stream = null;
- URL url = null;
+
+ //fetch recent plays
+ JSONArray array;
+ String name;
try {
- String addparam = "";
- if (noturl) {
- addparam = "&type=string";
- }
- if (modeId != 0) {
- addparam += ("&m=" + modeId);
- }
- url = new URL(
- "https://osu.ppy.sh/api/get_user_recent?k=" + osuAPI + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=50");
- stream = url.openStream();
- if (stream.available() < 4) {
- return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + id + "` or the player has not been playing in the last 24h.");
- }
- } catch (IOException ex) {
- ex.printStackTrace();
- return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex));
+ JSONObject res = getPlayerData("get_user_recent", search, modeId, user);
+ if(!res.has("result"))
+ return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + res.getString("search") + "` or the player has not been playing in the last 24h.");
+
+ array = res.getJSONArray("result");
+
+ if(array.length() == 0)
+ return new CommandResult(CommandResultType.FAILURE, "You have no recent plays in this 24h!");
+
+ //set name according to supported formats
+ name = res.getBoolean("isId") ? //isId might return true on cases inputted as https://osu.ppy.sh/users/despawningbone for example, which would make the fetching redundant but still works
+ getPlayerData("get_user", search, 0, user).getJSONArray("result").getJSONObject(0).getString("username"):
+ res.getString("search");
+ } catch (IOException e) {
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
- result = new JSONTokener(stream);
- JSONArray array = new JSONArray(result);
- //System.out.println(array);
- if(array.length() > 0) {
- JSONObject mostRecent = null;
- if(passOnly) {
- int times = 0;
- for(int i = 0; i < array.length(); i++) {
- if(!array.getJSONObject(i).getString("rank").equals("F")) {
- times++;
- if(times - 1 >= nrecent) {
- mostRecent = array.getJSONObject(i);
- nrecent = i;
- break;
- }
+
+ //get which play to return
+ JSONObject mostRecent = null;
+ if(passOnly) {
+ int times = 0;
+ for(int i = 0; i < array.length(); i++) {
+ if(!array.getJSONObject(i).getString("rank").equals("F")) {
+ times++;
+ if(times - 1 >= nrecent) {
+ mostRecent = array.getJSONObject(i);
+ nrecent = i;
+ break;
}
}
- if(mostRecent == null) {
- return new CommandResult(CommandResultType.INVALIDARGS, "You did not have this much passed plays in the recent 50 plays!");
- }
- } else {
- try {
- mostRecent = array.getJSONObject(nrecent);
- } catch (JSONException e) {
- return new CommandResult(CommandResultType.FAILURE, "You only played " + array.length() + " times recently!");
- }
- }
- String beatmap = mostRecent.getString("beatmap_id");
- String name;
- if (noturl) {
- name = id;
- id = mostRecent.getString("user_id");
- } else {
- try {
- Long.parseLong(id);
- //get more info from here?
- name = new JSONObject(new JSONTokener(new URL("https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + id).openStream())).getString("username");
- } catch (NumberFormatException e) {
- name = id;
- } catch (IOException e) {
- return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
- }
- }
- int i;
- String more = "";
- for(i = nrecent + 1; i < array.length(); i++) {
- if(!array.getJSONObject(i).getString("beatmap_id").equals(beatmap)) {
- break;
- }
- if(i == array.length() - 1) {
- more = " (or more)";
- }
- }
- if(i == array.length()) {
- more = " (or more)";
- }
- PPv2Parameters p = new Koohii.PPv2Parameters();
- try {
- p.beatmap = new Koohii.Parser()
- .map(new BufferedReader(new InputStreamReader(getMap(beatmap), "UTF-8")));
- } catch (IOException e1) {
- return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
- } catch (UnsupportedOperationException e1) {
- return new CommandResult(CommandResultType.FAILURE, "This gamemode is not yet supported.");
- } catch (IllegalArgumentException e1) {
- return new CommandResult(CommandResultType.INVALIDARGS, e1.getMessage());
}
- int mods = mostRecent.getInt("enabled_mods");
- if (p.beatmap.title.isEmpty()) {
- return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap.");
+ if(mostRecent == null) {
+ return new CommandResult(CommandResultType.INVALIDARGS, "You did not have this much passed plays in the recent 50 plays!");
}
- DiffCalc stars = new Koohii.DiffCalc().calc(p.beatmap, mods);
- p.n50 = mostRecent.getInt("count50");
- p.n100 = mostRecent.getInt("count100");
- p.n300 = mostRecent.getInt("count300");
- p.nmiss = mostRecent.getInt("countmiss");
- //System.out.println(p.n300);
- p.nobjects = p.n300 + p.n100 + p.n50 + p.nmiss;
- //System.out.println(p.nobjects);
- p.aim_stars = stars.aim;
- p.speed_stars = stars.speed;
- p.mods = mods;
- p.combo = mostRecent.getInt("maxcombo");
- EmbedBuilder eb = new EmbedBuilder();
- //System.out.println(mods);
- String modStr = Koohii.mods_str(mods);
- eb.setColor(new Color(0, 0, 0));
- double ppVal, ppMax, starVal, accVal;
- JSONObject obj;
+ } else {
try {
- String addparam = "";
- if(p.beatmap.mode == 1) {
- if((mods & 1<<8) != 0) addparam = "&mods=256";
- if((mods & 1<<6) != 0) if(addparam.isEmpty()) addparam = "&mods=64";
- else addparam = "";
- }
- obj = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + mostRecent.getInt("beatmap_id") + addparam).openStream())).getJSONObject(0);
- } catch (JSONException | IOException e) {
- return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
+ mostRecent = array.getJSONObject(nrecent);
+ } catch (JSONException e) {
+ return new CommandResult(CommandResultType.FAILURE, "You only played " + array.length() + " times recently!");
}
- try {
- PPv2 pp = new PPv2(p);
- starVal = stars.total;
- accVal = pp.computed_accuracy.value();
- ppVal = pp.total;
- ppMax = constructPP(stars.aim, stars.speed, p.beatmap, mods).total;
- } catch (UnsupportedOperationException e) {
- starVal = obj.getDouble("difficultyrating");
- TaikoPP pp = new TaikoPP(starVal, p);
- ppVal = pp.pp;
- accVal = pp.acc;
- ppMax = new TaikoPP(starVal, p.max_combo, mods, 1, 0, p.beatmap).pp;
+ }
+
+ String bid = mostRecent.getString("beatmap_id");
+
+ //get consecutive tries
+ int i;
+ for(i = nrecent + 1; i < array.length(); i++) {
+ if(!array.getJSONObject(i).getString("beatmap_id").equals(bid)) {
+ break;
}
- eb.setTitle((p.beatmap.artist_unicode.isEmpty() ? p.beatmap.artist : p.beatmap.artist_unicode) + " - " + (p.beatmap.title_unicode.isEmpty() ? p.beatmap.title : p.beatmap.title_unicode) + " [" + p.beatmap.version + "]" + (modStr.isEmpty() ? "" : " +" + modStr) + " (" + df.format(starVal) + "*)", "https://osu.ppy.sh/beatmaps/" + beatmap);
- eb.setAuthor(MiscUtils.ordinal(nrecent + 1) + " most recent " + modes.get(modeId) + " play by " + name, "https://osu.ppy.sh/users/" + id, "https://a.ppy.sh/" + id);
- eb.setDescription(MiscUtils.ordinal(i - nrecent) + more + " consecutive try\nRanking: " + mostRecent.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + (mostRecent.getString("rank").equals("F") ? " (Estimated completion percentage: " + df.format(p.nobjects * 100.0 / p.beatmap.objects.size()) + "%)" : ""));
- eb.setThumbnail("https://b.ppy.sh/thumb/" + obj.getString("beatmapset_id") + ".jpg");
- eb.addField("Score", String.valueOf(mostRecent.getInt("score")), true);
- eb.addField("Accuracy", df.format(accVal * 100) + " (" + p.n300 + "/" + p.n100 + "/" + p.n50 + "/" + p.nmiss + ")", true);
- eb.addField("Combo", (mostRecent.getInt("perfect") == 0 ? p.combo + "x / " + p.beatmap.max_combo() + "x" : "FC (" + p.combo + "x)"), true);
- eb.addField("PP", df.format(ppVal) + "pp / " + df.format(ppMax) + "pp", true);
- eb.setFooter("Score submitted", null);
- eb.setTimestamp(OffsetDateTime.parse(mostRecent.getString("date").replace(" ", "T") + "+00:00"));
- channel.sendMessage(eb.build()).queue();
- return new CommandResult(CommandResultType.SUCCESS);
- } else {
- return new CommandResult(CommandResultType.FAILURE, "You have no recent plays in this 24h!");
}
+ //if we reached the end of the list, there might be more that we missed since we can only fetch 50 recent plays
+ String more = "";
+ if(i == array.length()) {
+ more = " (or more)";
+ }
+
+ //parse map of the recent play and compute pp
+ int n50 = mostRecent.getInt("count50"), n100 = mostRecent.getInt("count100"), n300 = mostRecent.getInt("count300"),
+ miss = mostRecent.getInt("countmiss"), combo = mostRecent.getInt("maxcombo"), mods = mostRecent.getInt("enabled_mods");
+ Map beatmap = null;
+ JSONObject jbm = null;
+ try {
+ beatmap = new Koohii.Parser()
+ .map(new BufferedReader(new InputStreamReader(getMap(bid), "UTF-8")));
+ jbm = computePP(beatmap, bid, mods, n50, n100, n300, miss, combo);
+ } catch (IOException e1) {
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
+ } catch (UnsupportedOperationException e1) {
+ return new CommandResult(CommandResultType.FAILURE, "This gamemode is not yet supported.");
+ } catch (IllegalArgumentException e1) {
+ return new CommandResult(CommandResultType.INVALIDARGS, e1.getMessage());
+ }
+
+ //build embed
+ String modStr = Koohii.mods_str(mods);
+ EmbedBuilder eb = new EmbedBuilder();
+ eb.setColor(new Color(0, 0, 0));
+ eb.setTitle((beatmap.artist_unicode.isEmpty() ? beatmap.artist : beatmap.artist_unicode)
+ + " - " + (beatmap.title_unicode.isEmpty() ? beatmap.title : beatmap.title_unicode) + " [" + beatmap.version + "]"
+ + (modStr.isEmpty() ? "" : " +" + modStr) + " (" + df.format(jbm.getDouble("starsVal")) + "*)"
+ , "https://osu.ppy.sh/beatmaps/" + bid);
+ eb.setAuthor(MiscUtils.ordinal(nrecent + 1) + " most recent " + modes[modeId] + " play by " + name,
+ "https://osu.ppy.sh/users/" + mostRecent.getString("user_id"), "https://a.ppy.sh/" + mostRecent.getString("user_id"));
+
+ eb.setDescription(MiscUtils.ordinal(i - nrecent) + more + " consecutive try\n"
+ + "Ranking: " + mostRecent.getString("rank").replaceAll("H", "+").replaceAll("X", "SS")
+ + (mostRecent.getString("rank").equals("F") ? " (Estimated completion percentage: " + df.format((n50 + n100 + n300 + miss) * 100.0 / beatmap.objects.size()) + "%)" : ""));
+
+ eb.setThumbnail("https://b.ppy.sh/thumb/" + jbm.getString("beatmapset_id") + ".jpg");
+ eb.addField("Score", String.valueOf(mostRecent.getInt("score")), true);
+ eb.addField("Accuracy", df.format(jbm.getDouble("accVal") * 100) + " (" + n300 + "/" + n100 + "/" + n50 + "/" + miss + ")", true);
+ eb.addField("Combo", (mostRecent.getInt("perfect") == 0 ? combo + "x / " + beatmap.max_combo() + "x" : "FC (" + combo + "x)"), true);
+ eb.addField("PP", df.format(jbm.getDouble("ppVal")) + "pp / " + df.format(jbm.getDouble("ppMax")) + "pp", true);
+ eb.setFooter("Score submitted", null);
+ eb.setTimestamp(OffsetDateTime.parse(mostRecent.getString("date").replace(" ", "T") + "+00:00"));
+
+ channel.sendMessage(eb.build()).queue();
+ return new CommandResult(CommandResultType.SUCCESS);
}, "[-p] [username] [| index]", Arrays.asList("", "-p | 2", "-p despawningbone"),
"Check a user's recent play!", Arrays.asList(
" * Specifying the `-p` parameter will only search for plays that did not fail.",
" * Supports `-t` for taiko (Defaults to standard)."));
- //TODO tidy up first part, merge with getTopPlays()?
+ //DONE tidy up first part, merge with getTopPlays()?
registerSubCommand("ppchart", Arrays.asList("ppc"), (channel, user, msg, words) -> {
if(words.length < 30) {
List params = new ArrayList<>(Arrays.asList(words));
int modeId = getMode(params);
if(params.size() < 1) {
params.add(getPlayer(user));
}
+
+ //get all usernames
String[] split = String.join(" ", params).split("\\|");
String concName = String.join(" | ", split);
- XYChart chart = new XYChartBuilder().width(800).height(600).title("Top PP plays for " + concName + " (" + modes.get(modeId) + ")").yAxisTitle("PP").xAxisTitle("Plays (100 = top)").build();
+
+ //init chart
+ XYChart chart = new XYChartBuilder().width(800).height(600).title("Top PP plays for " + concName + " (" + modes[modeId] + ")").yAxisTitle("PP").xAxisTitle("Plays (100 = top)").build();
chart.getStyler().setDefaultSeriesRenderStyle(XYSeriesRenderStyle.Scatter);
chart.getStyler().setLegendPosition(LegendPosition.InsideSE);
chart.getStyler().setPlotContentSize(0.91);
chart.getStyler().setMarkerSize(7);
- //chart.getStyler().setAxisTitlePadding(24);
chart.getStyler().setChartTitlePadding(12);
chart.getStyler().setChartPadding(12);
chart.getStyler().setChartBackgroundColor(new Color(200, 220, 230));
- //ThreadLocalRandom ran = ThreadLocalRandom.current();
+
+ //plot each user's top plays onto chart
for(String name : split) {
List pps = new ArrayList<>();
try {
- JSONArray array = getUserBest(name.trim(), modeId);
+ JSONObject res = getPlayerData("get_user_best", name.trim(), modeId, user);
+ if(!res.has("result"))
+ return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + res.getString("search") + "`.");
+
+ JSONArray array = res.getJSONArray("result");
for(int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
pps.add(obj.getDouble("pp"));
}
- } catch (IllegalArgumentException e) {
- return new CommandResult(CommandResultType.INVALIDARGS, e.getMessage());
+ } catch (IOException e) {
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
- Collections.reverse(pps);
- /*XYSeries series =*/ chart.addSeries(name.trim() + "'s PP", pps);
- //use series.setXYSeriesRenderStyle() for average line
- //series.setMarkerColor(new Color(ran.nextInt(1, 255), ran.nextInt(1, 255), ran.nextInt(1, 255)));
+
+ Collections.reverse(pps); //so it orders from low to high left to right
+ chart.addSeries(name.trim() + "'s PP", pps);
}
+
+ //write to png
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
BitmapEncoder.saveBitmap(chart, os, BitmapFormat.PNG);
} catch (IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
channel.sendFile(new ByteArrayInputStream(os.toByteArray()), "ppchart.png").queue();
return new CommandResult(CommandResultType.SUCCESS);
} else {
return new CommandResult(CommandResultType.INVALIDARGS, "Please do not include so many players at once.");
}
}, "[mode] [username] [| usernames]...", Arrays.asList("", "taiko", "FlyingTuna | Rafis | despawningbone"),
"Get a graph with the users' top plays!", Arrays.asList(
" * You can specify up to 30 players at once, seperated by `|`."
+ " * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
+ //TODO reaction based paging?
registerSubCommand("set", Arrays.asList("s"), (channel, user, msg, words) -> {
int page = 1;
if (words.length < 1) {
return new CommandResult(CommandResultType.INVALIDARGS, "Please enter search words or an URL.");
- }/* else if (words.length > 2) {
- try {
- page = Integer.parseInt(words[2]);
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("Please enter a valid page number.");
- }
- }*/
+ }
+
String id = "", set = "";
try {
new URL(words[0]);
} catch (MalformedURLException e) {
- //throw new IllgalArgumentException("Invalid URL."); //NOFIX: for some reason, discord seems to continue the typing after this has been sent //its because of queue()
+ //if not url, search
try {
String[] split = String.join(" ", words).split("\\|");
String search = URLEncoder.encode(split[0].trim(), "UTF-8");
try {
if(split.length > 1) page = Integer.parseInt(split[1].trim());
} catch (NumberFormatException e1) {
return new CommandResult(CommandResultType.INVALIDARGS, "Please enter a valid page number.");
}
String url = "https://osusearch.com/query/?title=" + search + "&query_order=play_count&offset=0";
- URLConnection stream;
- stream = new URL(url).openConnection();
+ URLConnection stream = new URL(url).openConnection();
stream.addRequestProperty("User-Agent", "Mozilla/4.0");
JSONTokener tokener = new JSONTokener(stream.getInputStream());
id = String.valueOf(new JSONObject(tokener).getJSONArray("beatmaps").getJSONObject(0).getInt("beatmapset_id"));
set = "https://osu.ppy.sh/beatmapset/" + id;
- } catch (IOException | JSONException e1) {
- if(e1 instanceof JSONException) {
- return new CommandResult(CommandResultType.NORESULT);
- } else {
- return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
- }
+ } catch (IOException e1) {
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
+ } catch (JSONException e1) {
+ return new CommandResult(CommandResultType.NORESULT);
}
}
+
+ //empty id means words[0] was a valid url - accept and parse
if(id.equals("")) {
set = words[0];
id = set.substring(set.lastIndexOf("/") + 1);
if (set.contains("/beatmapsets/")) {
id = set.substring(set.lastIndexOf("/beatmapsets/"));
id = id.substring(id.lastIndexOf("/") + 1);
- } else {
+ } else { //old urls
if (set.contains("/b/") || set.contains("#")) {
return new CommandResult(CommandResultType.INVALIDARGS, "This is a specific beatmap, not a beatmap set.");
}
id = id.split("&")[0];
}
}
- JSONTokener result = null;
+
+ //get beatmap set info
InputStream stream = null;
- URL url = null;
try {
- url = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&s=" + id);
- stream = url.openStream();
+ stream = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&s=" + id).openStream();
} catch (IOException ex) {
- ex.printStackTrace();
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex));
}
- result = new JSONTokener(stream);
- JSONArray arr = new JSONArray(result);
+ JSONArray arr = new JSONArray(new JSONTokener(stream));
+
+ //convert it to a list of object for streaming
List obj = new ArrayList();
for (int i = 0; i < arr.length(); i++) {
obj.add(arr.getJSONObject(i));
}
+ //group by mode sorted by difficulty and flatten back to a list
List fobj = obj.stream().sorted(Comparator.comparing(e -> e.getDouble("difficultyrating")))
.collect(Collectors.groupingBy(e -> e.getInt("mode"))).values().stream().flatMap(List::stream)
.collect(Collectors.toList());
+
+
EmbedBuilder em = new EmbedBuilder();
- JSONObject fo;
+
+ JSONObject fo; //use first object's metadata for universal info like author
try {
fo = fobj.get(0);
} catch (IndexOutOfBoundsException e) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid beatmap URL.");
}
- if(page > Math.ceil((double) fobj.size() / 3)) {
+
+ if(page > Math.ceil((double) fobj.size() / 3)) { //each page is 3 entries
return new CommandResult(CommandResultType.INVALIDARGS, "The beatmap set does not have this many difficulties!");
}
+
+ //build embed page heading
em.setAuthor("Beatmap set information of " + fo.getString("title"), set,
"https://b.ppy.sh/thumb/" + id + ".jpg");
em.setDescription("Author: " + fo.getString("artist"));
em.addField("BPM", fo.getString("bpm"), true);
em.addField("Creator", fo.getString("creator"), true);
em.addField("Approved/Ranked", (fo.getInt("approved") >= 1 ? "Yes" : "No"), true);
em.addBlankField(false);
+
+ //build each entry
int n = ((page - 1) * 3);
- //String desc = "";
for (int i = n; (n + 3 <= fobj.size() ? i < n + 3 : i < fobj.size()); i++) {
JSONObject json = fobj.get(i);
- String mode = modes.get(json.getInt("mode"));
- /*desc += "**[" + json.getString("version") + "](https://osu.ppy.sh/b/" + json.getString("beatmap_id") + ")** (" + mode + ")\n";
- desc += df.format(json.getDouble("difficultyrating")) + "* " + (json.get("max_combo").toString() + "*").replaceAll("null*", "N/A") + "";*/
- em.addField("Difficulty", "[" + json.getString("version") + "](https://osu.ppy.sh/b/" + json.getString("beatmap_id") + ")" + " ([Preview](http://bloodcat.com/osu/preview.html#" + json.getString("beatmap_id") + "))", false);
- /*em.addField("URL", "https://osu.ppy.sh/b/" + json.getString("beatmap_id"), true);
- em.addBlankField(true);*/
+ em.addField("Difficulty", "[" + json.getString("version") + "](https://osu.ppy.sh/b/" + json.getString("beatmap_id") + ")" + " ([Preview](https://jmir.xyz/osu/#" + json.getString("beatmap_id") + "))", false); //bloodcat is no longer a thing but someone hosted a copy for previewing here
em.addField("Max combo", json.get("max_combo").toString().replaceAll("null", "N/A"), true);
em.addField("Stars", df.format(json.getDouble("difficultyrating")), true);
- em.addField("Mode", mode, true);
+ em.addField("Mode", modes[json.getInt("mode")], true);
}
- em.setFooter("Page: " + String.valueOf(page) + "/" + String.valueOf((int) Math.ceil((double) fobj.size() / 3)),
- null);
+
+ //build footer and send
+ em.setFooter("Page: " + String.valueOf(page) + "/" + String.valueOf((int) Math.ceil((double) fobj.size() / 3)), null);
channel.sendMessage(em.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
}, " [| page]\n", Arrays.asList("big black", "high free spirits | 2"),
"Check info about a beatmap set!", Arrays.asList(
" * Useful to get the specific difficulty URL for !desp osu pp."));
+
//TODO add userset for setting username to db so getPlayer wont be wrong?
registerSubCommand("user", Arrays.asList("u"), (channel, user, msg, words) -> {
List params = new ArrayList<>(Arrays.asList(words));
int modeId = getMode(params);
String search = String.join(" ", params);
- String id = "";
- boolean noturl = false;
- try {
- new URL(search);
- id = search.substring(search.lastIndexOf("/") + 1);
- } catch (MalformedURLException e) {
- noturl = true;
- id = search;
- }
- if(id.isEmpty()) {
- id = getPlayer(user);
- }
- JSONTokener result = null;
- InputStream stream = null;
- URL url = null;
+
+ //fetch user data
+ JSONObject usr;
try {
- String addparam = "";
- if (noturl) {
- addparam = "&type=string";
- }
- if (modeId != 0) {
- addparam += ("&m=" + modeId);
- }
- url = new URL(
- "https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam);
- stream = url.openStream();
- if (stream.available() < 4) {
- return new CommandResult(CommandResultType.INVALIDARGS, "Unknown user `" + id + "`.");
- }
- } catch (IOException ex) {
- ex.printStackTrace();
+ JSONObject res = getPlayerData("get_user", search, modeId, user);
+ if(!res.has("result"))
+ return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + res.getString("search") + "`.");
+
+ usr = res.getJSONArray("result").getJSONObject(0);
+ } catch (IOException e) {
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
- result = new JSONTokener(stream);
- JSONObject usr = new JSONArray(result).getJSONObject(0);
- id = usr.getString("user_id"); //no matter if its url or not user_id is still gonna be the most accurate one
+ String id = usr.getString("user_id"); //no matter if its url or not user_id is still gonna be the most accurate one
+ //build embed
EmbedBuilder em = new EmbedBuilder();
- em.setAuthor("User info of " + usr.getString("username") + " (" + modes.get(modeId) + ")", "https://osu.ppy.sh/users/" + id);
+ em.setAuthor("User info of " + usr.getString("username") + " (" + modes[modeId] + ")", "https://osu.ppy.sh/users/" + id);
+
+ //check if pfp exists, if not fallback to blank
String img = "https://a.ppy.sh/" + id;
try {
new URL(img).openStream();
} catch (IOException e) {
img = "https://s.ppy.sh/images/blank.jpg";
}
em.setThumbnail(img);
- try {
- em.addField("PP", usr.getDouble("pp_raw") == 0 ? "No Recent Plays" : usr.getString("pp_raw"), true);
- em.addField("Accuracy", df.format(usr.getDouble("accuracy")), true);
- } catch (JSONException e) {
- return new CommandResult(CommandResultType.FAILURE, "This user has not played any map in " + modes.get(modeId) + " before.");
- }
- StringBuffer unibuff = new StringBuffer();
- char[] ch = usr.getString("country").toLowerCase().toCharArray();
- for (char c : ch) {
- int temp = (int) c;
- int temp_integer = 96; //for lower case
- if (temp <= 122 & temp >= 97)
- unibuff.append(Character.toChars(127461 + (temp - temp_integer)));
- }
- em.addField("Rank", (usr.getInt("pp_rank") == 0 ? "N/A" : "#" + usr.getString("pp_rank")) + " | Country #" + usr.getString("pp_country_rank"), true);
- em.addField("Country", unibuff.toString(), true);
+
+ //if user doesnt have pp, there wont be any data pretty much - also might be a result of inactivity (?)
+ if(usr.isNull("pp_raw"))
+ return new CommandResult(CommandResultType.FAILURE, "This user has not played any map in " + modes[modeId] + " before.");
+
+ em.appendDescription("Joined " + usr.getString("join_date"));
+ em.addField("PP", usr.getDouble("pp_raw") == 0 ? "No Recent Plays" : usr.getString("pp_raw"), true);
+ em.addField("Accuracy", df.format(usr.getDouble("accuracy")), true);
+ em.addField("Rank", (usr.getInt("pp_rank") == 0 ? "N/A" : "#" + usr.getString("pp_rank")) +
+ " | " + MiscUtils.countryNameToUnicode(usr.getString("country")) + " #" + usr.getString("pp_country_rank"), true);
+ em.addField("Play count", usr.getString("playcount") +
+ " (" + df.format(Long.parseLong(usr.getString("total_seconds_played")) / 3600.0) + "h)", true);
+ em.addField("Total score", usr.getString("total_score"), true);
+ em.addField("Ranked score", usr.getString("ranked_score") +
+ " (" + df.format((usr.getDouble("ranked_score") / usr.getDouble("total_score")) * 100) + "%)", true);
em.addField("Level", df.format(usr.getDouble("level")), true);
- em.addField("Play count", usr.getString("playcount"), true);
- em.addField("SS+", usr.getString("count_rank_ssh"), true);
- em.addField("SS", usr.getString("count_rank_ss"), true);
- double total = usr.getDouble("total_score");
- em.addField("Total score", String.valueOf((long) total), true);
em.addField("S+", usr.getString("count_rank_sh"), true);
+ em.addField("SS+", usr.getString("count_rank_ssh"), true);
+ em.addField("A", usr.getString("count_rank_a"), true);
em.addField("S", usr.getString("count_rank_s"), true);
- double ranked = usr.getDouble("ranked_score");
- em.addField("Ranked score", String.valueOf((long) ranked) + " (" + df.format((ranked / total) * 100) + "%)", true);
+ em.addField("SS", usr.getString("count_rank_ss"), true);
+
em.setColor(new Color(66, 134, 244));
- em.setFooter("300: " + usr.getString("count300") + " | 100: " + usr.getString("count100") + " | 50: "
- + usr.getString("count50"), null); //TODO display percentage?
+ em.setFooter("300: " + usr.getString("count300") + " | 100: " + usr.getString("count100") + " | 50: " + usr.getString("count50"), null); //TODO display percentage?
channel.sendMessage(em.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
- }, "[gamemode] [user URL|keyword]", Arrays.asList("", "despawningbone", "taiko despawningbone"),
+ }, "[gamemode] [user URL|keyword]", Arrays.asList("", "despawningbone", "-t despawningbone"),
"Check info about a user!", Arrays.asList(
" * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
//DONE api demanding; need to add cooldown //nvm i changed it to use XHR now
//parts mergeable with getRecent()
registerSubCommand("topplays", Arrays.asList("top", "t"), (channel, user, msg, words) -> {
List params = new ArrayList<>(Arrays.asList(words));
int modeId = getMode(params), page;
+
+ //get page number
String[] split = String.join(" ", params).split("\\|");
String search = split[0];
try {
page = Integer.parseInt(split[1].trim());
if(page > 10) {
return new CommandResult(CommandResultType.INVALIDARGS, "You can only request your top 100 plays!");
}
} catch (NumberFormatException e) {
return new CommandResult(CommandResultType.INVALIDARGS, "Invalid page number!");
} catch (ArrayIndexOutOfBoundsException e) {
page = 1; //its normal for people to not input index
}
- String id = "";
- boolean noturl = false;
- try {
- new URL(search);
- id = search.substring(search.lastIndexOf("/") + 1);
- } catch (MalformedURLException e) {
- noturl = true;
- id = search;
- }
- if(id.isEmpty()) {
- id = getPlayer(user);
- }
- JSONTokener result = null;
- InputStream stream = null;
- String name, addparam = "";
+
+ //get user id then fetch top plays from XHR req
+ JSONArray array;
+ String name, id;
try {
- if (noturl) {
- addparam = "&type=string";
- }
- JSONObject userObj = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_user?k=" + osuAPI + "&u=" + id + addparam).openStream())).getJSONObject(0);
- name = userObj.getString("username");
- id = userObj.getString("user_id");
- URLConnection con = new URL("https://osu.ppy.sh/users/" + id + "/scores/best?mode=" + (modeId == 0 ? "osu" : modeId == 1 ? "taiko" : modeId == 2 ? "fruits" : modeId == 3 ? "mania" : "osu") + "&offset=" + (page - 1) * 10 + "&limit=10").openConnection();
- stream = con.getInputStream();
+ JSONObject res = getPlayerData("get_user", search, modeId, user);
+ if(!res.has("result"))
+ return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + res.getString("search") + "`.");
+
+ JSONObject usr = res.getJSONArray("result").getJSONObject(0);
+ name = usr.getString("username");
+ id = usr.getString("user_id");
+
+ //top plays internal api use different names for modes than listed
+ String[] m = new String[]{"osu", "taiko", "fruits", "mania"};
+
+ InputStream stream = new URL("https://osu.ppy.sh/users/" + id + "/scores/best"
+ + "?mode=" + m[modeId] + "&offset=" + (page - 1) * 10 + "&limit=10").openStream();
+
if (stream.available() < 4) {
return new CommandResult(CommandResultType.FAILURE, "This player does not have this many plays in this gamemode!");
}
- } catch (IOException ex) {
- return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex));
- } catch (JSONException ex) {
- return new CommandResult(CommandResultType.INVALIDARGS, "Unknown user `" + id + "`.");
+
+ array = new JSONArray(new JSONTokener(stream));
+ } catch (IOException e) {
+ return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
- result = new JSONTokener(stream);
- JSONArray array = new JSONArray(result);
+
+ //build embed
EmbedBuilder eb = new EmbedBuilder();
- eb.setAuthor(name + "'s top plays (" + modes.get(modeId) + ")", "https://osu.ppy.sh/users/" + id, "https://a.ppy.sh/" + id);
+ eb.setAuthor(name + "'s top plays (" + modes[modeId] + ")", "https://osu.ppy.sh/users/" + id, "https://a.ppy.sh/" + id);
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- try {
- for(int i = 0; i < array.length(); i++) {
- JSONObject obj = array.getJSONObject(i);
- String mods = obj.getJSONArray("mods").join("").replaceAll("\"", "");
- /*bInfo = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + bId).openStream())).getJSONObject(0);
- String info = "**" + obj.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + "** "
- + df.format(bInfo.getDouble("difficultyrating")) + "*"
- + (!mods.isEmpty() ? " **" + mods + "** " : " ")
- + " ([link](https://osu.ppy.sh/beatmap/" + bId + "))"
- + "\n" + (obj.getDouble("pp") + "pp (" + obj.getInt("count300") + "/" + obj.getInt("count100") + "/" + obj.getInt("count50") + "/" + obj.getInt("countmiss") + ")")
- + " " + (obj.getInt("perfect") == 1 ? "FC " + obj.getInt("maxcombo") + "x" : obj.getInt("maxcombo") + (!bInfo.isNull("max_combo") ? "/" + bInfo.getInt("max_combo") + "x" : "x"))
- + "\nPlayed on " + obj.getString("date");
- eb.addField((i + 1) + ". " + bInfo.getString("artist") + " - " + bInfo.getString("title") + " [" + bInfo.getString("version") + "]", info, false); //no unicode unfortunately*/
- JSONObject stats = obj.getJSONObject("statistics");
- JSONObject bInfo = obj.getJSONObject("beatmap");
- JSONObject bsInfo = obj.getJSONObject("beatmapset");
- int bId = bInfo.getInt("id");
- String info = "**" + obj.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + "**"
- + (!mods.isEmpty() ? " **" + mods + "** " : " ")
- + df.format(bInfo.getDouble("difficulty_rating")) + "*"
- + " ([map link](https://osu.ppy.sh/beatmaps/" + bId + "))"
- + "\nScore: " + obj.getInt("score")
- + "\n**" + (obj.getDouble("pp") + "pp** | Weighted " + df.format(obj.getJSONObject("weight").getDouble("pp")) + "pp (" + df.format(obj.getJSONObject("weight").getDouble("percentage")) + "%)"
- + "\n" + df.format(obj.getDouble("accuracy") * 100) + "% " + (obj.getBoolean("perfect") ? "FC " + obj.getInt("max_combo") + "x" : obj.getInt("max_combo") + "x") + " (" + stats.getInt("count_300") + "/" + stats.getInt("count_100") + "/" + stats.getInt("count_50") + "/" + stats.getInt("count_miss") + ")")
- + "\nPlayed on " + format.format(DatatypeConverter.parseDateTime(obj.getString("created_at")).getTime());
- eb.addField(((page - 1) * 10 + (i + 1)) + ". " + bsInfo.getString("artist") + " - " + bsInfo.getString("title") + " [" + bInfo.getString("version") + "]", info, false); //no unicode unfortunately*/
- }
- eb.setFooter("Page: " + page, null);
- eb.setColor(new Color(5, 255, 162));
- } catch (JSONException e1) {
- return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e1));
+
+ for(int i = 0; i < array.length(); i++) {
+ JSONObject obj = array.getJSONObject(i);
+ String mods = obj.getJSONArray("mods").join("").replaceAll("\"", "");
+ JSONObject stats = obj.getJSONObject("statistics");
+ JSONObject bInfo = obj.getJSONObject("beatmap");
+ JSONObject bsInfo = obj.getJSONObject("beatmapset");
+ int bId = bInfo.getInt("id");
+
+ String info = "**" + obj.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + "**"
+ + (!mods.isEmpty() ? " **" + mods + "** " : " ")
+ + df.format(bInfo.getDouble("difficulty_rating")) + "*"
+ + " ([map link](https://osu.ppy.sh/beatmaps/" + bId + "))"
+ + "\nScore: " + obj.getInt("score")
+ + "\n**" + (obj.getDouble("pp") + "pp** | Weighted " + df.format(obj.getJSONObject("weight").getDouble("pp")) + "pp (" + df.format(obj.getJSONObject("weight").getDouble("percentage")) + "%)"
+ + "\n" + df.format(obj.getDouble("accuracy") * 100) + "% " + (obj.getBoolean("perfect") ? "FC " + obj.getInt("max_combo") + "x" : obj.getInt("max_combo") + "x") + " (" + stats.getInt("count_300") + "/" + stats.getInt("count_100") + "/" + stats.getInt("count_50") + "/" + stats.getInt("count_miss") + ")")
+ + "\nPlayed on " + format.format(DatatypeConverter.parseDateTime(obj.getString("created_at")).getTime());
+
+ eb.addField(((page - 1) * 10 + (i + 1)) + ". " + bsInfo.getString("artist") + " - " + bsInfo.getString("title") + " [" + bInfo.getString("version") + "]", info, false); //no unicode unfortunately*/
}
+
+ eb.setFooter("Page: " + page, null);
+ eb.setColor(new Color(5, 255, 162));
channel.sendMessage(eb.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
}, "[username] [|page]", Arrays.asList("", "| 2", "despawningbone", "-m despawningbone"),
"Get the top plays of a user!", Arrays.asList(
" * Supports `-t` for taiko, `-c` for catch, and `-m` for mania (Defaults to standard)."));
-
- /*registerSubCommand("search", Arrays.asList("s"), (channel, user, msg, words) -> { //need to deprecate google search //no longer works at all
- String search = String.join(" ", Arrays.asList(words));
- List> url = new ArrayList<>();
- try {
- url = GoogleSearch
- .search(URLEncoder.encode("site:https://osu.ppy.sh/ " + search, "UTF-8"), 10);
- } catch (IOException e1) {
- e1.printStackTrace();
- }
- if (!url.isEmpty()) {
- String list = "";
- Stream> stream = url.stream();
- if (words[0].equals("mapsearch")) {
- stream = stream.filter(n -> n.getValue().contains("/b/") || n.getValue().contains("/s/")
- || n.getValue().contains("/beatmapsets/"));
- } else if (words[0].equals("usersearch")) {
- stream = stream.filter(n -> n.getValue().contains("/u/") || n.getValue().contains("/users/"));
- }
- list = "Search results for " + search + ":\n\n";
- list += stream
- .map(e -> (e.getKey() + "\n" + e.getValue() + "\n").replaceAll("", "").replaceAll("", ""))
- .collect(Collectors.joining("\n"));
- if (!list.isEmpty()) {
- channel.sendMessage(list).queue();
- return new CommandResult(CommandResultType.SUCCESS);
- } else {
- return new CommandResult(CommandResultType.NORESULT);
- }
- } else {
- return new CommandResult(CommandResultType.NORESULT);
- }
- }, "search|usersearch|mapsearch [keywords]", Arrays.asList("usersearch cookiezi", "search big black"),
- "Search the osu website!", null);*/
-
+
registerSubCommand("compare", Arrays.asList("c", "comp"), (channel, user, msg, words) -> {
String map = "", name = "", img = "", mode = "";
+
+ //get which card to compare with, starting from newest to oldest
int index = 1;
String[] param = String.join(" ", words).split("\\|");
try {
if(param.length > 1) index = Integer.parseInt(param[1].trim());
} catch(NumberFormatException e) {
channel.sendMessage("Invalid index specified. Defaulting to the most recent scorecard...").queue();
}
+
+ //iterate through recent 100 messsages
for(Message card : channel.getHistory().retrievePast(100).complete()) {
List embeds = card.getEmbeds();
if(embeds.size() > 0) {
MessageEmbed embed = embeds.get(0);
if(embed.getAuthor() == null) continue;
String author = embed.getAuthor().getName();
+
+ //our own cards
if(card.getAuthor().getId().equals(DiscordBot.BotID)) {
+ //recent
if(author.contains("most recent osu!")) {
map = embed.getUrl();
name = embed.getTitle().lastIndexOf("+") == -1 ? embed.getTitle().substring(0, embed.getTitle().lastIndexOf("(")) : embed.getTitle().substring(0, embed.getTitle().lastIndexOf("+"));
img = embed.getThumbnail().getUrl();
mode = author.split("most recent osu!")[1].split(" play")[0];
+
+ //pp
} else if(author.startsWith("PP information for")) {
map = embed.getAuthor().getUrl();
name = author.split("PP information for ")[1] + " [" +embed.getFields().get(3).getValue() + "]";
img = embed.getAuthor().getIconUrl();
mode = embed.getDescription().replace("Mode: osu!", "");
}
- } else if(card.getAuthor().getId().equals("289066747443675143")) { //owobot support
+
+ //owobot support
+ } else if(card.getAuthor().getId().equals("289066747443675143")) {
+ //new score tracker
if(author.startsWith("New") && author.contains("in osu!")) {
String markdown = embed.getDescription().substring(7, embed.getDescription().indexOf("\n")).trim();
map = markdown.substring(markdown.lastIndexOf("(") + 1, markdown.length() - 2);
name = markdown.substring(0, markdown.indexOf("__**]"));
img = embed.getThumbnail().getUrl();
mode = author.substring(author.lastIndexOf("in osu! "), author.length()).trim();
- } else if(card.getContentDisplay().contains("Most Recent ")) {
+
+ //>rs
+ } else if(card.getContentDisplay().contains("Recent ")) {
map = embed.getAuthor().getUrl();
name = author.substring(0, author.lastIndexOf("+"));
img = embed.getThumbnail().getUrl();
- mode = card.getContentDisplay().contains("Mania") ? "mania" : card.getContentDisplay().split("Most Recent ")[1].split(" ")[0]; //ye fucking blame owobot for being so inconsistent
+ mode = card.getContentDisplay().contains("Mania") ? "mania" : card.getContentDisplay().split("Recent ")[1].split(" ")[0]; //ye fucking blame owobot for being so inconsistent
+
+ //map url response
} else if(card.getContentDisplay().contains("map(s).")) {
map = embed.getAuthor().getUrl();
map = map.substring(0, map.length() - (map.endsWith("/") ? 1 : 0)); //fucking hell the url actually changes according to how ppl trigger it
name = author.split(" – ")[1];
String[] split = embed.getFields().get(0).getName().split("__", 3);
name = name.substring(0, name.lastIndexOf(" by ")) + " [" + split[1] + "]";
img = "https://b.ppy.sh/thumb/" + embed.getDescription().split("\\[map\\]\\(https:\\/\\/osu\\.ppy\\.sh\\/d\\/", 2)[1].split("\\)", 2)[0] + ".jpg";
mode = split[2].isEmpty() ? "std" : split[2].substring(2, split[2].lastIndexOf("]") + 1);
}
}
- if(index > 1) {
+
+ //if card hit check index and decrement + reset if neded
+ if(index > 1 && !map.isEmpty()) {
index--;
map = "";
}
+
+ //short circuit if found
if(!map.isEmpty()) break;
}
}
+
+ //yet another set of mode names so we have to parse
int modeId = 0;
if(mode.equalsIgnoreCase("taiko")) modeId = 1;
else if(mode.equalsIgnoreCase("catch") || mode.equals("ctb")) modeId = 2;
else if(mode.equalsIgnoreCase("mania")) modeId = 3;
+
+ //if still doesnt exist after iteration, fail
if(map.isEmpty()) return new CommandResult(CommandResultType.FAILURE, "There are no recent map/score cards to compare with in the past 100 messages!");
+
+ //if name or id is provided in the command use it else fallback to requester
String uid = param[0].isEmpty() ? getPlayer(user) : param[0].replaceAll(" ", "%20");
+
+ //get scores to compare
try {
URL url = new URL("https://osu.ppy.sh/api/get_scores?k=" + osuAPI + "&b=" + map.substring(map.lastIndexOf("/") + 1) + "&u=" + uid + "&m=" + modeId);
JSONArray arr = new JSONArray(new JSONTokener(url.openStream()));
+
+ //build embed
EmbedBuilder eb = new EmbedBuilder();
eb.setTitle("Top plays for " + arr.getJSONObject(0).getString("username") + " on " + name);
eb.setColor(new Color(237, 154, 70));
eb.setThumbnail(img);
+
+ //iterate through existing scores for this beatmap and add it to the embed
for(int i = 0; i < arr.length(); i++) {
JSONObject score = arr.getJSONObject(i);
String mods = Koohii.mods_str(score.getInt("enabled_mods"));
Accuracy acc = new Accuracy(score.getInt("count300"), score.getInt("count100"), score.getInt("count50"), score.getInt("countmiss"));
+
String info = "**`" + (mods.isEmpty() ? "No mods" : mods) + "` Score:\n"
+ score.getString("rank").replaceAll("H", "+").replaceAll("X", "SS") + " " + (score.isNull("pp") ? "No " : score.getDouble("pp")) + "pp** | Score **" + score.getInt("score") + "**\n"
- + df.format(acc.value() * 100) + "% " + (score.getInt("perfect") == 1 ? "FC " + score.getInt("maxcombo") + "x" : score.getInt("maxcombo") + "x") + " (" + acc.n300 + "/" + acc.n100 + "/" + acc.n50 + "/" + acc.nmisses + ")\n"
+ + df.format(acc.value(modeId) * 100) + "% " + (score.getInt("perfect") == 1 ? "FC " + score.getInt("maxcombo") + "x" : score.getInt("maxcombo") + "x") + " (" + acc.n300 + "/" + acc.n100 + "/" + acc.n50 + "/" + acc.nmisses + ")\n"
+ "Played on " + score.getString("date") + (score.getInt("replay_available") == 1 ? " (Replay available)" : "");
eb.appendDescription(info + "\n\n");
}
+
channel.sendMessage(eb.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
} catch (IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
} catch (JSONException e) {
e.printStackTrace();
return new CommandResult(CommandResultType.INVALIDARGS, "Unknown player `" + uid + "` or the player has no scores on this map.");
}
}, "[username] [| index]", null, "Compare your score with the most recent map/score card in the channel!", Arrays.asList(" * Specify the index to skip to the nth recent score card in the channel."));
}
private static double weightForPP(double pp, String ibid, JSONArray userBest) {
- //String[] dataset = test.split(" ");
- //List datas = Arrays.asList(dataset).stream().map(s -> Double.parseDouble(s)).collect(Collectors.toList());
-
double net = 0, change = -1;
List pps = new ArrayList<>();
+
+ //get new top pp list, replacing old score if needed
for(int i = 0; i < userBest.length(); i++) { //something like a treemap might work more elegantly, but i need to add pp to the list anyways so its fine i guess
- //for(int i = 0; i < datas.size(); i++) {
JSONObject obj = userBest.getJSONObject(i);
double v = obj.getDouble("pp");
- //double v = datas.get(i);
- //System.out.println("raw" + v);
String bid = obj.getString("beatmap_id");
if(bid.equals(ibid)) {
if(v >= pp) {
return -1;
} else {
change = v;
}
}
- if(v <= pp && !pps.contains(pp)) {
+ if(v <= pp && !pps.contains(pp)) { //insert on first smaller occurence
pps.add(pp);
}
pps.add(v);
}
+
+ //doesnt even get on top list - no pp gain
if(pps.indexOf(pp) == -1) {
return 0;
}
- //System.out.println(pps.indexOf(pp));
+
+ //calculate net weighted gain
if(change == -1) {
double last = pps.size() > 100 ? pps.remove(100) * (Math.pow(0.95, 99)): 0;
for(int i = pps.indexOf(pp); i < pps.size(); i++) {
double c = pps.get(i);
double w = c*(Math.pow(0.95, i));
- //System.out.println("pp" + w);
if(i == pps.indexOf(pp)) {
net += w;
} else {
net += c*(Math.pow(0.95, i - 1)) * (0.95 - 1);
}
- //System.out.println("t1" + net);
}
- //System.out.println("last" + last);
net -= last; //because the last one is completely not counted, not just shifted to a lower value
} else {
int old = pps.indexOf(change) - 1;
pps.remove(change);
for(int i = pps.indexOf(pp); i <= old; i++) {
double c = pps.get(i);
double w = c*(Math.pow(0.95, i));
if(c == pp) {
net += w - change*(Math.pow(0.95, old));
} else {
net += w * (0.95 - 1);
}
}
}
return net;
}
- private static JSONArray getUserBest(String id, int mode) {
+ //note: stripped ids are considered usernames - if you are sure that its an id (eg result from api) then wrap it in an url
+ private static JSONObject getPlayerData(String api, String input, int modeId, User requester) throws IOException {
+ //check if user url provided, if not assume username
+ String id = "";
boolean noturl = false;
try {
- new URL(id);
- id = id.substring(id.lastIndexOf("/") + 1);
+ new URL(input);
+ id = input.substring(input.lastIndexOf("/") + 1);
} catch (MalformedURLException e) {
noturl = true;
+ id = input;
}
- JSONTokener result = null;
- InputStream stream = null;
- URL url = null;
- try {
- String addparam = "";
- if (noturl) {
- addparam = "&type=string";
- }
- //System.out.println("https://osu.ppy.sh/api/get_user_best?k=" + osuAPI + "&m=" + mode + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=100");
- url = new URL(
- "https://osu.ppy.sh/api/get_user_best?k=" + osuAPI + "&m=" + mode + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=100");
- stream = url.openStream();
- if (stream.available() < 4) {
- throw new IllegalArgumentException("Unknown player `" + id + "`.");
- }
- } catch (IOException ex) {
- ex.printStackTrace();
+
+ //if empty fallback to own
+ if(id.isEmpty()) {
+ id = getPlayer(requester);
+ }
+
+ //set params and fetch
+ String addparam = "";
+ if (noturl) { //search as usernames
+ addparam = "&type=string";
+ }
+ if (modeId != 0) { //change mode
+ addparam += ("&m=" + modeId);
+ }
+
+ //limit either gets ignored or truncated to highest - is fine if i leave it as 100 (highest of any apis i use)
+ JSONArray a = new JSONArray(new JSONTokener(new URL("https://osu.ppy.sh/api/" + api + "?k=" + osuAPI + "&u=" + URLEncoder.encode(id, "UTF-8") + addparam + "&limit=100").openStream()));
+
+ //always return the val we did the search with, and only result if it exists
+ JSONObject wrap = new JSONObject("{\"search\": \"" + id + "\", \"isId\": " + !noturl + "}");
+ if (a.length() > 0) {
+ wrap.put("result", a);
+ }
+
+ return wrap;
+ }
+
+ //TODO find a way to parallelize map fetching and meta fetching if possible?
+ private static JSONObject computePP(Map beatmap, String bid, int mods, int n50, int n100, int n300, int miss, int combo) throws IllegalArgumentException, IOException {
+ //set osu-wide beatmap params (non mode specific)
+ PlayParameters p = new Koohii.PlayParameters();
+ p.beatmap = beatmap;
+ p.n50 = n50;
+ p.n100 = n100;
+ p.n300 = n300;
+ p.nmiss = miss;
+ p.nobjects = p.n300 + p.n100 + p.n50 + p.nmiss;
+ p.mods = mods;
+ if (combo > 0) {
+ p.combo = combo;
+ } else {
+ p.combo = p.beatmap.max_combo();
}
- result = new JSONTokener(stream);
- return new JSONArray(result);
+
+ //get beatmap metadata
+ String addparam = "";
+ if(p.beatmap.mode == 1) { //these mods alter metadata that taiko needs
+ if((mods & Koohii.MODS_HT) != 0) addparam = "&mods=256";
+ if((mods & (Koohii.MODS_DT | Koohii.MODS_NC)) != 0) if(addparam.isEmpty()) addparam = "&mods=64";
+ else addparam = "";
+ }
+ URL url = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&b=" + bid + addparam);
+ JSONObject jbm = new JSONArray(new JSONTokener(url.openStream())).getJSONObject(0);
+
+ //finally calc pp and put it in metadata
+ MapStats stats = new MapStats(p.beatmap);
+ switch(p.beatmap.mode) {
+ case 0: //osu
+ //std specific params - calc diff and acc
+ DiffCalc stars = new Koohii.DiffCalc().calc(p.beatmap, mods);
+ p.speed_stars = stars.speed;
+ p.aim_stars = stars.aim;
+
+ PPv2 oPP = new PPv2(p);
+ jbm.put("ppVal", oPP.total);
+ jbm.put("accVal", oPP.computed_accuracy.value(0)); //use computed
+ jbm.put("starsVal", stars.total);
+ jbm.put("ppMax", constructPP(stars.aim, stars.speed, p.beatmap, mods).total);
+ Koohii.mods_apply(mods, stats, 1|2|4|8);
+ break;
+ case 1: //taiko
+ //taiko specific params - get diff from metadata since theres no calculating mechanism
+ p.speed_stars = jbm.getDouble("difficultyrating");
+
+ TaikoPP tPP = new TaikoPP(p);
+ jbm.put("ppVal", tPP.pp);
+ jbm.put("accVal", tPP.acc);
+ jbm.put("starsVal", p.speed_stars);
+ jbm.put("ppMax", new TaikoPP(p.speed_stars, p.max_combo, mods, 1, 0, p.beatmap).pp);
+ tPP.taiko_mods_apply(mods, stats);
+ break;
+ default:
+ throw new UnsupportedOperationException("This gamemode is not yet supported.");
+ }
+
+ //mods_apply updates the values, so we cant use beatmap.cs etc and we gotta give back via jbm
+ jbm.put("cs", stats.cs);
+ jbm.put("hp", stats.hp);
+ jbm.put("ar", stats.ar);
+ jbm.put("od", stats.od);
+ jbm.put("speed", stats.speed);
+
+ return jbm;
}
private static PPv2 constructPP(double aim_stars, double speed_stars, Map b, int mods) {
- PPv2Parameters p = new PPv2Parameters();
+ PlayParameters p = new PlayParameters();
p.aim_stars = aim_stars;
p.speed_stars = speed_stars;
p.beatmap = b;
p.nobjects = b.objects.size();
p.mods = mods;
return new PPv2(p);
}
private static String getPlayer(User user) { //say im getting from presence/name?
try (Connection con = DiscordBot.db.getConnection()){
ResultSet rs = con.createStatement().executeQuery("SELECT game FROM users WHERE id = " + user.getId() + ";");
String player = rs.next() ? player = rs.getString(1).substring(0, rs.getString(1).indexOf("||")) : user.getName();
rs.close();
return player;
} catch (SQLException e) {
return user.getName();
}
}
private static int getMode(List params) {
- int modeId = 0; String mode = "";
- params.replaceAll(String::toLowerCase);
- if(params.contains("-t")) {
- mode = "-t";
+ int modeId = 0;
+ params.replaceAll(String::toLowerCase); //all searching should be case insensitive anyways, is fine if we change the entire thing
+
+ //pass by "ref" so it removes from the list;
+ //iterates over all mode params and remove, giving priority according to descending id and defaulting to standard (0)
+ if(params.removeAll(Collections.singleton("-t")))
modeId = 1;
- }
- if(params.contains("-c")) {
- mode = "-c";
+
+ if(params.removeAll(Collections.singleton("-c")))
modeId = 2;
- }
- if(params.contains("-m")) {
- mode = "-m";
+
+ if(params.removeAll(Collections.singleton("-m")))
modeId = 3;
- }
- params.removeAll(Collections.singleton(mode)); //pass by ref so it removes from the list
+
return modeId;
}
private static InputStream getMap(String origURL) throws IOException {
+ //get beatmap id
String id = origURL.substring(origURL.lastIndexOf("/") + 1);
- if (origURL.contains("/beatmapsets/")) {
+ if (origURL.contains("/beatmapsets/")) { //new urls with this format must include # to specify beatmap, or else its a set
if (!origURL.contains("#")) throw new IllegalArgumentException("Please specify a difficulty, instead of giving a set URL.");
- } else {
+ } else { //either /beatmap/ or old url /b/ /s/, latter of which we dont want
if (origURL.contains("/s/")) throw new IllegalArgumentException("Please specify a difficulty, instead of giving a set URL.");
id = id.split("&")[0]; //remove things like &m=0, which is no need
}
+
+ //fetch map
URLConnection mapURL;
try {
- mapURL = new URL("https://osu.ppy.sh/osu/" + id).openConnection();
- mapURL.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0)");
- } catch (IOException e) {
- //e.printStackTrace();
- throw new IllegalArgumentException("Invalid beatmap URL.");
- }
- try {
- return mapURL.getInputStream();
- } catch (IOException e) {
- if(e.getMessage().contains("503")) { //most likely cloudflare, unless they change the request method which is very unlikely
- mapURL = new URL("https://bloodcat.com/osu/b/" + id).openConnection();
- mapURL.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:57.0)");
- return mapURL.getInputStream(); //not catching this, if even this doesnt work just throw something went wrong.
- } else {
- throw e;
- }
+ mapURL = new URL("https://osu.ppy.sh/osu/" + id).openConnection(); //no longer has any IOExceptions, always return 200 no matter what
+ mapURL.setRequestProperty("User-Agent", "Mozilla/4.0");
+
+ InputStream stream = mapURL.getInputStream();
+
+ if (stream.available() < 4) throw new IOException(); //to fallback
+
+ return stream;
+ } catch (IOException e) { //try fallback regardless of exception reason
+ //fallback to bloodcat successor - much more convoluted and slower to fetch a single map now
+ //get map info
+ mapURL = new URL("https://api.chimu.moe/v1/map/" + id).openConnection();
+ mapURL.setRequestProperty("User-Agent", "Mozilla/4.0");
+
+ JSONObject mapData = new JSONObject(new JSONTokener(mapURL.getInputStream())).getJSONObject("data");
+
+ if(mapData.length() == 0) throw new IOException(e); //invalid beatmap
+
+ String mapName = mapData.getString("OsuFile").replaceAll("[\\\\/:*?\"<>|]", ""); //apparently the name returned isnt windows safe, so sanitize
+
+ //fetch beatmapset
+ mapURL = new URL("https://chimu.moe" + mapData.getString("DownloadPath")).openConnection();
+ mapURL.setRequestProperty("User-Agent", "Mozilla/4.0");
+
+ ZipInputStream set = new ZipInputStream(mapURL.getInputStream());
+
+ //iterate to get correct map
+ ZipEntry map = null;
+ while((map = set.getNextEntry()) != null) //assume always have the file specified in map info
+ if(map.getName().equals(mapName))
+ return set; //found at input stream current pointer
+
+ throw new IOException(e); //not catching this, if even this doesnt work just throw something went wrong.
}
}
}