aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
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);