diff options
Diffstat (limited to 'src/main/java/de')
12 files changed, 449 insertions, 12 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index f8a15d62..3d96cc50 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -21,6 +21,7 @@ import de.hysky.skyblocker.skyblock.dwarven.CrystalsHud; import de.hysky.skyblocker.skyblock.dwarven.CrystalsLocationsManager; import de.hysky.skyblocker.skyblock.dwarven.DwarvenHud; import de.hysky.skyblocker.skyblock.end.BeaconHighlighter; +import de.hysky.skyblocker.skyblock.end.EnderNodes; import de.hysky.skyblocker.skyblock.end.TheEnd; import de.hysky.skyblocker.skyblock.garden.FarmingHud; import de.hysky.skyblocker.skyblock.garden.LowerSensitivity; @@ -111,6 +112,7 @@ public class SkyblockerMod implements ClientModInitializer { FairySouls.init(); Relics.init(); MythologicalRitual.init(); + EnderNodes.init(); OrderedWaypoints.init(); BackpackPreview.init(); QuickNav.init(); @@ -149,6 +151,7 @@ public class SkyblockerMod implements ClientModInitializer { TeleportOverlay.init(); CustomItemNames.init(); CustomArmorDyeColors.init(); + CustomArmorAnimatedDyes.init(); CustomArmorTrims.init(); TicTacToe.init(); QuiverWarning.init(); @@ -167,6 +170,8 @@ public class SkyblockerMod implements ClientModInitializer { containerSolverManager.init(); statusBarTracker.init(); BeaconHighlighter.init(); + WarpAutocomplete.init(); + Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20); Scheduler.INSTANCE.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 200); Scheduler.INSTANCE.scheduleCyclic(LividColor::update, 10); diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java index 06ac748a..bf98ac1f 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java @@ -1,6 +1,7 @@ package de.hysky.skyblocker.config; import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.skyblock.item.CustomArmorAnimatedDyes; import de.hysky.skyblocker.skyblock.item.CustomArmorTrims; import de.hysky.skyblocker.utils.chat.ChatFilterResult; import de.hysky.skyblocker.utils.waypoint.Waypoint; @@ -263,6 +264,9 @@ public class SkyblockerConfig { @SerialEntry public Object2ObjectOpenHashMap<String, CustomArmorTrims.ArmorTrimId> customArmorTrims = new Object2ObjectOpenHashMap<>(); + + @SerialEntry + public Object2ObjectOpenHashMap<String, CustomArmorAnimatedDyes.AnimatedDye> customAnimatedDyes = new Object2ObjectOpenHashMap<>(); } public static class TabHudConf { @@ -1067,6 +1071,8 @@ public class SkyblockerConfig { } public static class TheEnd { + @SerialEntry + public boolean enableEnderNodeHelper = true; @SerialEntry public boolean hudEnabled = true; diff --git a/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java index 67512b78..46f3067c 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java @@ -85,6 +85,13 @@ public class LocationsCategory { .name(Text.translatable("text.autoconfig.skyblocker.option.locations.end")) .collapsed(false) .option(Option.<Boolean>createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.end.enableEnderNodeHelper")) + .binding(defaults.locations.end.enableEnderNodeHelper, + () -> config.locations.end.enableEnderNodeHelper, + newValue -> config.locations.end.enableEnderNodeHelper = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.end.hudEnabled")) .binding(defaults.locations.end.hudEnabled, () -> config.locations.end.hudEnabled, diff --git a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java index 8397292b..743f949f 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java @@ -7,6 +7,7 @@ import de.hysky.skyblocker.skyblock.FishingHelper; import de.hysky.skyblocker.skyblock.dungeon.DungeonScore; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.end.BeaconHighlighter; +import de.hysky.skyblocker.skyblock.end.EnderNodes; import de.hysky.skyblocker.skyblock.end.TheEnd; import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual; import de.hysky.skyblocker.utils.SlayerUtils; @@ -91,6 +92,7 @@ public abstract class ClientPlayNetworkHandlerMixin { @Inject(method = "onParticle", at = @At("RETURN")) private void skyblocker$onParticle(ParticleS2CPacket packet, CallbackInfo ci) { MythologicalRitual.onParticle(packet); + EnderNodes.onParticle(packet); } @ModifyExpressionValue(method = "onEntityStatus", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/s2c/play/EntityStatusS2CPacket;getEntity(Lnet/minecraft/world/World;)Lnet/minecraft/entity/Entity;")) diff --git a/src/main/java/de/hysky/skyblocker/mixin/CommandTreeS2CPacketMixin.java b/src/main/java/de/hysky/skyblocker/mixin/CommandTreeS2CPacketMixin.java new file mode 100644 index 00000000..1cc1b8de --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixin/CommandTreeS2CPacketMixin.java @@ -0,0 +1,21 @@ +package de.hysky.skyblocker.mixin; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import de.hysky.skyblocker.skyblock.WarpAutocomplete; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.command.CommandSource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(targets = "net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket$CommandTree") +public class CommandTreeS2CPacketMixin { + @ModifyExpressionValue(method = "getNode", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/s2c/play/CommandTreeS2CPacket$CommandTree;getNode(I)Lcom/mojang/brigadier/tree/CommandNode;", ordinal = 1)) + public CommandNode<? extends CommandSource> modifyCommandSuggestions(CommandNode<CommandSource> original) { + if (Utils.isOnHypixel() && WarpAutocomplete.commandNode != null && original instanceof LiteralCommandNode<?> literalCommandNode && literalCommandNode.getLiteral().equals("warp")) { + return WarpAutocomplete.commandNode; + } + return original; + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixin/DyeableItemMixin.java b/src/main/java/de/hysky/skyblocker/mixin/DyeableItemMixin.java index e5697085..64f6a452 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/DyeableItemMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/DyeableItemMixin.java @@ -2,6 +2,7 @@ package de.hysky.skyblocker.mixin; import com.llamalad7.mixinextras.injector.ModifyReturnValue; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.CustomArmorAnimatedDyes; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; import net.minecraft.item.DyeableItem; @@ -16,6 +17,10 @@ public interface DyeableItemMixin { if (Utils.isOnSkyblock()) { String itemUuid = ItemUtils.getItemUuid(stack); + if (SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(itemUuid)) { + return CustomArmorAnimatedDyes.animateColorTransition(SkyblockerConfigManager.get().general.customAnimatedDyes.get(itemUuid)); + } + return SkyblockerConfigManager.get().general.customDyeColors.getOrDefault(itemUuid, originalColor); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/Tips.java b/src/main/java/de/hysky/skyblocker/skyblock/Tips.java index 25f7e829..513dc4b7 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/Tips.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/Tips.java @@ -27,6 +27,7 @@ public class Tips { getTipFactory("skyblocker.tips.customItemNames", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom renameItem"), getTipFactory("skyblocker.tips.customArmorDyeColors", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom dyeColor"), getTipFactory("skyblocker.tips.customArmorTrims", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom armorTrim"), + getTipFactory("skyblocker.tips.customAnimatedDyes", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker custom animatedDye"), getTipFactory("skyblocker.tips.fancyTabExtraInfo"), getTipFactory("skyblocker.tips.helpCommand", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker help"), getTipFactory("skyblocker.tips.discordRichPresence", ClickEvent.Action.SUGGEST_COMMAND, "/skyblocker config"), diff --git a/src/main/java/de/hysky/skyblocker/skyblock/WarpAutocomplete.java b/src/main/java/de/hysky/skyblocker/skyblock/WarpAutocomplete.java new file mode 100644 index 00000000..8862185f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/WarpAutocomplete.java @@ -0,0 +1,46 @@ +package de.hysky.skyblocker.skyblock; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.tree.LiteralCommandNode; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.Http; +import de.hysky.skyblocker.utils.Utils; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.minecraft.command.CommandSource; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +/** + * the mixin {@link de.hysky.skyblocker.mixin.CommandTreeS2CPacketMixin} + */ +public class WarpAutocomplete { + private static final Logger LOGGER = LoggerFactory.getLogger(WarpAutocomplete.class); + @Nullable + public static LiteralCommandNode<FabricClientCommandSource> commandNode; + + public static void init() { + CompletableFuture.supplyAsync(() -> { + try { + JsonArray jsonElements = SkyblockerMod.GSON.fromJson(Http.sendGetRequest("https://hysky.de/api/locations"), JsonArray.class); + return jsonElements.asList().stream().map(JsonElement::getAsString).toList(); + } catch (Exception e) { + LOGGER.error("[Skyblocker] Failed to download warps list", e); + } + return List.<String>of(); + }).thenAccept(warps -> commandNode = literal("warp") + .requires(fabricClientCommandSource -> Utils.isOnSkyblock()) + .then(argument("destination", StringArgumentType.string()) + .suggests((context, builder) -> CommandSource.suggestMatching(warps, builder)) + ).build() + ); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java index f43574ab..9c37de51 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/CrystalsLocationsManager.java @@ -4,11 +4,11 @@ import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.logging.LogUtils; - import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.MessageScheduler; import de.hysky.skyblocker.utils.scheduler.Scheduler; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; @@ -24,7 +24,9 @@ import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.ClickEvent; import net.minecraft.text.MutableText; import net.minecraft.text.Text; +import net.minecraft.util.Formatting; import net.minecraft.util.math.BlockPos; +import org.slf4j.Logger; import java.awt.*; import java.util.Arrays; @@ -35,11 +37,10 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.slf4j.Logger; - import static com.mojang.brigadier.arguments.StringArgumentType.getString; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; +import static net.minecraft.command.CommandSource.suggestMatching; public class CrystalsLocationsManager { private static final Logger LOGGER = LogUtils.getLogger(); @@ -101,26 +102,34 @@ public class CrystalsLocationsManager { LOGGER.error("[Skyblocker Crystals Locations Manager] Encountered an exception while extracing a location from a chat message!", e); } } - protected static Boolean checkInCrystals(BlockPos pos){ + + protected static Boolean checkInCrystals(BlockPos pos) { //checks if a location is inside crystal hollows bounds return pos.getX() >= 202 && pos.getX() <= 823 && pos.getZ() >= 202 && pos.getZ() <= 823 - && pos.getY() >= 31 && pos.getY() <= 188; + && pos.getY() >= 31 && pos.getY() <= 188; } private static void registerWaypointLocationCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) { dispatcher.register(literal(SkyblockerMod.NAMESPACE) - .then(literal("crystalWaypoints") - .then(argument("pos", BlockPosArgumentType.blockPos()) - .then(argument("place", StringArgumentType.greedyString()) - .executes(context -> addWaypointFromCommand(context.getSource(), getString(context, "place"), context.getArgument("pos", PosArgument.class))) + .then(literal("crystalWaypoints") + .then(argument("pos", BlockPosArgumentType.blockPos()) + .then(argument("place", StringArgumentType.greedyString()) + .suggests((context, builder) -> suggestMatching(WAYPOINT_LOCATIONS.keySet(), builder)) + .executes(context -> addWaypointFromCommand(context.getSource(), getString(context, "place"), context.getArgument("pos", PosArgument.class))) + ) + ) + .then(literal("share") + .then(argument("place", StringArgumentType.greedyString()) + .suggests((context, builder) -> suggestMatching(WAYPOINT_LOCATIONS.keySet(), builder)) + .executes(context -> shareWaypoint(getString(context, "place"))) + ) ) ) - ) ); } - protected static Text getSetLocationMessage(String location,BlockPos blockPos) { + protected static Text getSetLocationMessage(String location, BlockPos blockPos) { MutableText text = Constants.PREFIX.get(); text.append(Text.literal("Added waypoint for ")); Color locationColor = WAYPOINT_LOCATIONS.get(location).color; @@ -159,6 +168,21 @@ public class CrystalsLocationsManager { return Command.SINGLE_SUCCESS; } + public static int shareWaypoint(String place) { + if (activeWaypoints.containsKey(place)) { + BlockPos pos = activeWaypoints.get(place).pos; + MessageScheduler.INSTANCE.sendMessageAfterCooldown(Constants.PREFIX.get().getString() + " " + place + ": " + pos.getX() + ", " + pos.getY() + ", " + pos.getZ()); + } else { + //send fail message + if (CLIENT.player == null || CLIENT.getNetworkHandler() == null) { + return 0; + } + CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("text.autoconfig.skyblocker.option.locations.dwarvenMines.crystalsWaypoints.shareFail").formatted(Formatting.RED)), false); + } + + return Command.SINGLE_SUCCESS; + } + private static void addCustomWaypoint(String waypointName, BlockPos pos) { CrystalsWaypoint.Category category = WAYPOINT_LOCATIONS.get(waypointName); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/end/EnderNodes.java b/src/main/java/de/hysky/skyblocker/skyblock/end/EnderNodes.java new file mode 100644 index 00000000..b4af7256 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/end/EnderNodes.java @@ -0,0 +1,138 @@ +package de.hysky.skyblocker.skyblock.end; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import de.hysky.skyblocker.utils.waypoint.Waypoint; +import it.unimi.dsi.fastutil.ints.IntIntMutablePair; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.fabricmc.fabric.api.event.player.AttackBlockCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.network.packet.s2c.play.ParticleS2CPacket; +import net.minecraft.particle.ParticleType; +import net.minecraft.particle.ParticleTypes; +import net.minecraft.util.ActionResult; +import net.minecraft.util.DyeColor; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Direction; +import net.minecraft.util.math.MathHelper; + +import java.util.HashMap; +import java.util.Map; + +public class EnderNodes { + private static final MinecraftClient client = MinecraftClient.getInstance(); + private static final Map<BlockPos, EnderNode> enderNodes = new HashMap<>(); + + public static void init() { + Scheduler.INSTANCE.scheduleCyclic(EnderNodes::update, 20); + WorldRenderEvents.AFTER_TRANSLUCENT.register(EnderNodes::render); + AttackBlockCallback.EVENT.register((player, world, hand, pos, direction) -> { + enderNodes.remove(pos); + return ActionResult.PASS; + }); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + } + + public static void onParticle(ParticleS2CPacket packet) { + if (!shouldProcess()) return; + ParticleType<?> particleType = packet.getParameters().getType(); + if (!ParticleTypes.PORTAL.getType().equals(particleType) && !ParticleTypes.WITCH.getType().equals(particleType)) + return; + + double x = packet.getX(); + double y = packet.getY(); + double z = packet.getZ(); + double xFrac = MathHelper.floorMod(x, 1); + double yFrac = MathHelper.floorMod(y, 1); + double zFrac = MathHelper.floorMod(z, 1); + BlockPos pos; + Direction direction; + if (yFrac == 0.25) { + pos = BlockPos.ofFloored(x, y - 1, z); + direction = Direction.UP; + } else if (yFrac == 0.75) { + pos = BlockPos.ofFloored(x, y + 1, z); + direction = Direction.DOWN; + } else if (xFrac == 0.25) { + pos = BlockPos.ofFloored(x - 1, y, z); + direction = Direction.EAST; + } else if (xFrac == 0.75) { + pos = BlockPos.ofFloored(x + 1, y, z); + direction = Direction.WEST; + } else if (zFrac == 0.25) { + pos = BlockPos.ofFloored(x, y, z - 1); + direction = Direction.SOUTH; + } else if (zFrac == 0.75) { + pos = BlockPos.ofFloored(x, y, z + 1); + direction = Direction.NORTH; + } else { + return; + } + + EnderNode enderNode = enderNodes.computeIfAbsent(pos, EnderNode::new); + IntIntPair particles = enderNode.particles.get(direction); + particles.left(particles.leftInt() + 1); + particles.right(particles.rightInt() + 1); + } + + private static void update() { + if (shouldProcess()) { + for (EnderNode enderNode : enderNodes.values()) { + enderNode.updateParticles(); + } + } + } + + private static void render(WorldRenderContext context) { + if (shouldProcess()) { + for (EnderNode enderNode : enderNodes.values()) { + if (enderNode.shouldRender()) { + enderNode.render(context); + } + } + } + } + + private static boolean shouldProcess() { + return SkyblockerConfigManager.get().locations.end.enableEnderNodeHelper && Utils.isInTheEnd(); + } + + private static void reset() { + enderNodes.clear(); + } + + public static class EnderNode extends Waypoint { + private final Map<Direction, IntIntPair> particles = Map.of( + Direction.UP, new IntIntMutablePair(0, 0), + Direction.DOWN, new IntIntMutablePair(0, 0), + Direction.EAST, new IntIntMutablePair(0, 0), + Direction.WEST, new IntIntMutablePair(0, 0), + Direction.SOUTH, new IntIntMutablePair(0, 0), + Direction.NORTH, new IntIntMutablePair(0, 0) + ); + private long lastConfirmed; + + private EnderNode(BlockPos pos) { + super(pos, () -> SkyblockerConfigManager.get().general.waypoints.waypointType, DyeColor.CYAN.getColorComponents(), false); + } + + private void updateParticles() { + long currentTimeMillis = System.currentTimeMillis(); + if (lastConfirmed + 2000 > currentTimeMillis || client.world == null || !particles.entrySet().stream().allMatch(entry -> entry.getValue().leftInt() >= 5 && entry.getValue().rightInt() >= 5 || !client.world.getBlockState(pos.offset(entry.getKey())).isAir())) return; + lastConfirmed = currentTimeMillis; + for (Map.Entry<Direction, IntIntPair> entry : particles.entrySet()) { + entry.getValue().left(0); + entry.getValue().right(0); + } + } + + @Override + public boolean shouldRender() { + return super.shouldRender() && lastConfirmed + 5000 > System.currentTimeMillis(); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorAnimatedDyes.java b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorAnimatedDyes.java new file mode 100644 index 00000000..b011b2b0 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorAnimatedDyes.java @@ -0,0 +1,181 @@ +package de.hysky.skyblocker.skyblock.item; + +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 com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.BoolArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.Utils; +import dev.isxander.yacl3.config.v2.api.SerialEntry; +import it.unimi.dsi.fastutil.objects.Object2ObjectFunction; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +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.ClientTickEvents; +import net.minecraft.command.CommandRegistryAccess; +import net.minecraft.item.DyeableItem; +import net.minecraft.item.ItemStack; +import net.minecraft.text.Text; +import net.minecraft.util.math.MathHelper; + +public class CustomArmorAnimatedDyes { + private static final Object2ObjectOpenHashMap<AnimatedDye, AnimatedDyeStateTracker> STATE_TRACKER_MAP = new Object2ObjectOpenHashMap<>(); + private static final Object2ObjectFunction<AnimatedDye, AnimatedDyeStateTracker> NEW_STATE_TRACKER = _dye -> AnimatedDyeStateTracker.create(); + private static final int DEFAULT_TICK_DELAY = 4; + private static int ticks; + + public static void init() { + ClientCommandRegistrationCallback.EVENT.register(CustomArmorAnimatedDyes::registerCommands); + ClientTickEvents.END_CLIENT_TICK.register(_client -> ++ticks); + } + + private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) { + dispatcher.register(literal(SkyblockerMod.NAMESPACE) + .then(literal("custom") + .then(literal("animatedDye") + .executes(context -> customizeAnimatedDye(context.getSource(), null, null, 0, false, 0)) + .then(argument("hex1", word()) + .then(argument("hex2", word()) + .then(argument("samples", IntegerArgumentType.integer(1)) + .then(argument("cycleBack", BoolArgumentType.bool()) + .executes(context -> customizeAnimatedDye(context.getSource(), getString(context, "hex1"), getString(context, "hex2"), IntegerArgumentType.getInteger(context, "samples"), BoolArgumentType.getBool(context, "cycleBack"), DEFAULT_TICK_DELAY)) + .then(argument("tickDelay", IntegerArgumentType.integer(0, 20)) + .executes(context ->customizeAnimatedDye(context.getSource(), getString(context, "hex1"), getString(context, "hex2"), IntegerArgumentType.getInteger(context, "samples"), BoolArgumentType.getBool(context, "cycleBack"), IntegerArgumentType.getInteger(context, "tickDelay"))))))))))); + } + + private static int customizeAnimatedDye(FabricClientCommandSource source, String hex1, String hex2, int samples, boolean cycleBack, int tickDelay) { + if (hex1 != null && hex2 != null && (!CustomArmorDyeColors.isHexadecimalColor(hex1) || !CustomArmorDyeColors.isHexadecimalColor(hex2))) { + source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customAnimatedDyes.invalidHex"))); + + return Command.SINGLE_SUCCESS; + } + + ItemStack heldItem = source.getPlayer().getMainHandStack(); + + if (Utils.isOnSkyblock() && heldItem != null && !heldItem.isEmpty()) { + if (heldItem.getItem() instanceof DyeableItem) { + String itemUuid = ItemUtils.getItemUuid(heldItem); + + if (!itemUuid.isEmpty()) { + Object2ObjectOpenHashMap<String, AnimatedDye> customAnimatedDyes = SkyblockerConfigManager.get().general.customAnimatedDyes; + + if (hex1 == null && hex2 == null) { + if (customAnimatedDyes.containsKey(itemUuid)) { + customAnimatedDyes.remove(itemUuid); + SkyblockerConfigManager.save(); + source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customAnimatedDyes.removed"))); + } else { + source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customAnimatedDyes.neverHad"))); + } + } else { + AnimatedDye animatedDye = new AnimatedDye(Integer.decode("0x" + hex1.replace("#", "")), Integer.decode("0x" + hex2.replace("#", "")), samples, cycleBack, tickDelay); + + customAnimatedDyes.put(itemUuid, animatedDye); + SkyblockerConfigManager.save(); + source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customAnimatedDyes.added"))); + } + } else { + source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customAnimatedDyes.noItemUuid"))); + } + } else { + source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customAnimatedDyes.notDyeable"))); + } + } else { + source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customAnimatedDyes.unableToSetDye"))); + } + + return Command.SINGLE_SUCCESS; + } + + public static int animateColorTransition(AnimatedDye animatedDye) { + AnimatedDyeStateTracker trackedState = STATE_TRACKER_MAP.computeIfAbsent(animatedDye, NEW_STATE_TRACKER); + + if (trackedState.lastRecordedTick + animatedDye.tickDelay() > ticks) { + return trackedState.lastColor; + } + + trackedState.lastRecordedTick = ticks; + + return animatedDye.interpolate(trackedState); + } + + //Credit to https://codepen.io/OliverBalfour/post/programmatically-making-gradients + private static int interpolate(int firstColor, int secondColor, double percentage) { + int r1 = MathHelper.square((firstColor >> 16) & 0xFF); + int g1 = MathHelper.square((firstColor >> 8) & 0xFF); + int b1 = MathHelper.square(firstColor & 0xFF); + + int r2 = MathHelper.square((secondColor >> 16) & 0xFF); + int g2 = MathHelper.square((secondColor >> 8) & 0xFF); + int b2 = MathHelper.square(secondColor & 0xFF); + + double inverse = 1d - percentage; + + int r3 = (int) Math.floor(Math.sqrt(r1 * inverse + r2 * percentage)); + int g3 = (int) Math.floor(Math.sqrt(g1 * inverse + g2 * percentage)); + int b3 = (int) Math.floor(Math.sqrt(b1 * inverse + b2 * percentage)); + + return (r3 << 16) | (g3 << 8 ) | b3; + } + + private static class AnimatedDyeStateTracker { + private int sampleCounter; + private boolean onBackCycle = false; + private int lastColor = 0; + private int lastRecordedTick = 0; + + boolean shouldCycleBack(int samples, boolean canCycleBack) { + return canCycleBack && sampleCounter == samples; + } + + int getAndDecrement() { + return sampleCounter--; + } + + int getAndIncrement() { + return sampleCounter++; + } + + static AnimatedDyeStateTracker create() { + return new AnimatedDyeStateTracker(); + } + } + + public record AnimatedDye(@SerialEntry int color1, @SerialEntry int color2, @SerialEntry int samples, @SerialEntry boolean cycleBack, @SerialEntry int tickDelay) { + + private int interpolate(AnimatedDyeStateTracker stateTracker) { + if (stateTracker.shouldCycleBack(samples, cycleBack)) stateTracker.onBackCycle = true; + + if (stateTracker.onBackCycle) { + double percent = (1d / (double) samples) * stateTracker.getAndDecrement(); + + //Go back to normal cycle once we've cycled all the way back + if (stateTracker.sampleCounter == 0) stateTracker.onBackCycle = false; + + int interpolatedColor = CustomArmorAnimatedDyes.interpolate(color1, color2, percent); + stateTracker.lastColor = interpolatedColor; + + return interpolatedColor; + } + + //This will only happen if cycleBack is false + if (stateTracker.sampleCounter == samples) stateTracker.sampleCounter = 0; + + double percent = (1d / (double) samples) * stateTracker.getAndIncrement(); + int interpolatedColor = CustomArmorAnimatedDyes.interpolate(color1, color2, percent); + + stateTracker.lastColor = interpolatedColor; + + return interpolatedColor; + } + } +} 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 637aea22..62c50735 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 @@ -218,7 +218,8 @@ public class ItemTooltip { } if (TooltipInfoType.COLOR.isTooltipEnabledAndHasOrNullWarning(internalID) && stack.getNbt() != null) { - boolean hasCustomDye = SkyblockerConfigManager.get().general.customDyeColors.containsKey(ItemUtils.getItemUuid(stack)); + String uuid = ItemUtils.getItemUuid(stack); + boolean hasCustomDye = SkyblockerConfigManager.get().general.customDyeColors.containsKey(uuid) || SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(uuid); if (!hasCustomDye && stack.getItem() instanceof DyeableItem item && item.hasColor(stack)) { String colorHex = String.format("%06X", item.getColor(stack)); |