aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-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
-rw-r--r--src/main/resources/assets/skyblocker/lang/en_us.json25
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button.pngbin0 -> 210 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button_disabled.pngbin0 -> 201 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button_highlighted.pngbin0 -> 210 bytes
-rw-r--r--src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/quick_craft_overlay.pngbin0 -> 250 bytes
19 files changed, 1195 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;
diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json
index dd44b8fe..84409b9f 100644
--- a/src/main/resources/assets/skyblocker/lang/en_us.json
+++ b/src/main/resources/assets/skyblocker/lang/en_us.json
@@ -90,6 +90,13 @@
"text.autoconfig.skyblocker.option.general.itemTooltip.enableMuseumInfo.@Tooltip": "If this item is donatable to the museum, then the item's category in the musuem is displayed. It also displays a marker indicating whether you've donated that item to your musuem or not (freebies not yet supported).\n\nMake sure to enable your Museum API for accurate information!",
"text.autoconfig.skyblocker.option.general.itemTooltip.enableExoticTooltip": "Enable Exotic Tooltip",
"text.autoconfig.skyblocker.option.general.itemTooltip.enableExoticTooltip.@Tooltip": "Displays the type of exotic below the item's name if an armor piece is exotic.",
+ "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper": "Enable Accessories Helper",
+ "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[0]": "When hovering over an accessory you are informed about whether you already have it or not, and whether it's worse than what you have already collected or better. List of Statuses:",
+ "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[1]": "You have the highest tier accessory from that family.",
+ "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[2]": "This accessory is an upgrade from the one in the same family that you already have. Also shows you what tier this accessory is in it's family.",
+ "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[3]": "This accessory can be upgraded. Also tells you what tier of accessory you have in that family.",
+ "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[4]": "You already own an accessory in the same family that is better than this one. Also tells you what tier of accessory you have in that family.",
+ "text.autoconfig.skyblocker.option.general.itemTooltip.enableAccessoriesHelper.@Tooltip[5]": "You don't own any accessory from this family.",
"text.autoconfig.skyblocker.option.general.dungeonQuality": "Dungeon Quality",
"text.autoconfig.skyblocker.option.general.dungeonQuality.@Tooltip": "Displays quality and tier of dungeon drops from mobs.\n\n\nReminder:\nTier 1-3 dropped from F1-F3\nTier 4-7 dropped from F4-F7 or M1-M4\nTier 8-10 are dropped only from M5-M7",
"text.autoconfig.skyblocker.option.general.enableNewYearCakesHelper": "Enable New Year Cakes Helper",
@@ -154,6 +161,7 @@
"text.autoconfig.skyblocker.option.general.searchOverlay.enableCommands.@Tooltip": "Opens the bazaar search with \"/bzs\" and auction house with \"/ahs\". A re-log is required for this setting to be updated.",
"text.autoconfig.skyblocker.option.general.searchOverlay.historyLabel": "History:",
"text.autoconfig.skyblocker.option.general.betterPartyFinder": "Better Party Finder",
+ "text.autoconfig.skyblocker.option.general.fancyCraftingTable": "Fancy Crafting Table UI",
"skyblocker.itemTooltip.nullMessage": "§cItem price information on tooltip will renew in max 60 seconds. If not, check latest.log",
"skyblocker.itemTooltip.noData": "§cNo Data",
@@ -596,6 +604,23 @@
"skyblocker.crimson.kuudra.noArrowPoison": "No Arrow Poison!",
"skyblocker.crimson.kuudra.lowArrowPoison": "Low on Arrow Poison!",
+
+ "skyblocker.waypoints.ordered.groupNonExistent": "§cThe waypoint group %s doesn't exist.",
+ "skyblocker.waypoints.ordered.add.invalidHexColor": "§cInvalid HEX color code!",
+ "skyblocker.waypoints.ordered.addAt.success": "Added a waypoint in group %s at index %d.",
+ "skyblocker.waypoints.ordered.add.success": "Added a waypoint in group %s at %s.",
+ "skyblocker.waypoints.ordered.removeGroup.success": "Successfully removed the waypoint group %s.",
+ "skyblocker.waypoints.ordered.remove.success": "Successfully removed the waypoint at %s from group %s.",
+ "skyblocker.waypoints.ordered.removeAt.success": "Successfully removed the waypoint at index %d from group %s.",
+ "skyblocker.waypoints.ordered.toggle.success": "Toggled the waypoint group %s.",
+ "skyblocker.waypoints.ordered.export.success": "Successfully copied your waypoints to your clipboard!",
+ "skyblocker.waypoints.ordered.export.fail": "§cFailed to export your waypoints, check the latest.log for more information.",
+ "skyblocker.waypoints.ordered.import.skyblocker.success": "Successfully imported waypoints from the Skyblocker Ordered Waypoints format!",
+ "skyblocker.waypoints.ordered.import.skyblocker.unknownFormatHeader": "§cThis import code doesn't look like its in the Skyblocker Ordered Waypoints format, double check your clipboard to see if everything is correct.",
+ "skyblocker.waypoints.ordered.import.skyblocker.fail": "§cFailed to import waypoints from the Skyblocker Ordered Waypoints format. Make sure to have the waypoint data copied to your clipboard!",
+ "skyblocker.waypoints.ordered.import.coleWeight.groupAlreadyExists": "§cThere is already an ordered waypoints group under the name \"%s\", please choose another name to import your waypoints under.",
+ "skyblocker.waypoints.ordered.import.coleWeight.success": "Successfully imported waypoints from the Cole Weight format.",
+ "skyblocker.waypoints.ordered.import.coleWeight.fail": "§cFailed to import waypoints from the Cole Weight format. Make sure to have the waypoint data copied to your clipboard!",
"emi.category.skyblocker.skyblock": "Skyblock"
}
diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button.png
new file mode 100644
index 00000000..7a6e3162
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button_disabled.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button_disabled.png
new file mode 100644
index 00000000..02a22b4f
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button_disabled.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button_highlighted.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button_highlighted.png
new file mode 100644
index 00000000..be41b321
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/more_button_highlighted.png
Binary files differ
diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/quick_craft_overlay.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/quick_craft_overlay.png
new file mode 100644
index 00000000..d8cf0c2b
--- /dev/null
+++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/quick_craft/quick_craft_overlay.png
Binary files differ