Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
5672ea1
Add reputation bar to redeemed cosmetics
violine1101 Aug 23, 2025
b4a85a3
Add chroma bar to cosmetics
violine1101 Aug 23, 2025
afd9bea
Add general progress bars
violine1101 Aug 23, 2025
e72c3c1
Refactoring and tweaks
violine1101 Aug 23, 2025
6156c2c
Full red durability bar for repairable tools
violine1101 Aug 24, 2025
270eac4
Use vanilla colors for tool durability bar
violine1101 Aug 24, 2025
5d635f7
Don't show empty progress bar if progress is non-zero but rounded dow…
violine1101 Aug 31, 2025
6e4cf1e
Add option to configure progress bar color; change default to white
violine1101 Aug 31, 2025
db90b62
Don't show progress bar on locked outfit slots
violine1101 Aug 31, 2025
d90c194
Add debug hotkey F3+Z for printing item components
violine1101 Sep 25, 2025
1b242d0
Fix broken regexes from MCCI update
violine1101 Sep 25, 2025
4884dec
Fix NullPointerException
violine1101 Sep 25, 2025
eae549b
Fix progress bar for fishing items
violine1101 Sep 25, 2025
1785cac
Don't show progress bar on badges with built-in progress indicator
violine1101 Oct 7, 2025
ef94c98
Adapt to code changes on main
violine1101 Oct 13, 2025
ae0e574
Merge branch 'fix-modifiers-null' into progress-bars
violine1101 Oct 13, 2025
50cbaa9
Fix half-filled progress bar character breaking progress detection
violine1101 Oct 31, 2025
89936ef
oops, fix regex, again
violine1101 Oct 31, 2025
ca38f7a
Fix chroma regex
violine1101 Oct 31, 2025
c7c0a37
Change default for reputation and chroma bars to off
violine1101 Oct 31, 2025
5117b29
fix progress regex for mcci update
violine1101 Dec 2, 2025
dea4e3e
Fix regex for detecting a fully zero progress bar
violine1101 Dec 5, 2025
a2be58e
Adjust regexes again; single point of truth for custom icon unicode c…
violine1101 Dec 14, 2025
781455e
Merge remote-tracking branch 'upstream/main' into progress-bars
violine1101 Dec 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions src/main/java/net/asodev/islandutils/mixins/ItemBarMixin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package net.asodev.islandutils.mixins;

import net.asodev.islandutils.modules.cosmetics.CosmeticChroma;
import net.asodev.islandutils.options.IslandOptions;
import net.asodev.islandutils.util.BarInfo;
import net.asodev.islandutils.util.Utils;
import net.minecraft.network.chat.Component;
import net.minecraft.util.ARGB;
import net.minecraft.util.Mth;
import net.minecraft.world.item.ItemStack;
import org.apache.commons.lang3.math.Fraction;
import org.jetbrains.annotations.NotNull;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

