diff options
Diffstat (limited to 'src/main/java')
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(); } } |