aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorAaron <51387595+AzureAaron@users.noreply.github.com>2025-03-12 15:30:03 -0400
committerGitHub <noreply@github.com>2025-03-12 15:30:03 -0400
commit01f3891df785059adfd7e6e932afda9d5397199e (patch)
tree95d2dee23eb0b207b314ec0734793c1e9f2c6c59 /src/main/java
parent04091c51ea285f592d73695a3e05aa391e8a9c7f (diff)
downloadSkyblocker-01f3891df785059adfd7e6e932afda9d5397199e.tar.gz
Skyblocker-01f3891df785059adfd7e6e932afda9d5397199e.tar.bz2
Skyblocker-01f3891df785059adfd7e6e932afda9d5397199e.zip
Spirit Leap Overlay (#1202)
* Spirit Leap Overlay * Add support for dead players * Add support for offline players * Pre-fetch profile Also refactors ApiUtils to use a LoadingCache (similar to the YggdrasilMinecraftSessionService) which has proper support for multi threaded access and ensures that the value is only loaded once regardless of how many tries try at the same time.
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java8
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/HandledScreenProviderMixin.java10
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/LeapOverlay.java230
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonPlayerManager.java18
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/ApiUtils.java50
6 files changed, 293 insertions, 26 deletions
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 47abbb30..45356c01 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java
@@ -60,6 +60,14 @@ public class DungeonsCategory {
.controller(ConfigUtils::createBooleanController)
.build())
.option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("skyblocker.config.dungeons.spiritLeapOverlay"))
+ .description(OptionDescription.of(Text.translatable("skyblocker.config.dungeons.spiritLeapOverlay.@Tooltip")))
+ .binding(defaults.dungeons.spiritLeapOverlay,
+ () -> config.dungeons.spiritLeapOverlay,
+ newValue -> config.dungeons.spiritLeapOverlay = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
.name(Text.translatable("skyblocker.config.dungeons.starredMobGlow"))
.description(OptionDescription.of(Text.translatable("skyblocker.config.dungeons.starredMobGlow.@Tooltip")))
.binding(defaults.dungeons.starredMobGlow,
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java
index 27accd4a..4b2bf8c1 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java
@@ -22,6 +22,9 @@ public class DungeonsConfig {
public boolean classBasedPlayerGlow = true;
@SerialEntry
+ public boolean spiritLeapOverlay = true;
+
+ @SerialEntry
public boolean starredMobGlow = false;
@SerialEntry
diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenProviderMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenProviderMixin.java
index 9b47f736..cb5a2527 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenProviderMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenProviderMixin.java
@@ -1,10 +1,10 @@
package de.hysky.skyblocker.mixins;
-
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.auction.AuctionBrowserScreen;
import de.hysky.skyblocker.skyblock.auction.AuctionHouseScreenHandler;
import de.hysky.skyblocker.skyblock.auction.AuctionViewScreen;
+import de.hysky.skyblocker.skyblock.dungeon.LeapOverlay;
import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen;
import de.hysky.skyblocker.skyblock.item.SkyblockCraftingTableScreenHandler;
import de.hysky.skyblocker.skyblock.item.SkyblockCraftingTableScreen;
@@ -104,6 +104,14 @@ public interface HandledScreenProviderMixin<T extends ScreenHandler> {
ci.cancel();
}
+ // Leap Overlay
+ case GenericContainerScreenHandler containerScreenHandler when SkyblockerConfigManager.get().dungeons.spiritLeapOverlay && nameLowercase.contains(LeapOverlay.TITLE.toLowerCase()) -> {
+ client.player.currentScreenHandler = containerScreenHandler;
+ client.setScreen(new LeapOverlay(containerScreenHandler));
+
+ ci.cancel();
+ }
+
case null, default -> {}
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LeapOverlay.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LeapOverlay.java
new file mode 100644
index 00000000..59f19685
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LeapOverlay.java
@@ -0,0 +1,230 @@
+package de.hysky.skyblocker.skyblock.dungeon;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Supplier;
+
+import org.jetbrains.annotations.Nullable;
+import org.lwjgl.glfw.GLFW;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonPlayerManager;
+import de.hysky.skyblocker.utils.ItemUtils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.gui.widget.GridWidget;
+import net.minecraft.client.gui.widget.SimplePositioningWidget;
+import net.minecraft.client.realms.util.RealmsUtil;
+import net.minecraft.client.gui.widget.GridWidget.Adder;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.component.DataComponentTypes;
+import net.minecraft.component.type.ProfileComponent;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.screen.GenericContainerScreenHandler;
+import net.minecraft.screen.ScreenHandler;
+import net.minecraft.screen.ScreenHandlerListener;
+import net.minecraft.screen.slot.SlotActionType;
+import net.minecraft.text.Text;
+import net.minecraft.util.Colors;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.ColorHelper;
+
+public class LeapOverlay extends Screen implements ScreenHandlerListener {
+ public static final String TITLE = "Spirit Leap";
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+ private static final Identifier BUTTON = Identifier.of(SkyblockerMod.NAMESPACE, "button/button");
+ private static final Identifier BUTTON_HIGHLIGHTED = Identifier.of(SkyblockerMod.NAMESPACE, "button/button_highlighted");
+ private static final int BUTTON_SPACING = 8;
+ private static final float SCALE = 1.5f;
+ private static final int BUTTON_WIDTH = (int) (130f * SCALE);
+ private static final int BUTTON_HEIGHT = (int) (50f * SCALE);
+ /**
+ * Compares first by class name then by player name.
+ */
+ private static final Comparator<PlayerReference> COMPARATOR = Comparator.<PlayerReference, String>comparing(ref -> ref.dungeonClass().displayName())
+ .thenComparing(PlayerReference::name);
+ private final GenericContainerScreenHandler handler;
+ private final List<PlayerReference> references = new ArrayList<>();
+
+ public LeapOverlay(GenericContainerScreenHandler handler) {
+ super(Text.literal("Skyblocker Leap Overlay"));
+ this.handler = handler;
+ this.client = CLIENT; //Stops an NPE due to items being sent (and calling clearAndInit) before the main init method can initialize this field
+
+ //Listen for slot updates
+ handler.addListener(this);
+ }
+
+ @Override
+ protected void init() {
+ GridWidget gridWidget = new GridWidget();
+ gridWidget.setSpacing(BUTTON_SPACING);
+
+ Adder adder = gridWidget.createAdder(2);
+
+ for (PlayerReference reference : references) {
+ adder.add(new PlayerButton(0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, reference));
+ }
+
+ gridWidget.refreshPositions();
+ SimplePositioningWidget.setPos(gridWidget, 0, 0, this.width, this.height, 0.5f, 0.5f);
+ gridWidget.forEachChild(this::addDrawableChild);
+ }
+
+ @Override
+ public void onSlotUpdate(ScreenHandler handler, int slotId, ItemStack stack) {
+ int containerSlots = this.handler.getRows() * 9;
+
+ if (slotId < containerSlots && stack.isOf(Items.PLAYER_HEAD) && stack.contains(DataComponentTypes.PROFILE)) {
+ ProfileComponent profile = stack.get(DataComponentTypes.PROFILE);
+
+ //All heads in the leap menu have the id property set
+ if (profile.id().isEmpty()) return;
+
+ UUID uuid = profile.id().get();
+ //We take the name from the item because the name from the profile component can leave out _ characters for some reason?
+ String name = stack.getName().getString();
+ DungeonClass dungeonClass = DungeonPlayerManager.getClassFromPlayer(name);
+ PlayerStatus status = switch (ItemUtils.getConcatenatedLore(stack).toLowerCase(Locale.ENGLISH)) {
+ case String s when s.contains("dead") -> PlayerStatus.DEAD;
+ case String s when s.contains("offline") -> PlayerStatus.OFFLINE;
+ default -> null;
+ };
+
+ PlayerReference reference = new PlayerReference(uuid, name, dungeonClass, status, handler.syncId, slotId);
+ tryInsertReference(reference);
+ }
+ }
+
+ @Override
+ public void onPropertyUpdate(ScreenHandler handler, int property, int value) {}
+
+ /**
+ * Inserts the {@code reference} into the list if it doesn't exist or updates current value then updates the screen.
+ */
+ private void tryInsertReference(PlayerReference reference) {
+ Optional<PlayerReference> existing = references.stream()
+ .filter(ref -> ref.uuid().equals(reference.uuid()))
+ .findAny();
+
+ if (existing.isEmpty()) {
+ references.add(reference);
+ references.sort(COMPARATOR);
+
+ this.clearAndInit();
+ } else if (!existing.get().equals(reference)) {
+ references.remove(existing.get());
+ references.add(reference);
+ references.sort(COMPARATOR);
+
+ this.clearAndInit();
+ }
+ }
+
+ @Override
+ public void tick() {
+ super.tick();
+
+ if (!this.client.player.isAlive() || this.client.player.isRemoved()) {
+ this.client.player.closeHandledScreen();
+ }
+ }
+
+ @Override
+ public void close() {
+ this.client.player.closeHandledScreen();
+ super.close();
+ }
+
+ @Override
+ public void removed() {
+ if (this.client != null && this.client.player != null) {
+ this.handler.onClosed(this.client.player);
+ this.handler.removeListener(this);
+ }
+ }
+
+ private static class PlayerButton extends ButtonWidget {
+ private static final int BORDER_THICKNESS = 2;
+ private static final int HEAD_SIZE = 32;
+ private final PlayerReference reference;
+
+ private PlayerButton(int x, int y, int width, int height, PlayerReference reference) {
+ super(x, y, width, height, Text.empty(), b -> {}, ts -> Text.empty());
+ this.reference = reference;
+ }
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ Identifier texture = this.isSelected() ? BUTTON_HIGHLIGHTED : BUTTON;
+ context.drawGuiTexture(RenderLayer::getGuiTextured, texture, this.getX(), this.getY(), this.getWidth(), this.getHeight());
+
+ int baseX = this.getX() + BORDER_THICKNESS;
+ int centreX = this.getX() + (this.getWidth() >> 1);
+ int centreY = this.getY() + (this.getHeight() >> 1);
+
+ //Draw Player Head
+ RealmsUtil.drawPlayerHead(context, baseX + 4, centreY - (HEAD_SIZE >> 1), HEAD_SIZE, reference.uuid());
+
+ MatrixStack matrices = context.getMatrices();
+ int halfFontHeight = (int) (CLIENT.textRenderer.fontHeight * SCALE) >> 1;
+
+ //Draw class as heading
+ matrices.push();
+ matrices.translate(centreX, this.getY() + halfFontHeight, 0f);
+ matrices.scale(SCALE, SCALE, 1f);
+ context.drawCenteredTextWithShadow(CLIENT.textRenderer, reference.dungeonClass().displayName(), 0, 0, ColorHelper.fullAlpha(reference.dungeonClass().color()));
+ matrices.pop();
+
+ //Draw name next to head
+ matrices.push();
+ matrices.translate(baseX + HEAD_SIZE + 8, centreY - halfFontHeight, 0f);
+ matrices.scale(SCALE, SCALE, 1f);
+ context.drawTextWithShadow(CLIENT.textRenderer, Text.literal(reference.name()), 0, 0, Colors.WHITE);
+ matrices.pop();
+
+ if (reference.status() != null) {
+ //Text
+ matrices.push();
+ matrices.translate(centreX, this.getY() + this.getHeight() - (halfFontHeight * 3), 0f);
+ matrices.scale(SCALE, SCALE, 1f);
+ context.drawCenteredTextWithShadow(CLIENT.textRenderer, reference.status().text.get(), 0, 0, Colors.WHITE);
+ matrices.pop();
+
+ //Overlay
+ matrices.push();
+ matrices.scale(1f, 1f, 1f);
+ context.fill(this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight(), reference.status().overlayColor);
+ matrices.pop();
+ }
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY) {
+ CLIENT.interactionManager.clickSlot(reference.syncId(), reference.slotId(), GLFW.GLFW_MOUSE_BUTTON_LEFT, SlotActionType.PICKUP, CLIENT.player);
+ }
+ }
+
+ private record PlayerReference(UUID uuid, String name, DungeonClass dungeonClass, @Nullable PlayerStatus status, int syncId, int slotId) {}
+
+ private enum PlayerStatus {
+ DEAD(() -> Text.translatable("text.skyblocker.dead").withColor(Colors.RED), ColorHelper.withAlpha(64, Colors.LIGHT_RED)),
+ OFFLINE(() -> Text.translatable("text.skyblocker.offline").withColor(Colors.GRAY), ColorHelper.withAlpha(64, Colors.LIGHT_GRAY));
+
+ private final Supplier<Text> text;
+ private final int overlayColor;
+
+ PlayerStatus(Supplier<Text> text, int overlayColor) {
+ this.text = text;
+ this.overlayColor = overlayColor;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonPlayerManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonPlayerManager.java
index 4c3a0e8c..b00a09de 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonPlayerManager.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonPlayerManager.java
@@ -1,16 +1,22 @@
package de.hysky.skyblocker.skyblock.dungeon.secrets;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Range;
+import com.mojang.util.UndashedUuid;
+
import de.hysky.skyblocker.annotations.Init;
import de.hysky.skyblocker.events.DungeonEvents;
import de.hysky.skyblocker.skyblock.dungeon.DungeonClass;
import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListManager;
+import de.hysky.skyblocker.utils.ApiUtils;
import it.unimi.dsi.fastutil.objects.Object2ReferenceMap;
import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap;
+import net.minecraft.client.MinecraftClient;
import net.minecraft.entity.player.PlayerEntity;
public class DungeonPlayerManager {
@@ -26,8 +32,10 @@ public class DungeonPlayerManager {
}
public static DungeonClass getClassFromPlayer(PlayerEntity player) {
- String name = player.getGameProfile().getName();
+ return getClassFromPlayer(player.getGameProfile().getName());
+ }
+ public static DungeonClass getClassFromPlayer(String name) {
return PLAYER_CLASSES.getOrDefault(name, DungeonClass.UNKNOWN);
}
@@ -44,6 +52,14 @@ public class DungeonPlayerManager {
if (dungeonClass != DungeonClass.UNKNOWN) PLAYER_CLASSES.put(name, dungeonClass);
}
}
+
+ //Pre-fetch game profiles for rendering skins in the leap overlay
+ for (Object2ReferenceMap.Entry<String, DungeonClass> entry : PLAYER_CLASSES.object2ReferenceEntrySet()) {
+ CompletableFuture.runAsync(() -> {
+ UUID uuid = UndashedUuid.fromString(ApiUtils.name2Uuid(entry.getKey()));
+ MinecraftClient.getInstance().getSessionService().fetchProfile(uuid, false);
+ });
+ }
}
private static void reset() {
diff --git a/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java
index 6f60050f..6ce5dca1 100644
--- a/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java
+++ b/src/main/java/de/hysky/skyblocker/utils/ApiUtils.java
@@ -1,56 +1,58 @@
package de.hysky.skyblocker.utils;
+import java.util.concurrent.TimeUnit;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
import com.google.gson.JsonParser;
import com.mojang.util.UndashedUuid;
-import de.hysky.skyblocker.annotations.Init;
+
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;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/*
* 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.
+ /**
+ * Similar to how the Auth Lib caches GameProfiles.
*/
- private static final Object2ObjectOpenHashMap<String, String> NAME_2_UUID_CACHE = new Object2ObjectOpenHashMap<>();
-
- @Init
- public static void init() {
- //Clear cache every 20 minutes
- Scheduler.INSTANCE.scheduleCyclic(NAME_2_UUID_CACHE::clear, 24_000, true);
- }
+ private static final LoadingCache<String, String> NAME_2_UUID_CACHE = CacheBuilder.newBuilder()
+ .expireAfterWrite(20, TimeUnit.MINUTES)
+ .build(new CacheLoader<>() {
+ @Override
+ public String load(String key) throws Exception {
+ return name2UuidInternal(key, 0);
+ }
+ });
/**
* Multithreading is to be handled by the method caller
*/
public static String name2Uuid(String name) {
- return name2Uuid(name, 0);
+ return NAME_2_UUID_CACHE.getUnchecked(name);
}
- private static String name2Uuid(String name, int retries) {
+ private static String name2UuidInternal(String name, int retries) {
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);
+ if (session.getUsername().equalsIgnoreCase(name)) {
+ return UndashedUuid.toString(session.getUuidOrNull());
+ }
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;
+ return JsonParser.parseString(response.content()).getAsJsonObject().get("id").getAsString();
} else if (response.ratelimited() && retries < 3) {
Thread.sleep(800);
- return name2Uuid(name, ++retries);
+ return name2UuidInternal(name, ++retries);
}
} catch (Exception e) {
LOGGER.error("[Skyblocker] Name to uuid lookup failed! Name: {}", name, e);