Page MenuHomedesp's stash

Osu.java
No OneTemporary

Osu.java

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.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.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 static final String osuAPI = DiscordBot.tokens.getProperty("osu");
private static final DecimalFormat df = new DecimalFormat("#.##");
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) -> {
List<String> amend = new ArrayList<String>(Arrays.asList(words));
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 = wParamIndex != -1;
if(weight) {
try {
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(wParamIndex, wParamIndex + wParamLength).clear(); //remove
} catch (IndexOutOfBoundsException e) {
uid = getPlayer(user);
}
}
String initmap;
try {
initmap = amend.get(0);
} catch (IndexOutOfBoundsException e) {
initmap = "null"; //dud
}
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 -> 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();
//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
}
} else {
return new CommandResult(CommandResultType.FAILURE, "There is no account of your rich presence, 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!");
}
//parse beatmap
Map beatmap = null;
try {
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 (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 < 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 params specified.");
}
//compute pp
Accuracy acc = null;
JSONObject jbm = null;
try {
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());
}
//build embed
EmbedBuilder eb = new EmbedBuilder();
if(jbm.has("source") && !jbm.getString("source").isEmpty()) eb.setTitle("Source: " + jbm.getString("source"));
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", 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 {
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(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));
//remove retrieve msg if sent
if(msgId.isDone()) {
msgId.thenAccept(m -> channel.deleteMessageById(m).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) -> {
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 {
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 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 targetPP = params.size() > index ? Double.parseDouble(params.get(index)) : 1;
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);
if(temp > targetPP) {
maxPP = mid;
} else {
minPP = mid;
}
i++;
}
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.");
}
}, "[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)."));
//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; //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!");
}
//fetch recent plays
JSONArray array;
String name;
try {
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));
}
//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 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;
}
}
//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)."));
//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);
//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().setChartTitlePadding(12);
chart.getStyler().setChartPadding(12);
chart.getStyler().setChartBackgroundColor(new Color(200, 220, 230));
//plot each user's top plays onto chart
for(String name : split) {
List<Double> pps = new ArrayList<>();
try {
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 (IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
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.");
}
String id = "", set = "";
try {
new URL(words[0]);
} catch (MalformedURLException e) {
//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 = 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 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 { //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];
}
}
//get beatmap set info
InputStream stream = null;
try {
stream = new URL("https://osu.ppy.sh/api/get_beatmaps?k=" + osuAPI + "&s=" + id).openStream();
} catch (IOException ex) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(ex));
}
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; //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)) { //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);
for (int i = n; (n + 3 <= fobj.size() ? i < n + 3 : i < fobj.size()); i++) {
JSONObject json = fobj.get(i);
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", modes[json.getInt("mode")], true);
}
//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);
//fetch user data
JSONObject usr;
try {
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));
}
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[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);
//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("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);
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?
channel.sendMessage(em.build()).queue();
return new CommandResult(CommandResultType.SUCCESS);
}, "[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
}
//get user id then fetch top plays from XHR req
JSONArray array;
String name, id;
try {
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!");
}
array = new JSONArray(new JSONTokener(stream));
} catch (IOException e) {
return new CommandResult(CommandResultType.ERROR, ExceptionUtils.getStackTrace(e));
}
//build embed
EmbedBuilder eb = new EmbedBuilder();
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");
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("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!", "");
}
//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();
//>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("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 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(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) {
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
JSONObject obj = userBest.getJSONObject(i);
double v = obj.getDouble("pp");
String bid = obj.getString("beatmap_id");
if(bid.equals(ibid)) {
if(v >= pp) {
return -1;
} else {
change = v;
}
}
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;
}
//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));
if(i == pps.indexOf(pp)) {
net += w;
} else {
net += c*(Math.pow(0.95, i - 1)) * (0.95 - 1);
}
}
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;
}
//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(input);
id = input.substring(input.lastIndexOf("/") + 1);
} catch (MalformedURLException e) {
noturl = true;
id = input;
}
//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();
}
//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) {
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;
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.removeAll(Collections.singleton("-c")))
modeId = 2;
if(params.removeAll(Collections.singleton("-m")))
modeId = 3;
return modeId;
}
private static InputStream getMap(String origURL) throws IOException {
//get beatmap id
String id = origURL.substring(origURL.lastIndexOf("/") + 1);
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 { //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(); //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

Mime Type
text/x-java
Expires
Sun, Jul 6, 4:19 AM (13 h, 42 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
a7/5f/c660e40023a006e55a5d0d303454

Event Timeline