aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/hysky')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java4
-rw-r--r--src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java17
-rw-r--r--src/main/java/de/hysky/skyblocker/mixin/HandledScreenProviderMixin.java12
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreen.java194
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreenHandler.java69
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java222
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java24
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java31
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java444
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/InstancedUtils.java104
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java50
-rw-r--r--src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java2
14 files changed, 1170 insertions, 11 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index 9e613431..639b340f 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -26,6 +26,7 @@ import de.hysky.skyblocker.skyblock.garden.FarmingHud;
import de.hysky.skyblocker.skyblock.garden.LowerSensitivity;
import de.hysky.skyblocker.skyblock.garden.VisitorHelper;
import de.hysky.skyblocker.skyblock.item.*;
+import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper;
import de.hysky.skyblocker.skyblock.item.tooltip.BackpackPreview;
import de.hysky.skyblocker.skyblock.item.tooltip.ItemTooltip;
import de.hysky.skyblocker.skyblock.itemlist.ItemRepository;
@@ -39,6 +40,7 @@ import de.hysky.skyblocker.skyblock.tabhud.screenbuilder.ScreenMaster;
import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
import de.hysky.skyblocker.skyblock.waypoint.FairySouls;
import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual;
+import de.hysky.skyblocker.skyblock.waypoint.OrderedWaypoints;
import de.hysky.skyblocker.skyblock.waypoint.Relics;
import de.hysky.skyblocker.utils.ApiUtils;
import de.hysky.skyblocker.utils.NEURepoManager;
@@ -103,10 +105,12 @@ public class SkyblockerMod implements ClientModInitializer {
PlayerHeadHashCache.init();
HotbarSlotLock.init();
ItemTooltip.init();
+ AccessoriesHelper.init();
WikiLookup.init();
FairySouls.init();
Relics.init();
MythologicalRitual.init();
+ OrderedWaypoints.init();
BackpackPreview.init();
QuickNav.init();
ItemCooldowns.init();
diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
index 29678683..06ac748a 100644
--- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
@@ -160,6 +160,9 @@ public class SkyblockerConfig {
public boolean betterPartyFinder = true;
@SerialEntry
+ public boolean fancyCraftingTable = true;
+
+ @SerialEntry
public boolean backpackPreviewWithoutShift = false;
@SerialEntry
@@ -558,6 +561,9 @@ public class SkyblockerConfig {
@SerialEntry
public boolean enableExoticTooltip = true;
+
+ @SerialEntry
+ public boolean enableAccessoriesHelper = true;
}
public static class ItemInfoDisplay {
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
index fe73a6a7..e310cb85 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/GeneralCategory.java
@@ -42,6 +42,13 @@ public class GeneralCategory {
.controller(ConfigUtils::createBooleanController)
.build())
.option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.fancyCraftingTable"))
+ .binding(defaults.general.fancyCraftingTable,
+ () -> config.general.fancyCraftingTable,
+ newValue -> config.general.fancyCraftingTable = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
+ .option(Option.<Boolean>createBuilder()
.name(Text.translatable("text.autoconfig.skyblocker.option.general.backpackPreviewWithoutShift"))
.binding(defaults.general.backpackPreviewWithoutShift,
() -> config.general.backpackPreviewWithoutShift,
@@ -442,6 +449,16 @@ public class GeneralCategory {
newValue -> config.general.itemTooltip.enableExoticTooltip = newValue)
.controller(ConfigUtils::createBooleanController)
.build())
+ .option(Option.<Boolean>createBuilder()
+ .name(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper"))
+ .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[0]"), Text.literal("\n\n✔ Collected").formatted(Formatting.GREEN), Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[1]"),
+ Text.literal("\n✦ Upgrade").withColor(0x218bff), Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[2]"), Text.literal("\n↑ Upgradable").withColor(0xf8d048), Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[3]"),
+ Text.literal("\n↓ Downgrade").formatted(Formatting.GRAY), Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[4]"), Text.literal("\n✖ Missing").formatted(Formatting.RED), Text.translatable("text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[5]")))
+ .binding(defaults.general.itemTooltip.enableAccessoriesHelper,
+ () -> config.general.itemTooltip.enableAccessoriesHelper,
+ newValue -> config.general.itemTooltip.enableAccessoriesHelper = newValue)
+ .controller(ConfigUtils::createBooleanController)
+ .build())
.build())
//Item Info Display
diff --git a/src/main/java/de/hysky/skyblocker/mixin/HandledScreenProviderMixin.java b/src/main/java/de/hysky/skyblocker/mixin/HandledScreenProviderMixin.java
index 94eb53a5..975c9c51 100644
--- a/src/main/java/de/hysky/skyblocker/mixin/HandledScreenProviderMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixin/HandledScreenProviderMixin.java
@@ -3,6 +3,9 @@ package de.hysky.skyblocker.mixin;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.dungeon.partyfinder.PartyFinderScreen;
+import de.hysky.skyblocker.skyblock.item.SkyblockCraftingTableScreenHandler;
+import de.hysky.skyblocker.skyblock.item.SkyblockCraftingTableScreen;
+import de.hysky.skyblocker.utils.Utils;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.ingame.HandledScreens;
import net.minecraft.client.network.ClientPlayerEntity;
@@ -19,11 +22,11 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public interface HandledScreenProviderMixin<T extends ScreenHandler> {
@Inject(method = "open", at = @At("HEAD"), cancellable = true)
default void skyblocker$open(Text name, ScreenHandlerType<T> type, MinecraftClient client, int id, CallbackInfo ci) {
- if (!SkyblockerConfigManager.get().general.betterPartyFinder) return;
ClientPlayerEntity player = client.player;
if (player == null) return;
+ if (!Utils.isOnSkyblock()) return;
T screenHandler = type.create(id, player.getInventory());
- if (screenHandler instanceof GenericContainerScreenHandler containerScreenHandler && PartyFinderScreen.possibleInventoryNames.contains(name.getString().toLowerCase())) {
+ if (SkyblockerConfigManager.get().general.betterPartyFinder && screenHandler instanceof GenericContainerScreenHandler containerScreenHandler && PartyFinderScreen.possibleInventoryNames.contains(name.getString().toLowerCase())) {
if (client.currentScreen != null) {
String lowerCase = client.currentScreen.getTitle().getString().toLowerCase();
if (lowerCase.contains("group builder")) return;
@@ -42,6 +45,11 @@ public interface HandledScreenProviderMixin<T extends ScreenHandler> {
}
ci.cancel();
+ } else if (SkyblockerConfigManager.get().general.fancyCraftingTable && screenHandler instanceof GenericContainerScreenHandler containerScreenHandler && name.getString().toLowerCase().contains("craft item")) {
+ SkyblockCraftingTableScreenHandler skyblockCraftingTableScreenHandler = new SkyblockCraftingTableScreenHandler(containerScreenHandler, player.getInventory());
+ client.player.currentScreenHandler = skyblockCraftingTableScreenHandler;
+ client.setScreen(new SkyblockCraftingTableScreen(skyblockCraftingTableScreenHandler, player.getInventory(), Text.literal("Craft Item")));
+ ci.cancel();
}
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java
index 509f79b7..639e98ed 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java
@@ -73,7 +73,7 @@ public class CustomArmorDyeColors {
return Command.SINGLE_SUCCESS;
}
- private static boolean isHexadecimalColor(String s) {
+ public static boolean isHexadecimalColor(String s) {
return s.replace("#", "").chars().allMatch(c -> "0123456789ABCDEFabcdef".indexOf(c) >= 0) && s.replace("#", "").length() == 6;
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreen.java
new file mode 100644
index 00000000..14ddb238
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreen.java
@@ -0,0 +1,194 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.skyblock.itemlist.ItemListWidget;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ButtonTextures;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
+import net.minecraft.client.gui.tooltip.Tooltip;
+import net.minecraft.client.gui.widget.TexturedButtonWidget;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.inventory.SimpleInventory;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.recipe.Recipe;
+import net.minecraft.recipe.RecipeEntry;
+import net.minecraft.recipe.RecipeMatcher;
+import net.minecraft.recipe.book.RecipeBookCategory;
+import net.minecraft.screen.AbstractRecipeScreenHandler;
+import net.minecraft.screen.ScreenHandlerType;
+import net.minecraft.screen.slot.Slot;
+import net.minecraft.screen.slot.SlotActionType;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+
+public class SkyblockCraftingTableScreen extends HandledScreen<SkyblockCraftingTableScreenHandler> {
+ private static final Identifier TEXTURE = new Identifier("textures/gui/container/crafting_table.png");
+ protected static final ButtonTextures MORE_CRAFTS_TEXTURES = new ButtonTextures(
+ new Identifier(SkyblockerMod.NAMESPACE, "quick_craft/more_button"),
+ new Identifier(SkyblockerMod.NAMESPACE, "quick_craft/more_button_disabled"),
+ new Identifier(SkyblockerMod.NAMESPACE, "quick_craft/more_button_highlighted")
+ );
+
+ protected static final Identifier QUICK_CRAFT = new Identifier(SkyblockerMod.NAMESPACE, "quick_craft/quick_craft_overlay");
+ private final ItemListWidget recipeBook = new ItemListWidget();
+ private boolean narrow;
+ private TexturedButtonWidget moreCraftsButton;
+
+ public SkyblockCraftingTableScreen(SkyblockCraftingTableScreenHandler handler, PlayerInventory inventory, Text title) {
+ super(handler, inventory, title);
+ this.backgroundWidth += 22;
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ this.narrow = this.width < 379;
+ this.recipeBook.initialize(this.width, this.height, this.client, this.narrow, new DummyRecipeScreenHandler());
+ this.x = this.recipeBook.findLeftEdge(this.width, this.backgroundWidth) + 11;
+ this.addDrawableChild(new TexturedButtonWidget(this.x + 5, this.height / 2 - 49, 20, 18, RecipeBookWidget.BUTTON_TEXTURES, button -> {
+ this.recipeBook.toggleOpen();
+ this.x = this.recipeBook.findLeftEdge(this.width, this.backgroundWidth) + 11;
+ button.setPosition(this.x + 5, this.height / 2 - 49);
+ if (moreCraftsButton != null) moreCraftsButton.setPosition(this.x + 174, this.y + 62);
+ }));
+ moreCraftsButton = new TexturedButtonWidget(this.x + 174, y + 62, 16, 16, MORE_CRAFTS_TEXTURES,
+ button -> this.onMouseClick(handler.slots.get(26), handler.slots.get(26).id, 0, SlotActionType.PICKUP));
+ moreCraftsButton.setTooltipDelay(250);
+ moreCraftsButton.setTooltip(Tooltip.of(Text.literal("More Crafts")));
+ this.addDrawableChild(moreCraftsButton);
+ assert (client != null ? client.player : null) != null;
+ client.player.currentScreenHandler = handler; // recipe book replaces it with the Dummy one fucking DUMBASS
+ this.addSelectableChild(this.recipeBook);
+ this.setInitialFocus(this.recipeBook);
+ this.titleX = 29;
+ }
+
+ @Override
+ public void handledScreenTick() {
+ super.handledScreenTick();
+ this.recipeBook.update();
+ if (moreCraftsButton == null) return;
+ ItemStack stack = handler.slots.get(26).getStack();
+ moreCraftsButton.active = stack.isEmpty() || stack.isOf(Items.PLAYER_HEAD);
+ }
+
+ @Override
+ public void render(DrawContext context, int mouseX, int mouseY, float delta) {
+ if (this.recipeBook.isOpen() && this.narrow) {
+ this.renderBackground(context, mouseX, mouseY, delta);
+ this.recipeBook.render(context, mouseX, mouseY, delta);
+ } else {
+ super.render(context, mouseX, mouseY, delta);
+ this.recipeBook.render(context, mouseX, mouseY, delta);
+ this.recipeBook.drawGhostSlots(context, this.x, this.y, true, delta);
+ }
+ this.drawMouseoverTooltip(context, mouseX, mouseY);
+ this.recipeBook.drawTooltip(context, this.x, this.y, mouseX, mouseY);
+ }
+
+
+ @Override
+ protected void drawSlot(DrawContext context, Slot slot) {
+ if (slot.id == 23 && slot.getStack().isOf(Items.BARRIER)) return;
+ super.drawSlot(context, slot);
+ }
+
+ @Override
+ protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) {
+ int i = this.x;
+ int j = (this.height - this.backgroundHeight) / 2;
+ context.drawTexture(TEXTURE, i, j, 0, 0, this.backgroundWidth, this.backgroundHeight);
+ context.drawGuiTexture(QUICK_CRAFT, i + 173, j, 0, 25, 84);
+ }
+
+ @Override
+ protected boolean isPointWithinBounds(int x, int y, int width, int height, double pointX, double pointY) {
+ return (!this.narrow || !this.recipeBook.isOpen()) && super.isPointWithinBounds(x, y, width, height, pointX, pointY);
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (this.recipeBook.mouseClicked(mouseX, mouseY, button)) {
+ this.setFocused(this.recipeBook);
+ return true;
+ }
+ if (this.narrow && this.recipeBook.isOpen()) {
+ return true;
+ }
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ @Override
+ protected boolean isClickOutsideBounds(double mouseX, double mouseY, int left, int top, int button) {
+ boolean bl = mouseX < (double) left || mouseY < (double) top || mouseX >= (double) (left + this.backgroundWidth) || mouseY >= (double) (top + this.backgroundHeight);
+ return this.recipeBook.isClickOutsideBounds(mouseX, mouseY, this.x, this.y, this.backgroundWidth, this.backgroundHeight, button) && bl;
+ }
+
+ @Override
+ protected void onMouseClick(Slot slot, int slotId, int button, SlotActionType actionType) {
+ super.onMouseClick(slot, slotId, button, actionType);
+ this.recipeBook.slotClicked(slot);
+ }
+
+
+ static class DummyRecipeScreenHandler extends AbstractRecipeScreenHandler<SimpleInventory> {
+
+ public DummyRecipeScreenHandler() {
+ super(ScreenHandlerType.GENERIC_9X6, -69);
+ }
+
+ @Override
+ public void populateRecipeFinder(RecipeMatcher finder) {}
+
+ @Override
+ public void clearCraftingSlots() {}
+
+ @Override
+ public boolean matches(RecipeEntry<? extends Recipe<SimpleInventory>> recipe) {
+ return false;
+ }
+
+ @Override
+ public int getCraftingResultSlotIndex() {
+ return 0;
+ }
+
+ @Override
+ public int getCraftingWidth() {
+ return 0;
+ }
+
+ @Override
+ public int getCraftingHeight() {
+ return 0;
+ }
+
+ @Override
+ public int getCraftingSlotCount() {
+ return 0;
+ }
+
+ @Override
+ public RecipeBookCategory getCategory() {
+ return RecipeBookCategory.CRAFTING;
+ }
+
+ @Override
+ public boolean canInsertIntoSlot(int index) {
+ return false;
+ }
+
+ @Override
+ public ItemStack quickMove(PlayerEntity player, int slot) {
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public boolean canUse(PlayerEntity player) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreenHandler.java b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreenHandler.java
new file mode 100644
index 00000000..04974ade
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/SkyblockCraftingTableScreenHandler.java
@@ -0,0 +1,69 @@
+package de.hysky.skyblocker.skyblock.item;
+
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.screen.GenericContainerScreenHandler;
+import net.minecraft.screen.ScreenHandlerType;
+import net.minecraft.screen.slot.Slot;
+
+import java.util.Arrays;
+
+public class SkyblockCraftingTableScreenHandler extends GenericContainerScreenHandler {
+
+ private static final int[] normalSlots = new int[]{
+ 10, 11, 12, 16,
+ 19, 20, 21, 23, 25,
+ 28, 29, 30, 34
+ };
+
+ public SkyblockCraftingTableScreenHandler(ScreenHandlerType<?> type, int syncId, PlayerInventory playerInventory, Inventory inventory, int rows) {
+ super(type, syncId, playerInventory, inventory, rows);
+ for (int i = 0; i < rows * 9; i++) {
+ Slot originalSlot = slots.get(i);
+ if (Arrays.binarySearch(normalSlots, i) >= 0) {
+ int[] coords = getCoords(i);
+ Slot slot = new Slot(originalSlot.inventory, originalSlot.getIndex(), coords[0], coords[1]);
+ slot.id = i;
+ slots.set(i, slot);
+ } else {
+ DisabledSlot slot = new DisabledSlot(originalSlot.inventory, originalSlot.getIndex(), originalSlot.x, originalSlot.y);
+ slot.id = i;
+ slots.set(i, slot);
+ }
+ }
+ int yOffset = (rows - 4) * 18 + 19;
+ for (int i = rows * 9; i < slots.size(); i++) {
+ Slot originalSlot = slots.get(i);
+ Slot slot = new Slot(originalSlot.inventory, originalSlot.getIndex(), originalSlot.x, originalSlot.y - yOffset);
+ slot.id = i;
+ slots.set(i, slot);
+ }
+ }
+
+ public SkyblockCraftingTableScreenHandler(GenericContainerScreenHandler handler, PlayerInventory playerInventory) {
+ this(handler.getType(), handler.syncId, playerInventory, handler.getInventory(), handler.getRows());
+ }
+
+ private int[] getCoords(int slot) {
+ if (slot == 23) return new int[]{124, 35};
+ if (slot == 16 || slot == 25 || slot == 34) {
+ int y = (slot / 9 - 1) * 18 + 8;
+ return new int[]{174, y};
+ }
+ int gridX = slot % 9 - 1;
+ int gridY = slot / 9 - 1;
+ return new int[]{30 + gridX * 18, 17 + gridY * 18};
+ }
+
+ public static class DisabledSlot extends Slot {
+
+ public DisabledSlot(Inventory inventory, int index, int x, int y) {
+ super(inventory, index, x, y);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java
new file mode 100644
index 00000000..69bc6f1c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/AccessoriesHelper.java
@@ -0,0 +1,222 @@
+package de.hysky.skyblocker.skyblock.item.tooltip;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Predicate;
+import java.util.function.ToIntFunction;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.slf4j.Logger;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.mojang.logging.LogUtils;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.JsonOps;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+import com.mojang.util.UndashedUuid;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.Pair;
+import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
+import net.minecraft.screen.GenericContainerScreenHandler;
+import net.minecraft.screen.slot.Slot;
+
+public class AccessoriesHelper {
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("collected_accessories.json");
+ private static final Pattern ACCESSORY_BAG_TITLE = Pattern.compile("Accessory Bag \\((?<page>\\d+)/\\d+\\)");
+ //UUID -> Profile Id & Data
+ private static final Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileAccessoryData>> COLLECTED_ACCESSORIES = new Object2ObjectOpenHashMap<>();
+ private static final Predicate<String> NON_EMPTY = s -> !s.isEmpty();
+ private static final Predicate<Accessory> HAS_FAMILY = Accessory::hasFamily;
+ private static final ToIntFunction<Accessory> ACCESSORY_TIER = Accessory::tier;
+
+ private static Map<String, Accessory> ACCESSORY_DATA = new Object2ObjectOpenHashMap<>();
+ //remove??
+ private static CompletableFuture<Void> loaded;
+
+ public static void init() {
+ ClientLifecycleEvents.CLIENT_STARTED.register((_client) -> load());
+ ClientLifecycleEvents.CLIENT_STOPPING.register((_client) -> save());
+ ScreenEvents.BEFORE_INIT.register((_client, screen, _scaledWidth, _scaledHeight) -> {
+ if (Utils.isOnSkyblock() && TooltipInfoType.ACCESSORIES.isTooltipEnabled() && !Utils.getProfileId().isEmpty() && screen instanceof GenericContainerScreen genericContainerScreen) {
+ Matcher matcher = ACCESSORY_BAG_TITLE.matcher(genericContainerScreen.getTitle().getString());
+
+ if (matcher.matches()) {
+ ScreenEvents.afterTick(screen).register(_screen -> {
+ GenericContainerScreenHandler handler = genericContainerScreen.getScreenHandler();
+
+ collectAccessories(handler.slots.subList(0, handler.getRows() * 9), Integer.parseInt(matcher.group("page")));
+ });
+ }
+ }
+ });
+ }
+
+ //Note: JsonOps.COMPRESSED must be used if you're using maps with non-string keys
+ private static void load() {
+ loaded = CompletableFuture.runAsync(() -> {
+ try (BufferedReader reader = Files.newBufferedReader(FILE)) {
+ COLLECTED_ACCESSORIES.putAll(ProfileAccessoryData.SERIALIZATION_CODEC.parse(JsonOps.COMPRESSED, JsonParser.parseReader(reader)).result().orElseThrow());
+ } catch (NoSuchFileException ignored) {
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Accessory Helper] Failed to load accessory file!", e);
+ }
+ });
+ }
+
+ private static void save() {
+ try (BufferedWriter writer = Files.newBufferedWriter(FILE)) {
+ SkyblockerMod.GSON.toJson(ProfileAccessoryData.SERIALIZATION_CODEC.encodeStart(JsonOps.COMPRESSED, COLLECTED_ACCESSORIES).result().orElseThrow(), writer);
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Accessory Helper] Failed to save accessory file!", e);
+ }
+ }
+
+ private static void collectAccessories(List<Slot> slots, int page) {
+ //Is this even needed?
+ if (!loaded.isDone()) return;
+
+ List<String> accessoryIds = slots.stream()
+ .map(Slot::getStack)
+ .map(ItemUtils::getItemId)
+ .filter(NON_EMPTY)
+ .toList();
+
+ String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());
+
+ COLLECTED_ACCESSORIES.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()).computeIfAbsent(Utils.getProfileId(), profileId -> ProfileAccessoryData.createDefault()).pages()
+ .put(page, new ObjectOpenHashSet<>(accessoryIds));
+ }
+
+ static Pair<AccessoryReport, String> calculateReport4Accessory(String accessoryId) {
+ if (!ACCESSORY_DATA.containsKey(accessoryId) || Utils.getProfileId().isEmpty()) return Pair.of(AccessoryReport.INELIGIBLE, null);
+
+ Accessory accessory = ACCESSORY_DATA.get(accessoryId);
+ String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());
+ Set<Accessory> collectedAccessories = COLLECTED_ACCESSORIES.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()).computeIfAbsent(Utils.getProfileId(), profileId -> ProfileAccessoryData.createDefault()).pages().values().stream()
+ .flatMap(ObjectOpenHashSet::stream)
+ .filter(ACCESSORY_DATA::containsKey)
+ .map(ACCESSORY_DATA::get)
+ .collect(Collectors.toSet());
+
+ //If the player has this accessory, and it doesn't belong to a family
+ if (collectedAccessories.contains(accessory) && accessory.family().isEmpty()) return Pair.of(AccessoryReport.HAS_HIGHEST_TIER, null);
+
+ Predicate<Accessory> HAS_SAME_FAMILY = accessory::hasSameFamily;
+ Set<Accessory> collectedAccessoriesInTheSameFamily = collectedAccessories.stream()
+ .filter(HAS_FAMILY)
+ .filter(HAS_SAME_FAMILY)
+ .collect(Collectors.toSet());
+
+ //If the player doesn't have any collected accessories with same family
+ if (collectedAccessoriesInTheSameFamily.isEmpty()) return Pair.of(AccessoryReport.MISSING, null);
+
+ Set<Accessory> accessoriesInTheSameFamily = ACCESSORY_DATA.values().stream()
+ .filter(HAS_FAMILY)
+ .filter(HAS_SAME_FAMILY)
+ .collect(Collectors.toSet());
+
+ ///If the player has the highest tier accessory in this family
+ //Take the accessories in the same family as {@code accessory}, then get the one with the highest tier
+ Optional<Accessory> highestTierOfFamily = accessoriesInTheSameFamily.stream()
+ .max(Comparator.comparingInt(ACCESSORY_TIER));
+ int maxTierInFamily = highestTierOfFamily.orElse(Accessory.EMPTY).tier();
+
+ if (collectedAccessoriesInTheSameFamily.stream().anyMatch(ca -> ca.tier() == maxTierInFamily)) return Pair.of(AccessoryReport.HAS_HIGHEST_TIER, null);
+
+ //If this accessory is a higher tier than all the other collected accessories in the same family
+ OptionalInt highestTierOfAllCollectedInFamily = collectedAccessoriesInTheSameFamily.stream()
+ .mapToInt(ACCESSORY_TIER)
+ .max();
+
+ if (accessory.tier() > highestTierOfAllCollectedInFamily.getAsInt()) return Pair.of(AccessoryReport.IS_GREATER_TIER, String.format("(%d→%d/%d)", highestTierOfAllCollectedInFamily.orElse(0), accessory.tier(), maxTierInFamily));
+
+ //If this accessory is a lower tier than one already obtained from same family
+ if (accessory.tier() < highestTierOfAllCollectedInFamily.getAsInt()) return Pair.of(AccessoryReport.OWNS_BETTER_TIER, String.format("(%d→%d/%d)", highestTierOfAllCollectedInFamily.orElse(0), accessory.tier(), maxTierInFamily));
+
+ //If there is an accessory in the same family that has a higher tier
+ //Take the accessories in the same family, then check if there is an accessory whose tier is greater than {@code accessory}
+ boolean hasGreaterTierInFamily = accessoriesInTheSameFamily.stream()
+ .anyMatch(ca -> ca.tier() > accessory.tier());
+
+ if (hasGreaterTierInFamily) return Pair.of(AccessoryReport.HAS_GREATER_TIER, String.format("(%d/%d)", highestTierOfAllCollectedInFamily.orElse(0), maxTierInFamily));
+
+ return Pair.of(AccessoryReport.MISSING, null);
+ }
+
+ static void refreshData(JsonObject data) {
+ try {
+ ACCESSORY_DATA = Accessory.MAP_CODEC.parse(JsonOps.INSTANCE, data).result().orElseThrow();
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Accessory Helper] Failed to parse data!", e);
+ }
+ }
+
+ private record ProfileAccessoryData(Int2ObjectOpenHashMap<ObjectOpenHashSet<String>> pages) {
+ private static final Codec<ProfileAccessoryData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.unboundedMap(Codec.INT, Codec.STRING.listOf().xmap(ObjectOpenHashSet::new, ObjectArrayList::new))
+ .xmap(Int2ObjectOpenHashMap::new, Int2ObjectOpenHashMap::new).fieldOf("pages").forGetter(ProfileAccessoryData::pages))
+ .apply(instance, ProfileAccessoryData::new));
+ private static final Codec<Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileAccessoryData>>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, Codec.unboundedMap(Codec.STRING, CODEC)
+ .xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new))
+ .xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new);
+
+ private static ProfileAccessoryData createDefault() {
+ return new ProfileAccessoryData(new Int2ObjectOpenHashMap<>());
+ }
+ }
+
+ /**
+ * @author AzureAaron
+ * @implSpec <a href="https://github.com/AzureAaron/aaron-mod/blob/1.20/src/main/java/net/azureaaron/mod/commands/MagicalPowerCommand.java#L475">Aaron's Mod</a>
+ */
+ private record Accessory(String id, Optional<String> family, int tier) {
+ private static final Codec<Accessory> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.STRING.fieldOf("id").forGetter(Accessory::id),
+ Codec.STRING.optionalFieldOf("family").forGetter(Accessory::family),
+ Codec.INT.optionalFieldOf("tier", 0).forGetter(Accessory::tier))
+ .apply(instance, Accessory::new));
+ private static final Codec<Map<String, Accessory>> MAP_CODEC = Codec.unboundedMap(Codec.STRING, CODEC);
+ private static final Accessory EMPTY = new Accessory("", Optional.empty(), 0);
+
+ private boolean hasFamily() {
+ return family.isPresent();
+ }
+
+ private boolean hasSameFamily(Accessory other) {
+ return other.family().equals(this.family);
+ }
+ }
+
+ enum AccessoryReport {
+ HAS_HIGHEST_TIER, //You've collected the highest tier - Collected
+ IS_GREATER_TIER, //This accessory is an upgrade from the one in the same family that you already have - Upgrade -- Shows you what tier this accessory is in its family
+ HAS_GREATER_TIER, //This accessory has a higher tier upgrade - Upgradable -- Shows you the highest tier accessory you've collected in that family
+ OWNS_BETTER_TIER, //You've collected an accessory in this family with a higher tier - Downgrade -- Shows you the highest tier accessory you've collected in that family
+ MISSING, //You don't have any accessories in this family - Missing
+ INELIGIBLE
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
index 1b3f402d..637aea22 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/ItemTooltip.java
@@ -5,10 +5,12 @@ import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.item.MuseumItemCache;
+import de.hysky.skyblocker.skyblock.item.tooltip.AccessoriesHelper.AccessoryReport;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import it.unimi.dsi.fastutil.Pair;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.item.TooltipContext;
import net.minecraft.item.DyeableItem;
@@ -238,6 +240,27 @@ public class ItemTooltip {
}
}
}
+
+ if (TooltipInfoType.ACCESSORIES.isTooltipEnabledAndHasOrNullWarning(internalID)) {
+ Pair<AccessoryReport, String> report = AccessoriesHelper.calculateReport4Accessory(internalID);
+
+ if (report.left() != AccessoryReport.INELIGIBLE) {
+ MutableText title = Text.literal(String.format("%-19s", "Accessory: ")).withColor(0xf57542);
+
+ Text stateText = switch (report.left()) {
+ case HAS_HIGHEST_TIER -> Text.literal("✔ Collected").formatted(Formatting.GREEN);
+ case IS_GREATER_TIER -> Text.literal("✦ Upgrade ").withColor(0x218bff).append(Text.literal(report.right()).withColor(0xf8f8ff));
+ case HAS_GREATER_TIER -> Text.literal("↑ Upgradable ").withColor(0xf8d048).append(Text.literal(report.right()).withColor(0xf8f8ff));
+ case OWNS_BETTER_TIER -> Text.literal("↓ Downgrade ").formatted(Formatting.GRAY).append(Text.literal(report.right()).withColor(0xf8f8ff));
+ case MISSING -> Text.literal("✖ Missing").formatted(Formatting.RED);
+
+ //Should never be the case
+ default -> Text.literal("? Unknown").formatted(Formatting.GRAY);
+ };
+
+ lines.add(title.append(stateText));
+ }
+ }
}
private static void addExoticTooltip(List<Text> lines, String internalID, NbtCompound nbt, String colorHex, String expectedHex, String existingTooltip) {
@@ -390,6 +413,7 @@ public class ItemTooltip {
TooltipInfoType.MOTES.downloadIfEnabled(futureList);
TooltipInfoType.MUSEUM.downloadIfEnabled(futureList);
TooltipInfoType.COLOR.downloadIfEnabled(futureList);
+ TooltipInfoType.ACCESSORIES.downloadIfEnabled(futureList);
CompletableFuture.allOf(futureList.toArray(CompletableFuture[]::new)).exceptionally(e -> {
LOGGER.error("Encountered unknown error while downloading tooltip data", e);
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java
index 4aba040d..6edee8d6 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/tooltip/TooltipInfoType.java
@@ -6,13 +6,15 @@ import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.utils.Http;
import de.hysky.skyblocker.utils.Utils;
-import org.jetbrains.annotations.Nullable;
import java.net.http.HttpHeaders;
import java.util.List;
import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
import java.util.function.Predicate;
+import org.jetbrains.annotations.Nullable;
+
public enum TooltipInfoType implements Runnable {
NPC("https://hysky.de/api/npcprice", itemTooltip -> itemTooltip.enableNPCPrice, true),
BAZAAR("https://hysky.de/api/bazaar", itemTooltip -> itemTooltip.enableBazaarPrice || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.enableProfitCalculator || SkyblockerConfigManager.get().locations.dungeons.dungeonChestProfit.croesusProfit || SkyblockerConfigManager.get().general.chestValue.enableChestValue, itemTooltip -> itemTooltip.enableBazaarPrice, false),
@@ -22,7 +24,8 @@ public enum TooltipInfoType implements Runnable {
MOTES("https://hysky.de/api/motesprice", itemTooltip -> itemTooltip.enableMotesPrice, itemTooltip -> itemTooltip.enableMotesPrice && Utils.isInTheRift(), true),
OBTAINED(itemTooltip -> itemTooltip.enableObtainedDate),
MUSEUM("https://hysky.de/api/museum", itemTooltip -> itemTooltip.enableMuseumInfo, true),
- COLOR("https://hysky.de/api/color", itemTooltip -> itemTooltip.enableExoticTooltip, true);
+ COLOR("https://hysky.de/api/color", itemTooltip -> itemTooltip.enableExoticTooltip, true),
+ ACCESSORIES("https://hysky.de/api/accessories", itemTooltip -> itemTooltip.enableAccessoriesHelper, true, AccessoriesHelper::refreshData);
private final String address;
private final Predicate<SkyblockerConfig.ItemTooltip> dataEnabled;
@@ -30,12 +33,23 @@ public enum TooltipInfoType implements Runnable {
private JsonObject data;
private final boolean cacheable;
private long hash;
+ private final Consumer<JsonObject> callback;
/**
* Use this for when you're adding tooltip info that has no data associated with it
*/
TooltipInfoType(Predicate<SkyblockerConfig.ItemTooltip> enabled) {
- this(null, itemTooltip -> false, enabled, null, false);
+ this(null, itemTooltip -> false, enabled, false, null);
+ }
+
+ /**
+ * @param address the address to download the data from
+ * @param enabled the predicate to check if the data should be downloaded and the tooltip should be shown
+ * @param cacheable whether the data should be cached
+ * @param callback called when the {@code data} is refreshed
+ */
+ TooltipInfoType(String address, Predicate<SkyblockerConfig.ItemTooltip> enabled, boolean cacheable, Consumer<JsonObject> callback) {
+ this(address, enabled, enabled, cacheable, callback);
}
/**
@@ -44,7 +58,7 @@ public enum TooltipInfoType implements Runnable {
* @param cacheable whether the data should be cached
*/
TooltipInfoType(String address, Predicate<SkyblockerConfig.ItemTooltip> enabled, boolean cacheable) {
- this(address, enabled, enabled, null, cacheable);
+ this(address, enabled, enabled, cacheable, null);
}
/**
@@ -54,7 +68,7 @@ public enum TooltipInfoType implements Runnable {
* @param cacheable whether the data should be cached
*/
TooltipInfoType(String address, Predicate<SkyblockerConfig.ItemTooltip> dataEnabled, Predicate<SkyblockerConfig.ItemTooltip> tooltipEnabled, boolean cacheable) {
- this(address, dataEnabled, tooltipEnabled, null, cacheable);
+ this(address, dataEnabled, tooltipEnabled, cacheable, null);
}
/**
@@ -64,12 +78,13 @@ public enum TooltipInfoType implements Runnable {
* @param data the data
* @param cacheable whether the data should be cached
*/
- TooltipInfoType(String address, Predicate<SkyblockerConfig.ItemTooltip> dataEnabled, Predicate<SkyblockerConfig.ItemTooltip> tooltipEnabled, @Nullable JsonObject data, boolean cacheable) {
+ TooltipInfoType(String address, Predicate<SkyblockerConfig.ItemTooltip> dataEnabled, Predicate<SkyblockerConfig.ItemTooltip> tooltipEnabled, boolean cacheable, @Nullable Consumer<JsonObject> callback) {
this.address = address;
this.dataEnabled = dataEnabled;
this.tooltipEnabled = tooltipEnabled;
- this.data = data;
+ this.data = null;
this.cacheable = cacheable;
+ this.callback = callback;
}
/**
@@ -146,6 +161,8 @@ public enum TooltipInfoType implements Runnable {
else this.hash = hash;
}
data = SkyblockerMod.GSON.fromJson(Http.sendGetRequest(address), JsonObject.class);
+
+ if (callback != null) callback.accept(data);
} catch (Exception e) {
ItemTooltip.LOGGER.warn("[Skyblocker] Failed to download " + this + " prices!", e);
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java
new file mode 100644
index 00000000..93c6b3f4
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/OrderedWaypoints.java
@@ -0,0 +1,444 @@
+package de.hysky.skyblocker.skyblock.waypoint;
+
+import static com.mojang.brigadier.arguments.StringArgumentType.getString;
+import static com.mojang.brigadier.arguments.StringArgumentType.word;
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Semaphore;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+import org.slf4j.Logger;
+
+import com.google.common.primitives.Floats;
+import com.google.gson.Gson;
+import com.google.gson.JsonParser;
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.logging.LogUtils;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.JsonOps;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.skyblock.item.CustomArmorDyeColors;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
+import it.unimi.dsi.fastutil.floats.FloatArrayList;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.command.CommandSource;
+import net.minecraft.command.argument.BlockPosArgumentType;
+import net.minecraft.command.argument.PosArgument;
+import net.minecraft.server.command.ServerCommandSource;
+import net.minecraft.text.Text;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.MathHelper;
+import net.minecraft.util.math.Vec3d;
+
+public class OrderedWaypoints {
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final Codec<Map<String, OrderedWaypointGroup>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, OrderedWaypointGroup.CODEC).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new);
+ private static final String PREFIX = "[Skyblocker::OrderedWaypoints::v1]";
+ private static final Path PATH = SkyblockerMod.CONFIG_DIR.resolve("ordered_waypoints.json");
+ private static final Map<String, OrderedWaypointGroup> WAYPOINTS = new Object2ObjectOpenHashMap<>();
+ private static final Semaphore SEMAPHORE = new Semaphore(1);
+ private static final Object2IntOpenHashMap<String> INDEX_STORE = new Object2IntOpenHashMap<>();
+ private static final int RADIUS = 2;
+ private static final float[] LIGHT_GRAY = { 192 / 255f, 192 / 255f, 192 / 255f };
+
+ private static CompletableFuture<Void> loaded;
+
+ public static void init() {
+ ClientLifecycleEvents.CLIENT_STARTED.register(_client -> load());
+ ClientLifecycleEvents.CLIENT_STOPPING.register(_client -> save());
+ ClientCommandRegistrationCallback.EVENT.register(OrderedWaypoints::registerCommands);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(OrderedWaypoints::render);
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(literal(SkyblockerMod.NAMESPACE)
+ .then(literal("waypoints")
+ .then(literal("ordered")
+ .then(literal("add")
+ .then(argument("groupName", word())
+ .suggests((source, builder) -> CommandSource.suggestMatching(WAYPOINTS.keySet(), builder))
+ .then(argument("pos", BlockPosArgumentType.blockPos())
+ .executes(context -> addWaypoint(context.getSource(), getString(context, "groupName"), context.getArgument("pos", PosArgument.class), Integer.MIN_VALUE, null))
+ .then(argument("hex", word())
+ .executes(context -> addWaypoint(context.getSource(), getString(context, "groupName"), context.getArgument("pos", PosArgument.class), Integer.MIN_VALUE, getString(context, "hex")))))))
+ .then(literal("addAt")
+ .then(argument("groupName", word())
+ .suggests((source, builder) -> CommandSource.suggestMatching(WAYPOINTS.keySet(), builder))
+ .then(argument("index", IntegerArgumentType.integer(0))
+ .then(argument("pos", BlockPosArgumentType.blockPos())
+ .executes(context -> addWaypoint(context.getSource(), getString(context, "groupName"), context.getArgument("pos", PosArgument.class), IntegerArgumentType.getInteger(context, "index"), null))
+ .then(argument("hex", word())
+ .executes(context -> addWaypoint(context.getSource(), getString(context, "groupName"), context.getArgument("pos", PosArgument.class), IntegerArgumentType.getInteger(context, "index"), getString(context, "hex"))))))))
+ .then(literal("remove")
+ .then(argument("groupName", word())
+ .suggests((source, builder) -> CommandSource.suggestMatching(WAYPOINTS.keySet(), builder))
+ .executes(context -> removeWaypointGroup(context.getSource(), getString(context, "groupName")))
+ .then(argument("pos", BlockPosArgumentType.blockPos())
+ .executes(context -> removeWaypoint(context.getSource(), getString(context, "groupName"), context.getArgument("pos", PosArgument.class), Integer.MIN_VALUE)))))
+ .then(literal("removeAt")
+ .then(argument("groupName", word())
+ .suggests((source, builder) -> CommandSource.suggestMatching(WAYPOINTS.keySet(), builder))
+ .then(argument("index", IntegerArgumentType.integer(0))
+ .executes(context -> removeWaypoint(context.getSource(), getString(context, "groupName"), null, IntegerArgumentType.getInteger(context, "index"))))))
+ .then(literal("toggle")
+ .then(argument("groupName", word())
+ .suggests((source, builder) -> CommandSource.suggestMatching(WAYPOINTS.keySet(), builder))
+ .executes(context -> toggleGroup(context.getSource(), getString(context, "groupName")))))
+ .then(literal("import")
+ .then(literal("coleWeight")
+ .then(argument("groupName", word())
+ .executes(context -> fromColeWeightFormat(context.getSource(), getString(context, "groupName")))))
+ .then(literal("skyblocker")
+ .executes(context -> fromSkyblockerFormat(context.getSource()))))
+ .then(literal("export")
+ .executes(context -> export(context.getSource()))))));
+ }
+
+ private static int addWaypoint(FabricClientCommandSource source, String groupName, PosArgument posArgument, int index, String hex) {
+ BlockPos pos = posArgument.toAbsoluteBlockPos(new ServerCommandSource(null, source.getPosition(), source.getRotation(), null, 0, null, null, null, null));
+
+ SEMAPHORE.acquireUninterruptibly();
+
+ if (hex != null && !CustomArmorDyeColors.isHexadecimalColor(hex)) {
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.add.invalidHexColor")));
+ SEMAPHORE.release();
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ int rgb = hex != null ? Integer.decode("0x" + hex.replace("#", "")) : Integer.MIN_VALUE;
+ float[] colorComponents = rgb != Integer.MIN_VALUE ? new float[] { ((rgb >> 16) & 0xFF) / 255f, ((rgb >> 8) & 0xFF) / 255f, (rgb & 0xFF) / 255f } : new float[0];
+
+ OrderedWaypointGroup group = WAYPOINTS.computeIfAbsent(groupName, name -> new OrderedWaypointGroup(name, true, new ObjectArrayList<>()));
+ OrderedWaypoint waypoint = new OrderedWaypoint(pos, colorComponents);
+
+ if (index != Integer.MIN_VALUE) {
+ int indexToAddAt = MathHelper.clamp(index, 0, group.waypoints().size());
+
+ group.waypoints().add(indexToAddAt, waypoint);
+ INDEX_STORE.removeInt(group.name());
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.addAt.success", group.name(), indexToAddAt)));
+ } else {
+ group.waypoints().add(waypoint);
+ INDEX_STORE.removeInt(group.name());
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.add.success", group.name(), pos.toShortString())));
+ }
+
+ SEMAPHORE.release();
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static int removeWaypointGroup(FabricClientCommandSource source, String groupName) {
+ if (WAYPOINTS.containsKey(groupName)) {
+ SEMAPHORE.acquireUninterruptibly();
+ WAYPOINTS.remove(groupName);
+ INDEX_STORE.removeInt(groupName);
+ SEMAPHORE.release();
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.removeGroup.success", groupName)));
+ } else {
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.groupNonExistent", groupName)));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static int removeWaypoint(FabricClientCommandSource source, String groupName, PosArgument posArgument, int index) {
+ if (WAYPOINTS.containsKey(groupName)) {
+ SEMAPHORE.acquireUninterruptibly();
+ OrderedWaypointGroup group = WAYPOINTS.get(groupName);
+
+ if (posArgument != null) {
+ BlockPos pos = posArgument.toAbsoluteBlockPos(new ServerCommandSource(null, source.getPosition(), source.getRotation(), null, 0, null, null, null, null));
+
+ group.waypoints().removeIf(waypoint -> waypoint.getPos().equals(pos));
+ INDEX_STORE.removeInt(group.name());
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.remove.success", pos.toShortString(), group.name())));
+ }
+
+ if (index != Integer.MIN_VALUE) {
+ int indexToRemove = MathHelper.clamp(index, 0, group.waypoints().size() - 1);
+
+ group.waypoints().remove(indexToRemove);
+ INDEX_STORE.removeInt(group.name());
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.removeAt.success", indexToRemove, group.name())));
+ }
+
+ SEMAPHORE.release();
+ } else {
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.groupNonExistent", groupName)));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static int toggleGroup(FabricClientCommandSource source, String groupName) {
+ if (WAYPOINTS.containsKey(groupName)) {
+ SEMAPHORE.acquireUninterruptibly();
+ WAYPOINTS.put(groupName, WAYPOINTS.get(groupName).toggle());
+ SEMAPHORE.release();
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.toggle.success", groupName)));
+ } else {
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.groupNonExistent", groupName)));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static void render(WorldRenderContext wrc) {
+ if ((Utils.isInCrystalHollows() || Utils.isInDwarvenMines()) && loaded.isDone() && SEMAPHORE.tryAcquire()) {
+ for (OrderedWaypointGroup group : WAYPOINTS.values()) {
+ if (group.enabled()) {
+ List<OrderedWaypoint> waypoints = group.waypoints();
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ int centreIndex = INDEX_STORE.computeIfAbsent(group.name(), name -> 0);
+
+ for (int i = 0; i < waypoints.size(); i++) {
+ OrderedWaypoint waypoint = waypoints.get(i);
+
+ if (waypoint.getPos().isWithinDistance(player.getPos(), RADIUS)) {
+ centreIndex = i;
+ INDEX_STORE.put(group.name(), i);
+
+ break;
+ }
+ }
+
+ int previousIndex = (centreIndex - 1 + waypoints.size()) % waypoints.size();
+ int currentIndex = (centreIndex + waypoints.size()) % waypoints.size();
+ int nextIndex = (centreIndex + 1) % waypoints.size();
+
+ OrderedWaypoint previous = waypoints.get(previousIndex);
+ OrderedWaypoint current = waypoints.get(currentIndex);
+ OrderedWaypoint next = waypoints.get(nextIndex);
+
+ previous.render(wrc, RelativeIndex.PREVIOUS, previousIndex);
+ current.render(wrc, RelativeIndex.CURRENT, currentIndex);
+ next.render(wrc, RelativeIndex.NEXT, nextIndex);
+
+ RenderHelper.renderLineFromCursor(wrc, Vec3d.ofCenter(next.getPos().up()), LIGHT_GRAY, 1f, 5f);
+ }
+ }
+
+ SEMAPHORE.release();
+ }
+ }
+
+ private static void load() {
+ loaded = CompletableFuture.runAsync(() -> {
+ try (BufferedReader reader = Files.newBufferedReader(PATH)) {
+ WAYPOINTS.putAll(SERIALIZATION_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseReader(reader)).result().orElseThrow());
+ } catch (NoSuchFileException ignored) {
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Ordered Waypoints] Failed to load the waypoints! :(", e);
+ }
+ });
+ }
+
+ private static void save() {
+ try (BufferedWriter writer = Files.newBufferedWriter(PATH)) {
+ SkyblockerMod.GSON.toJson(SERIALIZATION_CODEC.encodeStart(JsonOps.INSTANCE, WAYPOINTS).result().orElseThrow(), writer);
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Ordered Waypoints] Failed to save the waypoints! :(", e);
+ }
+ }
+
+ private static int export(FabricClientCommandSource source) {
+ try {
+ String json = new Gson().toJson(SERIALIZATION_CODEC.encodeStart(JsonOps.INSTANCE, WAYPOINTS).result().orElseThrow());
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ GZIPOutputStream gzip = new GZIPOutputStream(out);
+
+ gzip.write(json.getBytes());
+ gzip.close();
+
+ String encoded = new String(Base64.getEncoder().encode(out.toByteArray()));
+ String exportCode = PREFIX + encoded;
+
+ MinecraftClient.getInstance().keyboard.setClipboard(exportCode);
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.export.success")));
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Ordered Waypoints] Failed to export waypoints!", e);
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.export.fail")));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ //TODO in future handle for when the group names clash?
+ private static int fromSkyblockerFormat(FabricClientCommandSource source) {
+ try {
+ String importCode = MinecraftClient.getInstance().keyboard.getClipboard();
+
+ if (importCode.startsWith(PREFIX)) {
+ String encoded = importCode.replace(PREFIX, "");
+ byte[] decoded = Base64.getDecoder().decode(encoded);
+
+ String json = new String(new GZIPInputStream(new ByteArrayInputStream(decoded)).readAllBytes());
+ Map<String, OrderedWaypointGroup> importedWaypoints = SERIALIZATION_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(json)).result().orElseThrow();
+
+ SEMAPHORE.acquireUninterruptibly();
+ WAYPOINTS.putAll(importedWaypoints);
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.import.skyblocker.success")));
+ SEMAPHORE.release();
+ } else {
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.import.skyblocker.unknownFormatHeader")));
+ }
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Ordered Waypoints] Failed to import waypoints!", e);
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.import.skyblocker.fail")));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static int fromColeWeightFormat(FabricClientCommandSource source, String groupName) {
+ try {
+ if (WAYPOINTS.containsKey(groupName)) {
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.import.coleWeight.groupAlreadyExists", groupName)));
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ String json = MinecraftClient.getInstance().keyboard.getClipboard();
+ List<ColeWeightWaypoint> coleWeightWaypoints = ColeWeightWaypoint.LIST_CODEC.parse(JsonOps.INSTANCE, JsonParser.parseString(json)).result().orElseThrow();
+ ObjectArrayList<OrderedWaypoint> convertedWaypoints = new ObjectArrayList<>();
+
+ for (ColeWeightWaypoint waypoint : coleWeightWaypoints) {
+ if (waypoint.x().isPresent() && waypoint.y().isPresent() && waypoint.z().isPresent()) {
+ //I think Cole Weight ignores the colors and overrides them so we will comment this out
+ //float[] colorComponents = (waypoint.r().isPresent() && waypoint.g().isPresent() && waypoint.b().isPresent()) ? new float[] { waypoint.r().get() / 255f, waypoint.g().get() / 255f, waypoint.b().get() / 255f } : new float[0];
+
+ convertedWaypoints.add(new OrderedWaypoint(new BlockPos(waypoint.x().get(), waypoint.y().get(), waypoint.z().get()), new float[0]));
+ }
+ }
+
+ SEMAPHORE.acquireUninterruptibly();
+ WAYPOINTS.put(groupName, new OrderedWaypointGroup(groupName, true, convertedWaypoints));
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.import.coleWeight.success")));
+ SEMAPHORE.release();
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Ordered Waypoints] Failed to import waypoints from the Cole Weight format!", e);
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.waypoints.ordered.import.coleWeight.fail")));
+ }
+
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private record OrderedWaypointGroup(String name, boolean enabled, ObjectArrayList<OrderedWaypoint> waypoints) {
+ static final Codec<OrderedWaypointGroup> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.STRING.fieldOf("name").forGetter(OrderedWaypointGroup::name),
+ Codec.BOOL.fieldOf("enabled").forGetter(OrderedWaypointGroup::enabled),
+ OrderedWaypoint.LIST_CODEC.fieldOf("waypoints").xmap(ObjectArrayList::new, ObjectArrayList::new).forGetter(OrderedWaypointGroup::waypoints))
+ .apply(instance, OrderedWaypointGroup::new));
+
+ OrderedWaypointGroup toggle() {
+ return new OrderedWaypointGroup(name, !enabled, waypoints);
+ }
+ }
+
+ private static class OrderedWaypoint extends Waypoint {
+ static final Codec<OrderedWaypoint> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ BlockPos.CODEC.fieldOf("pos").forGetter(OrderedWaypoint::getPos),
+ Codec.floatRange(0, 1).listOf().xmap(Floats::toArray, FloatArrayList::new).optionalFieldOf("colorComponents", new float[0]).forGetter(inst -> inst.colorComponents.length == 3 ? inst.colorComponents : new float[0]))
+ .apply(instance, OrderedWaypoint::new));
+ static final Codec<List<OrderedWaypoint>> LIST_CODEC = CODEC.listOf();
+ static final float[] RED = { 1f, 0f, 0f };
+ static final float[] WHITE = { 1f, 1f, 1f };
+ static final float[] GREEN = { 0f, 1f, 0f };
+
+ private RelativeIndex relativeIndex;
+ private int waypointIndex;
+
+ OrderedWaypoint(BlockPos pos, float[] colorComponents) {
+ super(pos, Type.WAYPOINT, colorComponents);
+ }
+
+ private BlockPos getPos() {
+ return this.pos;
+ }
+
+ @Override
+ protected float[] getColorComponents() {
+ if (this.colorComponents.length != 3) {
+ return switch (this.relativeIndex) {
+ case PREVIOUS -> RED;
+ case CURRENT -> WHITE;
+ case NEXT -> GREEN;
+ };
+ }
+
+ return this.colorComponents;
+ }
+
+ private void render(WorldRenderContext context, RelativeIndex relativeIndex, int waypointIndex) {
+ this.relativeIndex = relativeIndex;
+ this.waypointIndex = waypointIndex;
+
+ render(context);
+ }
+
+ @Override
+ public void render(WorldRenderContext context) {
+ super.render(context);
+ RenderHelper.renderText(context, Text.of(String.valueOf(waypointIndex)), Vec3d.ofCenter(pos.up(2)), true);
+ }
+ }
+
+ private record ColeWeightWaypoint(Optional<Integer> x, Optional<Integer> y, Optional<Integer> z, Optional<Integer> r, Optional<Integer> g, Optional<Integer> b, Optional<Options> options) {
+ static final Codec<ColeWeightWaypoint> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.INT.optionalFieldOf("x").forGetter(ColeWeightWaypoint::x),
+ Codec.INT.optionalFieldOf("y").forGetter(ColeWeightWaypoint::y),
+ Codec.INT.optionalFieldOf("z").forGetter(ColeWeightWaypoint::z),
+ Codec.INT.optionalFieldOf("r").forGetter(ColeWeightWaypoint::r),
+ Codec.INT.optionalFieldOf("g").forGetter(ColeWeightWaypoint::g),
+ Codec.INT.optionalFieldOf("b").forGetter(ColeWeightWaypoint::b),
+ Options.CODEC.optionalFieldOf("options").forGetter(ColeWeightWaypoint::options))
+ .apply(instance, ColeWeightWaypoint::new));
+ static final Codec<List<ColeWeightWaypoint>> LIST_CODEC = CODEC.listOf();
+
+ //Even though we don't import the name this is still here incase that eventually changes
+ record Options(Optional<String> name) {
+ static final Codec<Options> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.STRING.optionalFieldOf("name").forGetter(Options::name))
+ .apply(instance, Options::new));
+ }
+ }
+
+ private enum RelativeIndex {
+ PREVIOUS,
+ CURRENT,
+ NEXT
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/InstancedUtils.java b/src/main/java/de/hysky/skyblocker/utils/InstancedUtils.java
new file mode 100644
index 00000000..1da4fa11
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/utils/InstancedUtils.java
@@ -0,0 +1,104 @@
+package de.hysky.skyblocker.utils;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Field;
+import java.lang.runtime.ObjectMethods;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+
+import com.mojang.logging.LogUtils;
+
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+
+/**
+ * @implNote If implementing any of these onto a class, ensure that all subclasses have an implementation of the methods too.
+ */
+public class InstancedUtils {
+ private static final Logger LOGGER = LogUtils.getLogger();
+ private static final Map<Class<?>, MethodHandle> EQUALS_CACHE = new ConcurrentHashMap<>();
+ private static final Map<Class<?>, MethodHandle> HASH_CODE_CACHE = new ConcurrentHashMap<>();
+ private static final Map<Class<?>, MethodHandle> TO_STRING_CACHE = new ConcurrentHashMap<>();
+
+ public static MethodHandle equals(Class<?> type) {
+ if (EQUALS_CACHE.containsKey(type)) return EQUALS_CACHE.get(type);
+
+ try {
+ Field[] fields = getClassFields(type);
+ MethodHandle[] getters = getFieldGetters(fields);
+
+ //The field names param can be anything as equals and hashCode don't care about it.
+ MethodHandle equalsHandle = (MethodHandle) ObjectMethods.bootstrap(MethodHandles.lookup(), "equals", MethodHandle.class, type, "", getters);
+
+ EQUALS_CACHE.put(type, equalsHandle);
+
+ return equalsHandle;
+ } catch (Throwable t) {
+ LOGGER.error("[Skyblocked Instanced Utils] Failed to create an equals method handle.", t);
+
+ throw new RuntimeException();
+ }
+ }
+
+ public static MethodHandle hashCode(Class<?> type) {
+ if (HASH_CODE_CACHE.containsKey(type)) return HASH_CODE_CACHE.get(type);
+
+ try {
+ Field[] fields = getClassFields(type);
+ MethodHandle[] getters = getFieldGetters(fields);
+
+ //The field names param can be anything as equals and hashCode don't care about it.
+ MethodHandle hashCodeHandle = (MethodHandle) ObjectMethods.bootstrap(MethodHandles.lookup(), "hashCode", MethodHandle.class, type, "", getters);
+
+ HASH_CODE_CACHE.put(type, hashCodeHandle);
+
+ return hashCodeHandle;
+ } catch (Throwable t) {
+ LOGGER.error("[Skyblocked Instanced Utils] Failed to create a hashCode method handle.", t);
+
+ throw new RuntimeException();
+ }
+ }
+
+ public static MethodHandle toString(Class<?> type) {
+ if (TO_STRING_CACHE.containsKey(type)) return TO_STRING_CACHE.get(type);
+
+ try {
+ Field[] fields = getClassFields(type);
+ MethodHandle[] getters = getFieldGetters(fields);
+ String fieldNames = String.join(";", Arrays.stream(fields).map(Field::getName).toArray(String[]::new));
+
+ MethodHandle toStringHandle = (MethodHandle) ObjectMethods.bootstrap(MethodHandles.lookup(), "toString", MethodHandle.class, type, fieldNames, getters);
+
+ TO_STRING_CACHE.put(type, toStringHandle);
+
+ return toStringHandle;
+ } catch (Throwable t) {
+ LOGGER.error("[Skyblocked Instanced Utils] Failed to create a toString method handle.", t);
+
+ throw new RuntimeException();
+ }
+ }
+
+ private static Field[] getClassFields(Class<?> type) {
+ return Stream.concat(Arrays.stream(type.getDeclaredFields()), Arrays.stream(type.getFields())).distinct().toArray(Field[]::new);
+ }
+
+ private static MethodHandle[] getFieldGetters(Field[] fields) throws Throwable {
+ ObjectOpenHashSet<MethodHandle> handles = new ObjectOpenHashSet<>();
+
+ for (Field field : fields) {
+ field.setAccessible(true);
+
+ MethodHandle getter = MethodHandles.lookup().unreflectGetter(field);
+
+ handles.add(getter);
+ }
+
+ return handles.toArray(MethodHandle[]::new);
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
index 05514d02..e39b5364 100644
--- a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
+++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
@@ -187,6 +187,56 @@ public class RenderHelper {
RenderSystem.depthFunc(GL11.GL_LEQUAL);
}
+ public static void renderLineFromCursor(WorldRenderContext context, Vec3d point, float[] colorComponents, float alpha, float lineWidth) {
+ Vec3d camera = context.camera().getPos();
+ MatrixStack matrices = context.matrixStack();
+
+ matrices.push();
+ matrices.translate(-camera.x, -camera.y, -camera.z);
+
+ Tessellator tessellator = RenderSystem.renderThreadTesselator();
+ BufferBuilder buffer = tessellator.getBuffer();
+ Matrix4f positionMatrix = matrices.peek().getPositionMatrix();
+
+ GL11.glEnable(GL11.GL_LINE_SMOOTH);
+ GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST);
+
+ RenderSystem.setShader(GameRenderer::getRenderTypeLinesProgram);
+ RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
+ RenderSystem.lineWidth(lineWidth);
+ RenderSystem.enableBlend();
+ RenderSystem.defaultBlendFunc();
+ RenderSystem.disableCull();
+ RenderSystem.enableDepthTest();
+ RenderSystem.depthFunc(GL11.GL_ALWAYS);
+
+ Vec3d offset = Vec3d.fromPolar(context.camera().getPitch(), context.camera().getYaw());
+ Vec3d cameraPoint = camera.add(offset);
+
+ buffer.begin(DrawMode.LINES, VertexFormats.LINES);
+ Vector3f normal = new Vector3f((float) offset.x, (float) offset.y, (float) offset.z);
+ buffer
+ .vertex(positionMatrix, (float) cameraPoint.x , (float) cameraPoint.y, (float) cameraPoint.z)
+ .color(colorComponents[0], colorComponents[1], colorComponents[2], alpha)
+ .normal(normal.x, normal.y, normal.z)
+ .next();
+
+ buffer
+ .vertex(positionMatrix, (float) point.getX(), (float) point.getY(), (float) point.getZ())
+ .color(colorComponents[0], colorComponents[1], colorComponents[2], alpha)
+ .normal(normal.x, normal.y, normal.z)
+ .next();
+
+
+ tessellator.draw();
+
+ matrices.pop();
+ GL11.glDisable(GL11.GL_LINE_SMOOTH);
+ RenderSystem.lineWidth(1f);
+ RenderSystem.enableCull();
+ RenderSystem.depthFunc(GL11.GL_LEQUAL);
+ }
+
public static void renderQuad(WorldRenderContext context, Vec3d[] points, float[] colorComponents, float alpha, boolean throughWalls) {
Vec3d camera = context.camera().getPos();
MatrixStack matrices = context.matrixStack();
diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java
index 7f3d4eda..622e1658 100644
--- a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java
+++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java
@@ -14,7 +14,7 @@ public class Waypoint implements Renderable {
public final BlockPos pos;
final Box box;
final Supplier<Type> typeSupplier;
- final float[] colorComponents;
+ protected final float[] colorComponents;
final float alpha;
final float lineWidth;
final boolean throughWalls;