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