| 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
- Wed, Apr 1, 2:29 PM (38 m, 36 s)
- Storage Engine
- local-disk
- Storage Format
- Raw Data
- Storage Handle
- cc/5f/d8ee6d9a0b3c6edd71fbe385f099