import java.util.ArrayList;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Mixin(ItemStack.class)
public class ItemBarMixin {
@Unique
private static final String BAR_SEGMENT_SPACE = "\\uE001";
@Unique
private static final String BAR_SEGMENT_EMPTY = "\\uE27C";
@Unique
private static final String BAR_SEGMENT_HALF = "\\uE282";
@Unique
private static final String BAR_SEGMENT_FULL = "\\uE27F";
@Unique
private static final Pattern PROGRESS_REGEX = Pattern.compile(".*\\n[" + BAR_SEGMENT_SPACE + BAR_SEGMENT_EMPTY + BAR_SEGMENT_HALF + BAR_SEGMENT_FULL + "]* (\\d+)%.*");
@Unique
private static final Pattern PROGRESS_TRUE_ZERO_REGEX = Pattern.compile(".*\\n[" + BAR_SEGMENT_SPACE + BAR_SEGMENT_EMPTY + "]* 0%.*");
@Unique
private static final String PROGRESS_COSMETIC_LABEL = "Left-Click to Equip\n";
@Unique
private static final String PROGRESS_COSMETIC_LABEL_2 = "Left-Click to Unequip\n";
@Unique
private static final String PROGRESS_BADGE_EARNED_LABEL = "Click to Display on Profile";
@Unique
private static final String PROGRESS_BADGE_INCOMPLETE_LABEL = "\n\nStage incomplete\n\n";
@Unique
private static final String PROGRESS_BADGE_PINNED_LABEL = "Shift-Click to Unpin";
@Unique
private static final String PROGRESS_BADGE_LOCKED_LABEL = "\nUnlock this slot by reaching the below\namount of Skill Trophies.\n";
@Unique
private static final String PROGRESS_OUTFIT_LOCKED_LABEL = "Locked Outfit Slot\n\nSave a loadout of cosmetics and their";
@Unique
private static final String PROGRESS_QUEST_LABEL = "\nComplete any of the below tasks to\nfinish the quest.\n";
@Unique
private static final String PROGRESS_TOOL_USES_LABEL = "\nUses Remaining: ";
@Unique
private static final String PROGRESS_TOOL_LINE_LABEL = "\nYour equipped line adds to your\n";
@Unique
private static final String PROGRESS_TOOL_REPAIRABLE_LABEL = "\nThis item is out of uses! You can Repair\nit to restore all its uses, you can only\ndo this once per item.\n";
@Unique
private static final String PROGRESS_TOOL_REPAIRED_LABEL = "\nThis item has previously been repaired.\n";
@Unique
private static final String PROGRESS_TOOL_BROKEN_LABEL = "\nThis item is out of uses! You've already\nrepaired it once, so cannot do so\nagain.";

@Unique
private static final Pattern REPUTATION_REGEX = Pattern.compile(".*\\nRoyal Donations: (\\d+)/(\\d+)\\n.*");

@Unique
private static final String CHROMA_LABEL = "\nChromas Unlocked:\n";
@Unique
private static final String STYLE_TROPHY_ICON = "\\uE34D";
@Unique
private static final Pattern CHROMA_REGEX = Pattern.compile(".*\\n(" + CosmeticChroma.CHAR_REGEX + "{5}) - (\\d+)" + STYLE_TROPHY_ICON + "\\n.*");

@Unique
private static final int REPAIRABLE_BAR_COLOR = ARGB.colorFromFloat(1.0F, 1.0F, 0.33F, 0.33F);

@Unique
private static final int REPUTATION_BAR_COLOR = ARGB.colorFromFloat(1.0F, 0.66F, 0.33F, 1.0F);

@Unique
private static final int CHROMA_BAR_COLOR_SPEED_MS = 2000;

@Unique
private Optional<BarInfo> getProgressBar(String lore) {
if (!IslandOptions.getMisc().isShowProgressBar()) return Optional.empty();

var isCosmetic = lore.contains(PROGRESS_COSMETIC_LABEL) || lore.contains(PROGRESS_COSMETIC_LABEL_2);
var isNonEarnedBadge = lore.contains(PROGRESS_BADGE_INCOMPLETE_LABEL) && !lore.contains(PROGRESS_BADGE_EARNED_LABEL);
var isProfileBadge = lore.contains(PROGRESS_BADGE_PINNED_LABEL) || lore.contains(PROGRESS_BADGE_LOCKED_LABEL);
var isOutfitSlot = lore.contains(PROGRESS_OUTFIT_LOCKED_LABEL);
if (isCosmetic || isNonEarnedBadge || isProfileBadge || isOutfitSlot) return Optional.empty();

var isBrokenTool = lore.contains(PROGRESS_TOOL_BROKEN_LABEL);
if (isBrokenTool) return Optional.of(new BarInfo(Fraction.ZERO, ARGB.alpha(0)));

var isRepairableTool = lore.contains(PROGRESS_TOOL_REPAIRABLE_LABEL);
if (isRepairableTool) return Optional.of(new BarInfo(Fraction.ONE_HALF, REPAIRABLE_BAR_COLOR));

var progresses = getProgresses(lore);
if (progresses.isEmpty()) return Optional.empty();

var isQuest = lore.contains(PROGRESS_QUEST_LABEL);
var fraction = isQuest ? progresses.stream().max(Fraction::compareTo) : progresses.stream().findFirst();

var isNonBrokenTool = lore.contains(PROGRESS_TOOL_USES_LABEL);
if (isNonBrokenTool) {
return fraction.map(f -> {
var canRepairTool = !lore.contains(PROGRESS_TOOL_LINE_LABEL);
if (canRepairTool) {
var isRepairedTool = lore.contains(PROGRESS_TOOL_REPAIRED_LABEL);
f = (isRepairedTool ? f : f.add(Fraction.ONE)).divideBy(Fraction.getFraction(2, 1));
}
var color = Mth.hsvToRgb(f.floatValue() / 3.0F, 1.0F, 1.0F);
return new BarInfo(f, color);
});
}

return fraction.map(f -> new BarInfo(f, IslandOptions.getMisc().getProgressBarColorARGB()));
}

@Unique
private static @NotNull ArrayList<Fraction> getProgresses(String lore) {
var matcher = PROGRESS_REGEX.matcher(lore);
var progresses = new ArrayList<Fraction>();
while (matcher.find()) {
var percentage = Integer.parseInt(matcher.group(1));
if (percentage == 0) {
var trueZeroMatcher = PROGRESS_TRUE_ZERO_REGEX.matcher(matcher.group(0));
if (!trueZeroMatcher.matches()) {
percentage = 1;
}
}

var fraction = Fraction.getFraction(percentage, 100);
progresses.add(fraction);
}
return progresses;
}

@Unique
private Optional<BarInfo> getReputationBar(String lore) {
if (!IslandOptions.getCosmetics().isShowReputationBar()) return Optional.empty();

var matcher = REPUTATION_REGEX.matcher(lore);
if (!matcher.find()) return Optional.empty();

var level = Integer.parseInt(matcher.group(1));
var max = Integer.parseInt(matcher.group(2));

try {
var fraction = Fraction.getFraction(level, max);
return Optional.of(new BarInfo(fraction, REPUTATION_BAR_COLOR));
} catch (ArithmeticException e) {
return Optional.empty();
}
}

@Unique
private Optional<BarInfo> getChromaBar(String lore) {
if (!IslandOptions.getCosmetics().isShowChromaBar()) return Optional.empty();

if (!lore.contains(CHROMA_LABEL)) return Optional.empty();

var matcher = CHROMA_REGEX.matcher(lore);
if (!matcher.find()) return Optional.empty();
var chromaChars = Optional.of(matcher.group(1).chars().mapToObj(c -> (char) c).toList());

return chromaChars.map(charList ->
charList.stream()
.map(CosmeticChroma::fromChar)
.filter(Optional::isPresent)
.map(Optional::get)
.toList()
).map(chromas -> {
var fraction = CosmeticChroma.toFraction(chromas);

var color = 0;
if (!chromas.isEmpty()) {
var currentChromaIdx = (int) ((System.currentTimeMillis() / CHROMA_BAR_COLOR_SPEED_MS) % chromas.size());
var nextChromaIdx = (currentChromaIdx + 1) % chromas.size();

var currentChromaColor = chromas.get(currentChromaIdx).toColor();
var nextChromaColor = chromas.get(nextChromaIdx).toColor();

var interpolatedProgress = (float) (System.currentTimeMillis() % CHROMA_BAR_COLOR_SPEED_MS) / CHROMA_BAR_COLOR_SPEED_MS;
color = ARGB.lerp(interpolatedProgress, currentChromaColor, nextChromaColor);
}

return new BarInfo(fraction, color);
});
}

@Unique
private Optional<BarInfo> getItemBar() {
var itemStack = (ItemStack)(Object)this;
var lore = Utils.getTooltipLines(itemStack);
if (lore == null) return Optional.empty();

var loreStr = lore.stream().map(Component::getString).collect(Collectors.joining("\n"));

return this.getChromaBar(loreStr)
.or(() -> this.getReputationBar(loreStr))
.or(() -> this.getProgressBar(loreStr));
}

@Inject(method = "isBarVisible", at = @At("RETURN"), cancellable = true)
private void injectBarVisibility(CallbackInfoReturnable<Boolean> cir) {
if (this.getItemBar().isPresent()) {
cir.setReturnValue(true);
}
}

@Inject(method = "getBarWidth", at = @At("RETURN"), cancellable = true)
private void injectBarWidth(CallbackInfoReturnable<Integer> cir) {
this.getItemBar().ifPresent(barInfo -> cir.setReturnValue(barInfo.getBarWidth()));
}

@Inject(method = "getBarColor", at = @At("RETURN"), cancellable = true)
private void injectBarColor(CallbackInfoReturnable<Integer> cir) {
this.getItemBar().ifPresent(barInfo -> cir.setReturnValue(barInfo.color()));
}
}
63 changes: 56 additions & 7 deletions src/main/java/net/asodev/islandutils/mixins/ItemIDMixin.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,30 @@
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.component.ItemLore;
import org.apache.commons.lang3.StringEscapeUtils;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Mixin(ItemStack.class)
public abstract class ItemIDMixin implements DataComponentHolder {

@Unique
private static final Logger LOGGER = LoggerFactory.getLogger("IslandUtils");

@Unique
private int itemPrintCounter = -1;

@Inject(
method = "getTooltipLines",
at = @At(
Expand All @@ -42,8 +54,14 @@ public abstract class ItemIDMixin implements DataComponentHolder {
)
private void addTooltipLines(Item.TooltipContext tooltipContext, @Nullable Player player, TooltipFlag tooltipFlag, CallbackInfoReturnable<List<Component>> cir, @Local List<Component> list) {
if (!MccIslandState.isOnline() || !IslandOptions.getMisc().isDebugMode()) return;
if (!InputConstants.isKeyDown(Minecraft.getInstance().getWindow().getWindow(), InputConstants.KEY_LCONTROL))
return;

var window = Minecraft.getInstance().getWindow().getWindow();

var itemPrintHotkeyHeld = InputConstants.isKeyDown(window, InputConstants.KEY_F3) && InputConstants.isKeyDown(window, InputConstants.KEY_Z);
itemPrintCounter = (itemPrintHotkeyHeld) ? itemPrintCounter + 1 : -1;
if (itemPrintCounter == 0) printItemInfo();

if (!InputConstants.isKeyDown(window, InputConstants.KEY_LCONTROL)) return;

MutableComponent toAppend = Component.empty();
if (this.has(DataComponents.CUSTOM_DATA)) tryAddCustomItemID(toAppend); // Append MCCI Custom Item ID
Expand All @@ -62,15 +80,46 @@ private void tryAddCustomItemID(MutableComponent toAppend) {
}

@Unique
private void tryAddSlotNumber(MutableComponent toAppend) {
private Optional<Slot> getHoveredSlot() {
ItemStack me = (ItemStack) (Object) this;
Screen screen = Minecraft.getInstance().screen;
if (!(screen instanceof AbstractContainerScreen<?> containerScreen)) return;
if (screen == null) return Optional.empty();
if (!(screen instanceof AbstractContainerScreen<?> containerScreen)) return Optional.empty();
Slot hoveredSlot = ((ContainerScreenAccessor) containerScreen).getHoveredSlot();

if (hoveredSlot == null || !hoveredSlot.hasItem() || hoveredSlot.getItem() != me) return;
Component component = Component.literal(" (" + hoveredSlot.getContainerSlot() + ")").withStyle(ChatFormatting.GRAY);
toAppend.append(component);
if (hoveredSlot == null || !hoveredSlot.hasItem()) return Optional.empty();
if (hoveredSlot.getItem() != me) return Optional.empty();
return Optional.of(hoveredSlot);
}

@Unique
private void tryAddSlotNumber(MutableComponent toAppend) {
getHoveredSlot().ifPresent(hoveredSlot -> {
Component component = Component.literal(" (" + hoveredSlot.getContainerSlot() + ")").withStyle(ChatFormatting.GRAY);
toAppend.append(component);
});
}

@Unique
private void printItemInfo() {
getHoveredSlot().ifPresent(hoveredSlot -> {
LOGGER.info("-----");
LOGGER.info("Printing components of {}", hoveredSlot.getItem().getItem().getDescriptionId());
LOGGER.info("-----");
hoveredSlot.getItem().getComponents().forEach(component -> {
if (component.value() instanceof Component) {
LOGGER.info("{}=>\"{}\"", component.type(), ((Component) component.value()).getString());
} else if (component.value() instanceof ItemLore) {
var lines = ((ItemLore) component.value()).lines();
var lineStrings = lines.stream().map(Component::getString).map(StringEscapeUtils::escapeJava);
var fullString = lineStrings.collect(Collectors.joining("\\n"));
LOGGER.info("{}=>\"{}\"", component.type(), fullString);
} else {
LOGGER.info(component.toString());
}
});
LOGGER.info("-----");
});
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package net.asodev.islandutils.modules.cosmetics;

import com.google.common.collect.EnumHashBiMap;
import com.google.common.collect.ImmutableMap;
import net.minecraft.util.ARGB;
import org.apache.commons.lang3.math.Fraction;

import java.util.List;
import java.util.Optional;

public enum CosmeticChroma {
MECHANICAL,
NATURAL,
OCEANIC,
MAGICAL,
GRAYSCALE;

private static final char EMPTY_CHAR = '\uE02A';
private static final EnumHashBiMap<CosmeticChroma, Character> CHARS = EnumHashBiMap.create(
ImmutableMap.of(
MECHANICAL, '\uE02E',
NATURAL, '\uE02F',
OCEANIC, '\uE02C',
MAGICAL, '\uE02D',
GRAYSCALE, '\uE02B'
)
);
public static final String CHAR_REGEX =
"[" + EMPTY_CHAR + String.join("", CHARS.values().stream().map(String::valueOf).toList()) + "]";

public static final int COUNT = CHARS.size();

public final char toChar() {
return Optional.ofNullable(CHARS.get(this)).orElse(EMPTY_CHAR);
}

public static Optional<CosmeticChroma> fromChar(char c) {
return Optional.ofNullable(CHARS.inverse().get(c));
}

public int toColor() {
return switch (this) {
case MECHANICAL -> ARGB.colorFromFloat(1.0f, 1.0f, 0.33f, 0.0f);
case NATURAL -> ARGB.colorFromFloat(1.0f, 0.0f, 0.66f, 0.33f);
case OCEANIC -> ARGB.colorFromFloat(1.0f, 0.0f, 0.5f, 1.0f);
case MAGICAL -> ARGB.colorFromFloat(1.0f, 0.5f, 0.0f, 1.0f);
case GRAYSCALE -> ARGB.colorFromFloat(1.0f, 0.5f, 0.5f, 0.5f);
};
}

public static Fraction toFraction(List<CosmeticChroma> chromas) {
return Fraction.getFraction(chromas.size(), COUNT);
}
}
Loading
Loading