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 . | |
* | |
* <blockquote><pre> | |
* this is meant to be a single file library that's as portable and | |
* easy to set up as possible for java projects that need | |
* pp/difficulty calculation. | |
* | |
* when running the test suite, speed is roughly equivalent to the C | |
* implementation, but peak memory usage is almost 80 times higher. | |
* if you are on a system with limited resources or you don't want | |
* to spend time installing and setting up java, you can use the C | |
* implementation which doesn't depend on any third party software. | |
* ----------------------------------------------------------------- | |
* usage: | |
* put Koohii.java in your project's folder | |
* ----------------------------------------------------------------- | |
* import java.io.BufferedReader; | |
* import java.io.InputStreamReader; | |
* | |
* class Example { | |
* | |
* public static void main(String[] args) throws java.io.IOException | |
* { | |
* BufferedReader stdin = | |
* new BufferedReader(new InputStreamReader(System.in) | |
* ); | |
* | |
* Koohii.Map beatmap = new Koohii.Parser().map(stdin); | |
* Koohii.DiffCalc stars = new Koohii.DiffCalc().calc(beatmap); | |
* System.out.printf("%s stars\n", stars.total); | |
* | |
* Koohii.PPv2 pp = Koohii.PPv2( | |
* stars.aim, stars.speed, beatmap | |
* ); | |
* | |
* System.out.printf("%s pp\n", pp.total); | |
* } | |
* | |
* } | |
* ----------------------------------------------------------------- | |
* javac Example.java | |
* cat /path/to/file.osu | java Example | |
* ----------------------------------------------------------------- | |
* this is free and unencumbered software released into the | |
* public domain. | |
* | |
* refer to the attached UNLICENSE or http://unlicense.org/ | |
* </pre></blockquote> | |
* | |
* @author Franc[e]sco ([email protected]) | |
*/ | |
public final class Koohii { | |
private Koohii() {} | |
-public final int VERSION_MAJOR = 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<HitObject> objects = | |
new ArrayList<HitObject>(512); | |
public final ArrayList<Timing> tpoints = | |
new ArrayList<Timing>(32); | |
public Map() { reset(); } | |
/** clears the instance so that it can be reused. */ | |
public void reset() | |
{ | |
title = title_unicode = | |
artist = artist_unicode = | |
creator = | |
version = ""; | |
ncircles = nsliders = nspinners = 0; | |
hp = cs = od = ar = 5.0f; | |
sv = tick_rate = 1.0f; | |
objects.clear(); tpoints.clear(); | |
} | |
public String toString() | |
{ | |
StringBuilder sb = new StringBuilder(); | |
for (HitObject obj : objects) | |
{ | |
sb.append(obj); | |
sb.append(", "); | |
} | |
String objs_str = sb.toString(); | |
sb.setLength(0); | |
for (Timing t : tpoints) | |
{ | |
sb.append(t); | |
sb.append(", "); | |
} | |
String timing_str = sb.toString(); | |
return String.format( | |
"beatmap { mode=%d, title=%s, title_unicode=%s, " + | |
"artist=%s, artist_unicode=%s, creator=%s, " + | |
"version=%s, ncircles=%d, nsliders=%d, nspinners=%d," + | |
" hp=%s, cs=%s, od=%s, ar=%s, sv=%s, tick_rate=%s, " + | |
"tpoints=[ %s ], objects=[ %s ] }", | |
mode, title, title_unicode, artist, artist_unicode, | |
creator, version, ncircles, nsliders, nspinners, hp, | |
cs, od, ar, sv, tick_rate, timing_str, objs_str | |
); | |
} | |
+ //CUSTOM: added taiko handling method which is way less complicated than this | |
public int max_combo() | |
{ | |
- int res = 0; | |
- 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. | |
* | |
* <blockquote><pre> | |
* Koohii.MapStats mapstats = new Koohii.MapStats(); | |
* mapstats.ar = 9; | |
* Koohii.mods_apply(Koohii.MODS_DT, mapstats, Koohii.APPLY_AR); | |
* // mapstats.ar is now 10.33, mapstats.speed is 1.5 | |
* </pre></blockquote> | |
* | |
* @param mapstats the base beatmap stats | |
* @param flags bitmask that specifies which stats to modify. only | |
* the stats specified here need to be initialized in | |
* mapstats. | |
* @return mapstats | |
* @see MapStats | |
*/ | |
public static | |
MapStats mods_apply(int mods, MapStats mapstats, int flags) | |
{ | |
+ mapstats.speed = 1.0f; | |
+ | |
if ((mods & MODS_MAP_CHANGING) == 0) { | |
return mapstats; | |
} | |
if ((mods & (MODS_DT | MODS_NC)) != 0) { | |
mapstats.speed = 1.5f; | |
} | |
if ((mods & MODS_HT) != 0) { | |
mapstats.speed *= 0.75f; | |
} | |
float od_ar_hp_multiplier = 1.0f; | |
if ((mods & MODS_HR) != 0) { | |
od_ar_hp_multiplier = 1.4f; | |
} | |
if ((mods & MODS_EZ) != 0) { | |
od_ar_hp_multiplier *= 0.5f; | |
} | |
if ((flags & APPLY_AR) != 0) | |
{ | |
mapstats.ar *= od_ar_hp_multiplier; | |
/* convert AR into milliseconds window */ | |
double arms = mapstats.ar < 5.0f ? | |
AR0_MS - AR_MS_STEP1 * mapstats.ar | |
: AR5_MS - AR_MS_STEP2 * (mapstats.ar - 5.0f); | |
/* stats must be capped to 0-10 before HT/DT which brings | |
them to a range of -4.42->11.08 for OD and -5->11 for AR */ | |
arms = Math.min(AR0_MS, Math.max(AR10_MS, arms)); | |
arms /= mapstats.speed; | |
mapstats.ar = (float)( | |
arms > AR5_MS ? | |
(AR0_MS - arms) / AR_MS_STEP1 | |
: 5.0 + (AR5_MS - arms) / AR_MS_STEP2 | |
); | |
} | |
if ((flags & APPLY_OD) != 0) | |
{ | |
mapstats.od *= od_ar_hp_multiplier; | |
double odms = OD0_MS - Math.ceil(OD_MS_STEP * mapstats.od); | |
odms = Math.min(OD0_MS, Math.max(OD10_MS, odms)); | |
odms /= mapstats.speed; | |
mapstats.od = (float)((OD0_MS - odms) / OD_MS_STEP); | |
} | |
if ((flags & APPLY_CS) != 0) | |
{ | |
if ((mods & MODS_HR) != 0) { | |
mapstats.cs *= 1.3f; | |
} | |
if ((mods & MODS_EZ) != 0) { | |
mapstats.cs *= 0.5f; | |
} | |
mapstats.cs = Math.min(10.0f, mapstats.cs); | |
} | |
if ((flags & APPLY_HP) != 0) | |
{ | |
mapstats.hp = | |
Math.min(10.0f, mapstats.hp * od_ar_hp_multiplier); | |
} | |
return mapstats; | |
} | |
/* ------------------------------------------------------------- */ | |
/* difficulty calculator */ | |
/** | |
* arbitrary thresholds to determine when a stream is spaced | |
* enough that it becomes hard to alternate. | |
*/ | |
private final static double SINGLE_SPACING = 125.0; | |
/** strain decay per interval. */ | |
private final static double[] DECAY_BASE = { 0.3, 0.15 }; | |
/** balances speed and aim. */ | |
private final static double[] WEIGHT_SCALING = { 1400.0, 26.25 }; | |
/** | |
* max strains are weighted from highest to lowest, this is how | |
* much the weight decays. | |
*/ | |
private final static double DECAY_WEIGHT = 0.9; | |
/** | |
* strains are calculated by analyzing the map in chunks and taking | |
* the peak strains in each chunk. this is the length of a strain | |
* interval in milliseconds | |
*/ | |
private final static double STRAIN_STEP = 400.0; | |
/** non-normalized diameter where the small circle buff starts. */ | |
private final static double CIRCLESIZE_BUFF_THRESHOLD = 30.0; | |
/** global stars multiplier. */ | |
private final static double STAR_SCALING_FACTOR = 0.0675; | |
/** in osu! pixels */ | |
private final static double PLAYFIELD_WIDTH = 512.0, | |
PLAYFIELD_HEIGHT = 384.0; | |
private final static Vector2 PLAYFIELD_CENTER = new Vector2( | |
PLAYFIELD_WIDTH / 2.0, PLAYFIELD_HEIGHT / 2.0 | |
); | |
/** | |
* 50% of the difference between aim and speed is added to total | |
* star rating to compensate for aim/speed only maps | |
*/ | |
private final static double EXTREME_SCALING_FACTOR = 0.5; | |
private final static double MIN_SPEED_BONUS = 75.0; | |
private final static double MAX_SPEED_BONUS = 45.0; | |
private final static double ANGLE_BONUS_SCALE = 90.0; | |
private final static double AIM_TIMING_THRESHOLD = 107; | |
private final static double SPEED_ANGLE_BONUS_BEGIN = 5 * Math.PI / 6; | |
private final static double AIM_ANGLE_BONUS_BEGIN = Math.PI / 3; | |
private static | |
double d_spacing_weight(int type, double distance, double delta_time, | |
double prev_distance, double prev_delta_time, double angle) | |
{ | |
double strain_time = Math.max(delta_time, 50.0); | |
double prev_strain_time = Math.max(prev_delta_time, 50.0); | |
double angle_bonus; | |
switch (type) | |
{ | |
case DIFF_AIM: { | |
double result = 0.0; | |
if (!Double.isNaN(angle) && angle > AIM_ANGLE_BONUS_BEGIN) { | |
angle_bonus = Math.sqrt( | |
Math.max(prev_distance - ANGLE_BONUS_SCALE, 0.0) * | |
Math.pow(Math.sin(angle - AIM_ANGLE_BONUS_BEGIN), 2.0) * | |
Math.max(distance - ANGLE_BONUS_SCALE, 0.0) | |
); | |
result = ( | |
1.5 * Math.pow(Math.max(0.0, angle_bonus), 0.99) / | |
Math.max(AIM_TIMING_THRESHOLD, prev_strain_time) | |
); | |
} | |
double weighted_distance = Math.pow(distance, 0.99); | |
return Math.max(result + | |
weighted_distance / | |
Math.max(AIM_TIMING_THRESHOLD, strain_time), | |
weighted_distance / strain_time); | |
} | |
case DIFF_SPEED: { | |
distance = Math.min(distance, SINGLE_SPACING); | |
delta_time = Math.max(delta_time, MAX_SPEED_BONUS); | |
double speed_bonus = 1.0; | |
if (delta_time < MIN_SPEED_BONUS) { | |
speed_bonus += | |
Math.pow((MIN_SPEED_BONUS - delta_time) / 40.0, 2); | |
} | |
angle_bonus = 1.0; | |
if (!Double.isNaN(angle) && angle < SPEED_ANGLE_BONUS_BEGIN) { | |
double s = Math.sin(1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)); | |
angle_bonus += Math.pow(s, 2) / 3.57; | |
if (angle < Math.PI / 2.0) { | |
angle_bonus = 1.28; | |
if (distance < ANGLE_BONUS_SCALE && angle < Math.PI / 4.0) { | |
angle_bonus += (1.0 - angle_bonus) * | |
Math.min((ANGLE_BONUS_SCALE - distance) / 10.0, 1.0); | |
+ } else if (distance < ANGLE_BONUS_SCALE) { | |
+ angle_bonus += (1.0 - angle_bonus) * | |
+ Math.min((ANGLE_BONUS_SCALE - distance) / 10.0, 1.0) * | |
+ Math.sin((Math.PI / 2.0 - angle) * 4.0 / Math.PI); | |
} | |
- } else if (distance < ANGLE_BONUS_SCALE) { | |
- angle_bonus += (1.0 - angle_bonus) * | |
- Math.min((ANGLE_BONUS_SCALE - distance) / 10.0, 1.0) * | |
- Math.sin((Math.PI / 2.0 - angle) * 4.0 / Math.PI); | |
} | |
} | |
return ( | |
(1 + (speed_bonus - 1) * 0.75) * angle_bonus * | |
(0.95 + speed_bonus * Math.pow(distance / SINGLE_SPACING, 3.5)) | |
) / strain_time; | |
} | |
} | |
throw new UnsupportedOperationException( | |
"this difficulty type does not exist" | |
); | |
} | |
/** | |
* calculates the strain for one difficulty type and stores it in | |
* obj. this assumes that normpos is already computed. | |
* this also sets is_single if type is DIFF_SPEED | |
*/ | |
private static | |
void d_strain(int type, HitObject obj, HitObject prev, | |
double speed_mul) | |
{ | |
double value = 0.0; | |
double time_elapsed = (obj.time - prev.time) / speed_mul; | |
double decay = | |
Math.pow(DECAY_BASE[type], time_elapsed / 1000.0); | |
obj.delta_time = time_elapsed; | |
/* this implementation doesn't account for sliders */ | |
if ((obj.type & (OBJ_SLIDER | OBJ_CIRCLE)) != 0) | |
{ | |
double distance = | |
new Vector2(obj.normpos).sub(prev.normpos).len(); | |
obj.d_distance = distance; | |
if (type == DIFF_SPEED) { | |
obj.is_single = distance > SINGLE_SPACING; | |
} | |
value = d_spacing_weight(type, distance, time_elapsed, | |
prev.d_distance, prev.delta_time, obj.angle); | |
value *= WEIGHT_SCALING[type]; | |
} | |
obj.strains[type] = prev.strains[type] * decay + value; | |
} | |
/** | |
* difficulty calculator, can be reused in subsequent calc() calls. | |
*/ | |
public static class DiffCalc | |
{ | |
/** star rating. */ | |
public double total; | |
/** aim stars. */ | |
public double aim; | |
/** aim difficulty (used to calc length bonus) */ | |
public double aim_difficulty; | |
/** aim length bonus (unused at the moment) */ | |
public double aim_length_bonus; | |
/** speed stars. */ | |
public double speed; | |
/** speed difficulty (used to calc length bonus) */ | |
public double speed_difficulty; | |
/** speed length bonus (unused at the moment) */ | |
public double speed_length_bonus; | |
/** | |
* number of notes that are considered singletaps by the | |
* difficulty calculator. | |
*/ | |
public int nsingles; | |
/** | |
* number of taps slower or equal to the singletap threshold | |
* value. | |
*/ | |
public int nsingles_threshold; | |
/** | |
* the beatmap we want to calculate the difficulty for. | |
* must be set or passed to calc() explicitly. | |
* persists across calc() calls unless it's changed or explicity | |
* passed to calc() | |
* @see DiffCalc#calc(Koohii.Map, int, double) | |
* @see DiffCalc#calc(Koohii.Map, int) | |
* @see DiffCalc#calc(Koohii.Map) | |
*/ | |
public Map beatmap = null; | |
private double speed_mul; | |
private final ArrayList<Double> strains = | |
new ArrayList<Double>(512); | |
public DiffCalc() { reset(); } | |
/** sets up the instance for re-use by resetting fields. */ | |
private void reset() | |
{ | |
total = aim = speed = 0.0; | |
nsingles = nsingles_threshold = 0; | |
speed_mul = 1.0; | |
} | |
public String toString() | |
{ | |
return String.format("%s stars (%s aim, %s speed)", | |
total, aim, speed); | |
} | |
private static double length_bonus(double stars, double difficulty) { | |
return ( | |
0.32 + 0.5 * | |
(Math.log10(difficulty + stars) - Math.log10(stars)) | |
); | |
} | |
private class DiffValues | |
{ | |
public double difficulty, total; | |
public DiffValues(double difficulty, double total) { | |
this.difficulty = difficulty; | |
this.total = total; | |
} | |
}; | |
private DiffValues calc_individual(int type) | |
{ | |
strains.clear(); | |
double strain_step = STRAIN_STEP * speed_mul; | |
- 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<Integer, String> 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 = "<subcommands>"; | |
//TODO add a command to parse replays? | |
registerSubCommand("pp", Arrays.asList("map"), (channel, user, msg, words) -> { | |
- //OffsetDateTime timesent = OffsetDateTime.now(); | |
List<String> amend = new ArrayList<String>(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<String> mid = new ArrayList<String>(); | |
- if (!initmap.startsWith("https://") && !initmap.startsWith("http://")) { //check if no map input, use discord rich presence | |
+ | |
+ CompletableFuture<String> 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<String> 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<String> 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<String> 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<String> 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<Double> 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<JSONObject> obj = new ArrayList<JSONObject>(); | |
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<JSONObject> 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); | |
}, "<beatmap set URL/search words> [| 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<String> 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<String> 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<Entry<String, String>> 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<Entry<String, String>> 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("<b>", "").replaceAll("</b>", "")) | |
- .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<MessageEmbed> 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<Double> datas = Arrays.asList(dataset).stream().map(s -> Double.parseDouble(s)).collect(Collectors.toList()); | |
- | |
double net = 0, change = -1; | |
List<Double> 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<String> 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. | |
} | |
} | |
} |
File Metadata
File Metadata
- Mime Type
- text/x-diff
- Expires
- Thu, May 8, 7:39 PM (1 d, 1 m)
- Storage Engine
- local-disk
- Storage Format
- Raw Data
- Storage Handle
- 65/62/317d49b4765c9d9a5fd162260431