aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker
diff options
context:
space:
mode:
authorAaron <51387595+AzureAaron@users.noreply.github.com>2023-10-31 01:08:28 -0400
committerGitHub <noreply@github.com>2023-10-31 01:08:28 -0400
commit5bb91104d3275283d7479f0b35c1b18be470d632 (patch)
treefd19087bdc6ffdcecfbb492763efa65fb52379cc /src/main/java/de/hysky/skyblocker
parent1389107c648a5b1d8395d6629cf8328a3ca67324 (diff)
downloadSkyblocker-5bb91104d3275283d7479f0b35c1b18be470d632.tar.gz
Skyblocker-5bb91104d3275283d7479f0b35c1b18be470d632.tar.bz2
Skyblocker-5bb91104d3275283d7479f0b35c1b18be470d632.zip
Player Secrets Tracker (#394)
* Player Secrets Tracker * Refactor SecretsTracker --------- Co-authored-by: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>
Diffstat (limited to 'src/main/java/de/hysky/skyblocker')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java174
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPlayerWidget.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ApiUtils.java53
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Http.java22
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/Utils.java7
9 files changed, 263 insertions, 14 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index 5178a777..b398e9b6 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -7,6 +7,7 @@ import de.hysky.skyblocker.skyblock.*;
import de.hysky.skyblocker.skyblock.diana.MythologicalRitual;
import de.hysky.skyblocker.skyblock.dungeon.*;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonSecrets;
+import de.hysky.skyblocker.skyblock.dungeon.secrets.SecretsTracker;
import de.hysky.skyblocker.skyblock.dwarven.DwarvenHud;
import de.hysky.skyblocker.skyblock.item.*;
import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview;
@@ -20,6 +21,7 @@ import de.hysky.skyblocker.skyblock.spidersden.Relics;
import de.hysky.skyblocker.skyblock.tabhud.TabHud;
import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.ScreenMaster;
import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.utils.ApiUtils;
import de.hysky.skyblocker.utils.NEURepoManager;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.chat.ChatMessageListener;
@@ -109,6 +111,8 @@ public class SkyblockerMod implements ClientModInitializer {
CreeperBeams.init();
ItemRarityBackgrounds.init();
MuseumItemCache.init();
+ SecretsTracker.init();
+ ApiUtils.init();
containerSolverManager.init();
statusBarTracker.init();
Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20);
diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
index 15017995..68520cab 100644
--- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
@@ -552,6 +552,9 @@ public class SkyblockerConfig {
@SerialEntry
public int mapY = 2;
+
+ @SerialEntry
+ public boolean playerSecretsTracker = false;
@SerialEntry
public boolean starredMobGlow = true;
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java
index fdb13892..02913a28 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java
@@ -242,6 +242,14 @@ public class DungeonsCategory {
.controller(FloatFieldControllerBuilder::create)
.build())
.option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretsTracker"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretsTracker.@Tooltip")))
+ .binding(defaults.locations.dungeons.playerSecretsTracker,
+ () -> config.locations.dungeons.playerSecretsTracker,
+ newValue -> config.locations.dungeons.playerSecretsTracker = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
.name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.starredMobGlow"))
.description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.starredMobGlow.@Tooltip")))
.binding(defaults.locations.dungeons.starredMobGlow,
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java
new file mode 100644
index 00000000..0690952e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java
@@ -0,0 +1,174 @@
+package de.hysky.skyblocker.skyblock.dungeon.secrets;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.DungeonPlayerWidget;
+import de.hysky.skyblocker.utils.ApiUtils;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.Http;
+import de.hysky.skyblocker.utils.Http.ApiResponse;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import it.unimi.dsi.fastutil.objects.Object2IntMap.Entry;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.text.HoverEvent;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Tracks the amount of secrets players get every run
+ */
+public class SecretsTracker {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SecretsTracker.class);
+ private static final Pattern TEAM_SCORE_PATTERN = Pattern.compile(" +Team Score: [0-9]+ \\([A-z+]+\\)");
+
+ private static volatile TrackedRun currentRun = null;
+ private static volatile TrackedRun lastRun = null;
+ private static volatile long lastRunEnded = 0L;
+
+ public static void init() {
+ ClientReceiveMessageEvents.GAME.register(SecretsTracker::onMessage);
+ }
+
+ //If -1 is somehow encountered, it would be very rare, so I just disregard its possibility for now
+ //people would probably recognize if it was inaccurate so yeah
+ private static void calculate(RunPhase phase) {
+ switch (phase) {
+ case START -> CompletableFuture.runAsync(() -> {
+ TrackedRun newlyStartedRun = new TrackedRun();
+
+ //Initialize players in new run
+ for (int i = 0; i < 5; i++) {
+ String playerName = getPlayerNameAt(i + 1);
+
+ //The player name will be blank if there isn't a player at that index
+ if (!playerName.isEmpty()) {
+
+ //If the player was a part of the last run (and didn't have -1 secret count) and that run ended less than 5 mins ago then copy the secrets over
+ if (lastRun != null && System.currentTimeMillis() <= lastRunEnded + 300_000 && lastRun.secretCounts().getOrDefault(playerName, -1) != -1) {
+ newlyStartedRun.secretCounts().put(playerName, lastRun.secretCounts().getInt(playerName));
+ } else {
+ newlyStartedRun.secretCounts().put(playerName, getPlayerSecrets(playerName).leftInt());
+ }
+ }
+ }
+
+ currentRun = newlyStartedRun;
+ });
+
+ case END -> CompletableFuture.runAsync(() -> {
+ //In case the game crashes from something
+ if (currentRun != null) {
+ Object2ObjectOpenHashMap<String, IntIntPair> secretsFound = new Object2ObjectOpenHashMap<>();
+
+ //Update secret counts
+ for (Entry<String> entry : currentRun.secretCounts().object2IntEntrySet()) {
+ String playerName = entry.getKey();
+ int startingSecrets = entry.getIntValue();
+ IntIntPair secretsNow = getPlayerSecrets(playerName);
+ int secretsPlayerFound = secretsNow.leftInt() - startingSecrets;
+
+ secretsFound.put(playerName, IntIntPair.of(secretsPlayerFound, secretsNow.rightInt()));
+ entry.setValue(secretsNow.leftInt());
+ }
+
+ //Print the results all in one go, so its clean and less of a chance of it being broken up
+ for (Map.Entry<String, IntIntPair> entry : secretsFound.entrySet()) {
+ sendResultMessage(entry.getKey(), entry.getValue().leftInt(), entry.getValue().rightInt(), true);
+ }
+
+ //Swap the current and last run as well as mark the run end time
+ lastRunEnded = System.currentTimeMillis();
+ lastRun = currentRun;
+ currentRun = null;
+ } else {
+ sendResultMessage(null, -1, -1, false);
+ }
+ });
+ }
+ }
+
+ private static void sendResultMessage(String player, int secrets, int cacheAge, boolean success) {
+ PlayerEntity playerEntity = MinecraftClient.getInstance().player;
+ if (playerEntity != null) {
+ if (success) {
+ playerEntity.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secretsTracker.feedback", Text.literal(player).styled(Constants.WITH_COLOR.apply(0xf57542)), "ยง7" + secrets, getCacheText(cacheAge))));
+ } else {
+ playerEntity.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secretsTracker.failFeedback")));
+ }
+ }
+ }
+
+ private static Text getCacheText(int cacheAge) {
+ return Text.literal("\u2139").styled(style -> style.withColor(cacheAge == -1 ? 0x218bff : 0xeac864).withHoverEvent(
+ new HoverEvent(HoverEvent.Action.SHOW_TEXT, cacheAge == -1 ? Text.translatable("skyblocker.api.cache.MISS") : Text.translatable("skyblocker.api.cache.HIT", cacheAge))));
+ }
+
+ private static void onMessage(Text text, boolean overlay) {
+ if (Utils.isInDungeons() && SkyblockerConfigManager.get().locations.dungeons.playerSecretsTracker) {
+ String message = Formatting.strip(text.getString());
+
+ try {
+ if (message.equals("[NPC] Mort: Here, I found this map when I first entered the dungeon.")) calculate(RunPhase.START);
+ if (TEAM_SCORE_PATTERN.matcher(message).matches()) calculate(RunPhase.END);
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Encountered an unknown error while trying to track player secrets!", e);
+ }
+ }
+ }
+
+ private static String getPlayerNameAt(int index) {
+ Matcher matcher = PlayerListMgr.regexAt(1 + (index - 1) * 4, DungeonPlayerWidget.PLAYER_PATTERN);
+
+ return matcher != null ? matcher.group("name") : "";
+ }
+
+ private static IntIntPair getPlayerSecrets(String name) {
+ String uuid = ApiUtils.name2Uuid(name);
+
+ if (!uuid.isEmpty()) {
+ try (ApiResponse response = Http.sendHypixelRequest("player", "?uuid=" + uuid)) {
+ return IntIntPair.of(getSecretCountFromAchievements(JsonParser.parseString(response.content()).getAsJsonObject()), response.age());
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Encountered an error while trying to fetch {} secret count!", name + "'s", e);
+ }
+ }
+
+ return IntIntPair.of(-1, -1);
+ }
+
+ /**
+ * Gets a player's secret count from their hypixel achievements
+ */
+ private static int getSecretCountFromAchievements(JsonObject playerJson) {
+ JsonObject player = playerJson.get("player").getAsJsonObject();
+ JsonObject achievements = (player.has("achievements")) ? player.get("achievements").getAsJsonObject() : null;
+ return (achievements != null && achievements.has("skyblock_treasure_hunter")) ? achievements.get("skyblock_treasure_hunter").getAsInt() : 0;
+ }
+
+ /**
+ * This will either reflect the value at the start or the end depending on when this is called
+ */
+ private record TrackedRun(Object2IntOpenHashMap<String> secretCounts) {
+ private TrackedRun() {
+ this(new Object2IntOpenHashMap<>());
+ }
+ }
+
+ private enum RunPhase {
+ START, END
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java
index cb11d702..ac9b1bf0 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java
@@ -73,9 +73,7 @@ public class MuseumItemCache {
private static void updateData4ProfileMember(String uuid, String profileId) {
CompletableFuture.runAsync(() -> {
- try {
- ApiResponse response = Http.sendHypixelRequest("skyblock/museum", "?profile=" + profileId);
-
+ try (ApiResponse response = Http.sendHypixelRequest("skyblock/museum", "?profile=" + profileId)) {
//The request was successful
if (response.ok()) {
JsonObject profileData = JsonParser.parseString(response.content()).getAsJsonObject();
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPlayerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPlayerWidget.java
index be1a3c6e..d71eb190 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPlayerWidget.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/tabhud/widget/DungeonPlayerWidget.java
@@ -27,7 +27,7 @@ public class DungeonPlayerWidget extends Widget {
// group 3: level (or nothing, if pre dungeon start)
// this regex filters out the ironman icon as well as rank prefixes and emblems
// \[\d*\] (?:\[[A-Za-z]+\] )?(?<name>[A-Za-z0-9_]*) (?:.* )?\((?<class>\S*) ?(?<level>[LXVI]*)\)
- private static final Pattern PLAYER_PATTERN = Pattern
+ public static final Pattern PLAYER_PATTERN = Pattern
.compile("\\[\\d*\\] (?:\\[[A-Za-z]+\\] )?(?<name>[A-Za-z0-9_]*) (?:.* )?\\((?<class>\\S*) ?(?<level>[LXVI]*)\\)");
private static final HashMap<String, ItemStack> ICOS = new HashMap<>();
diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java
new file mode 100644
index 00000000..c0648eba
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java
@@ -0,0 +1,53 @@
+package de.hysky.skyblocker.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.JsonParser;
+import com.mojang.util.UndashedUuid;
+
+import de.hysky.skyblocker.utils.Http.ApiResponse;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.session.Session;
+
+/*
+ * Contains only basic helpers for using Http APIs
+ */
+public class ApiUtils {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ApiUtils.class);
+ /**
+ * Do not iterate over this map, it will be accessed and modified by multiple threads.
+ */
+ private static final Object2ObjectOpenHashMap<String, String> NAME_2_UUID_CACHE = new Object2ObjectOpenHashMap<>();
+
+ public static void init() {
+ //Clear cache every 20 minutes
+ Scheduler.INSTANCE.scheduleCyclic(NAME_2_UUID_CACHE::clear, 24_000, true);
+ }
+
+ /**
+ * Multithreading is to be handled by the method caller
+ */
+ public static String name2Uuid(String name) {
+ Session session = MinecraftClient.getInstance().getSession();
+
+ if (session.getUsername().equals(name)) return UndashedUuid.toString(session.getUuidOrNull());
+ if (NAME_2_UUID_CACHE.containsKey(name)) return NAME_2_UUID_CACHE.get(name);
+
+ try (ApiResponse response = Http.sendName2UuidRequest(name)) {
+ if (response.ok()) {
+ String uuid = JsonParser.parseString(response.content()).getAsJsonObject().get("id").getAsString();
+
+ NAME_2_UUID_CACHE.put(name, uuid);
+
+ return uuid;
+ }
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Name to uuid lookup failed! Name: {}", name, e);
+ }
+
+ return "";
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Http.java b/src/main/java/de/hysky/skyblocker/utils/Http.java
index eabb02e4..573c3458 100644
--- a/src/main/java/de/hysky/skyblocker/utils/Http.java
+++ b/src/main/java/de/hysky/skyblocker/utils/Http.java
@@ -49,9 +49,11 @@ public class Http {
HttpResponse<InputStream> response = HTTP_CLIENT.send(request, BodyHandlers.ofInputStream());
InputStream decodedInputStream = getDecodedInputStream(response);
+
String body = new String(decodedInputStream.readAllBytes());
+ HttpHeaders headers = response.headers();
- return new ApiResponse(body, response.statusCode(), getCacheStatus(response.headers()));
+ return new ApiResponse(body, response.statusCode(), getCacheStatus(headers), getAge(headers));
}
public static HttpHeaders sendHeadRequest(String url) throws IOException, InterruptedException {
@@ -66,8 +68,8 @@ public class Http {
return response.headers();
}
- public static String sendName2UuidRequest(String name) throws IOException, InterruptedException {
- return sendGetRequest(NAME_2_UUID + name);
+ public static ApiResponse sendName2UuidRequest(String name) throws IOException, InterruptedException {
+ return sendCacheableGetRequest(NAME_2_UUID + name);
}
/**
@@ -115,12 +117,16 @@ public class Http {
*
* @see <a href="https://developers.cloudflare.com/cache/concepts/default-cache-behavior/#cloudflare-cache-responses">Cloudflare Cache Docs</a>
*/
- public static String getCacheStatus(HttpHeaders headers) {
+ private static String getCacheStatus(HttpHeaders headers) {
return headers.firstValue("CF-Cache-Status").orElse("UNKNOWN");
}
+ private static int getAge(HttpHeaders headers) {
+ return Integer.parseInt(headers.firstValue("Age").orElse("-1"));
+ }
+
//TODO If ever needed, we could just replace cache status with the response headers and go from there
- public record ApiResponse(String content, int statusCode, String cacheStatus) {
+ public record ApiResponse(String content, int statusCode, String cacheStatus, int age) implements AutoCloseable {
public boolean ok() {
return statusCode == 200;
@@ -129,5 +135,11 @@ public class Http {
public boolean cached() {
return cacheStatus.equals("HIT");
}
+
+ @Override
+ public void close() {
+ //Allows for nice syntax when dealing with api requests in try catch blocks
+ //Maybe one day we'll have some resources to free
+ }
}
}
diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java
index b1a347f9..71e08865 100644
--- a/src/main/java/de/hysky/skyblocker/utils/Utils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java
@@ -33,10 +33,10 @@ import java.util.List;
public class Utils {
private static final Logger LOGGER = LoggerFactory.getLogger(Utils.class);
private static final String ALTERNATE_HYPIXEL_ADDRESS = System.getProperty("skyblocker.alternateHypixelAddress", "");
+ private static final String DUNGEONS_LOCATION = "dungeon";
private static final String PROFILE_PREFIX = "Profile: ";
private static boolean isOnHypixel = false;
private static boolean isOnSkyblock = false;
- private static boolean isInDungeons = false;
private static boolean isInjected = false;
/**
* The profile name parsed from the player list.
@@ -79,7 +79,7 @@ public class Utils {
}
public static boolean isInDungeons() {
- return isInDungeons;
+ return getLocationRaw().equals(DUNGEONS_LOCATION) || FabricLoader.getInstance().isDevelopmentEnvironment();
}
public static boolean isInTheRift() {
@@ -164,7 +164,6 @@ public class Utils {
sidebar = Collections.emptyList();
} else {
isOnSkyblock = false;
- isInDungeons = false;
return;
}
}
@@ -188,7 +187,6 @@ public class Utils {
} else {
onLeaveSkyblock();
}
- isInDungeons = fabricLoader.isDevelopmentEnvironment() || isOnSkyblock && string.contains("The Catacombs");
} else if (isOnHypixel) {
isOnHypixel = false;
onLeaveSkyblock();
@@ -205,7 +203,6 @@ public class Utils {
private static void onLeaveSkyblock() {
if (isOnSkyblock) {
isOnSkyblock = false;
- isInDungeons = false;
SkyblockEvents.LEAVE.invoker().onSkyblockLeave();
}
}