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