Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
dce7213
basic implementation
viciscat Nov 28, 2025
21a489d
show price after selling
viciscat Nov 30, 2025
b55fb65
cleaner optional chain
viciscat Nov 30, 2025
3cbd145
highlight collected tier for upgrades
viciscat Nov 30, 2025
32fc3e1
implement HoveredItemStackProvider
viciscat Dec 3, 2025
970a82d
remember stuff when you switch page
viciscat Dec 3, 2025
7bbdfc9
highest tier only option
viciscat Dec 3, 2025
d8c4dbf
open wiki on clicky
viciscat Dec 3, 2025
72dd7b2
translatable
viciscat Dec 3, 2025
8a1e1f5
move some things around
viciscat Dec 3, 2025
62d73c4
config
viciscat Dec 3, 2025
fd16f03
Merge branch 'master' into accessory-helper
viciscat Dec 6, 2025
54abd39
fix upgrade's price per MP not taking in account the upgraded accesso…
viciscat Dec 6, 2025
d7ecd29
rename a few things for consistency
viciscat Dec 6, 2025
049705e
add new fields from the API (thanks aaron)
viciscat Dec 6, 2025
44acbc2
recomb
viciscat Dec 6, 2025
e7fa8cc
commit the background texture
viciscat Dec 6, 2025
b9c788b
Merge branch 'master' into accessory-helper
viciscat Dec 12, 2025
575616a
fixes
viciscat Dec 12, 2025
f58685a
Merge branch 'master' into accessory-helper
viciscat Dec 12, 2025
7d5f659
1.21.11
viciscat Dec 12, 2025
aeccfa8
checkstyle
viciscat Dec 13, 2025
ad2d788
prepare mojmap transition
viciscat Dec 13, 2025
07e521e
Merge branch 'master' into accessory-helper
viciscat Dec 13, 2025
aa8c0f2
Merge branch 'master' into accessory-helper
viciscat Dec 13, 2025
43515bf
little merge oopsies
viciscat Dec 13, 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
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig
newValue -> config.helpers.enableBuildersWandPreview = newValue)
.controller(ConfigUtils.createBooleanController())
.build())
// Accessories Helper Widget
.option(Option.<Boolean>createBuilder()
.name(Text.translatable("skyblocker.config.helpers.enableAccessoriesHelperWidget"))
.description(Text.translatable("skyblocker.config.helpers.enableAccessoriesHelperWidget.@Tooltip"))
.binding(defaults.helpers.enableAccessoriesHelperWidget,
() -> config.helpers.enableAccessoriesHelperWidget,
newValue -> config.helpers.enableAccessoriesHelperWidget = newValue)
.controller(ConfigUtils.createBooleanController())
.build())
//Mythological Ritual
.group(OptionGroup.createBuilder()
.name(Text.translatable("skyblocker.config.helpers.mythologicalRitual"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public class HelperConfig {

public boolean enableBuildersWandPreview = true;

public boolean enableAccessoriesHelperWidget = true;

public MythologicalRitual mythologicalRitual = new MythologicalRitual();

public Jerry jerry = new Jerry();
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure you can just slice for the one you want.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but i liek expressions :((
more readable than to target the first changed local or whatever

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package de.hysky.skyblocker.mixins;

import com.llamalad7.mixinextras.expression.Definition;
import com.llamalad7.mixinextras.expression.Expression;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.screen.GenericContainerScreenHandler;
import net.minecraft.text.Text;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;

@Mixin(GenericContainerScreen.class)
public abstract class GenericContainerScreenMixin extends HandledScreen<GenericContainerScreenHandler> {

public GenericContainerScreenMixin(GenericContainerScreenHandler handler, PlayerInventory inventory, Text title) {
super(handler, inventory, title);
}

@Definition(id = "width", field = "Lnet/minecraft/client/gui/screen/ingame/GenericContainerScreen;width:I")
@Definition(id = "backgroundWidth", field = "Lnet/minecraft/client/gui/screen/ingame/GenericContainerScreen;backgroundWidth:I")
@Expression("(this.width - this.backgroundWidth) / ?")
@ModifyExpressionValue(method = "drawBackground", at = @At("MIXINEXTRAS:EXPRESSION"))
public int x(int ignored) {
return x;
}

@Definition(id = "height", field = "Lnet/minecraft/client/gui/screen/ingame/GenericContainerScreen;height:I")
@Definition(id = "backgroundHeight", field = "Lnet/minecraft/client/gui/screen/ingame/GenericContainerScreen;backgroundHeight:I")
@Expression("(this.height - this.backgroundHeight) / ?")
@ModifyExpressionValue(method = "drawBackground", at = @At("MIXINEXTRAS:EXPRESSION"))
public int y(int ignored) {
return y;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ public interface HandledScreenAccessor {
@Accessor("x")
int getX();

@Accessor("x")
void setX(int x);

@Accessor("y")
int getY();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package de.hysky.skyblocker.skyblock.accessories;

import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.utils.container.SimpleContainerSolver;
import de.hysky.skyblocker.utils.render.gui.ColorHighlight;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Colors;
import net.minecraft.util.math.ColorHelper;
import org.jetbrains.annotations.Nullable;

import java.util.List;

public class AccessoriesContainerSolver extends SimpleContainerSolver {
private static final int COLOR = ColorHelper.withAlpha(0.7f, Colors.GREEN);
public static final AccessoriesContainerSolver INSTANCE = new AccessoriesContainerSolver();

@Nullable String highlightedAccessory;

protected AccessoriesContainerSolver() {
super(AccessoriesHelper.ACCESSORY_BAG_TITLE);
}

@Override
public List<ColorHighlight> getColors(Int2ObjectMap<ItemStack> slots) {
if (highlightedAccessory == null) return List.of();
return slots.int2ObjectEntrySet().stream()
.filter(entry -> entry.getValue().getSkyblockId().equals(highlightedAccessory))
.map(entry -> new ColorHighlight(entry.getIntKey(), COLOR))
.toList();
}

@Override
public boolean isEnabled() {
return SkyblockerConfigManager.get().helpers.enableAccessoriesHelperWidget;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package de.hysky.skyblocker.skyblock.item.tooltip;
package de.hysky.skyblocker.skyblock.accessories;

import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.annotations.Init;
import de.hysky.skyblocker.skyblock.item.tooltip.info.TooltipInfoType;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.data.ProfiledData;
import it.unimi.dsi.fastutil.Pair;
Expand All @@ -19,6 +20,7 @@
import net.minecraft.screen.slot.Slot;

import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand All @@ -30,20 +32,21 @@
import java.util.stream.Collectors;

public class AccessoriesHelper {
private static final ObjectOpenHashSet<String> EMPTY = new ObjectOpenHashSet<>(0);
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+\\))?");
static final Pattern ACCESSORY_BAG_TITLE = Pattern.compile("Accessory Bag(?: \\((?<page>\\d+)\\/\\d+\\))?");
//UUID -> Profile Id & Data
private static final ProfiledData<ProfileAccessoryData> COLLECTED_ACCESSORIES = new ProfiledData<>(FILE, ProfileAccessoryData.CODEC, true);
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<>();
public static Map<String, Accessory> ACCESSORY_DATA = new Object2ObjectOpenHashMap<>();

@Init
public static void init() {
COLLECTED_ACCESSORIES.init();
ScreenEvents.BEFORE_INIT.register((_client, screen, _scaledWidth, _scaledHeight) -> {
ScreenEvents.AFTER_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());

Expand All @@ -54,6 +57,7 @@ public static void init() {

collectAccessories(handler.slots.subList(0, handler.getRows() * 9), page);
});
AccessoriesHelperWidget.attachToScreen(genericContainerScreen);
}
}
});
Expand All @@ -69,8 +73,17 @@ private static void collectAccessories(List<Slot> slots, int page) {
.filter(NON_EMPTY)
.toList();

COLLECTED_ACCESSORIES.computeIfAbsent(ProfileAccessoryData::createDefault).pages()
.put(page, new ObjectOpenHashSet<>(accessoryIds));
List<String> recombobulated = slots.stream()
.map(Slot::getStack)
.filter(stack -> ItemUtils.getCustomData(stack).getInt("rarity_upgrades", 0) > 0)
.map(ItemStack::getSkyblockId)
.filter(NON_EMPTY)
.toList();

ProfileAccessoryData data = COLLECTED_ACCESSORIES.computeIfAbsent(ProfileAccessoryData::createDefault);
data.recombobulatedAccessories().removeAll(data.pages().getOrDefault(page, EMPTY)); // Remove previous accessories.
data.recombobulatedAccessories().addAll(recombobulated);
data.pages().put(page, new ObjectOpenHashSet<>(accessoryIds));
}

public static Pair<AccessoryReport, String> calculateReport4Accessory(String accessoryId) {
Expand All @@ -81,41 +94,21 @@ public static Pair<AccessoryReport, String> calculateReport4Accessory(String acc
//Ignore rift-only accessories
if (accessory.origin().orElse("").equals("RIFT")) return Pair.of(AccessoryReport.INELIGIBLE, null);

Set<Accessory> collectedAccessories = COLLECTED_ACCESSORIES.computeIfAbsent(ProfileAccessoryData::createDefault).pages().values().stream()
.flatMap(ObjectOpenHashSet::stream)
.filter(ACCESSORY_DATA::containsKey)
.map(ACCESSORY_DATA::get)
.collect(Collectors.toSet());
Set<Accessory> collectedAccessories = getCollectedAccessories();

// If the accessory doesn't belong to a family
if (accessory.family().isEmpty()) {
//If the player has this accessory or player doesn't have this accessory
return collectedAccessories.contains(accessory) ? Pair.of(AccessoryReport.HAS_HIGHEST_TIER, null) : Pair.of(AccessoryReport.MISSING, "");
}

Predicate<Accessory> HAS_SAME_FAMILY = accessory::hasSameFamily;
Set<Accessory> collectedAccessoriesInTheSameFamily = collectedAccessories.stream()
.filter(HAS_FAMILY)
.filter(HAS_SAME_FAMILY)
.collect(Collectors.toSet());

Set<Accessory> accessoriesInTheSameFamily = ACCESSORY_DATA.values().stream()
.filter(HAS_FAMILY)
.filter(HAS_SAME_FAMILY)
.collect(Collectors.toSet());

int highestTierInFamily = accessoriesInTheSameFamily.stream()
.mapToInt(ACCESSORY_TIER)
.max()
.orElse(0);
FamilyReport report = calculateFamilyReport(accessory, collectedAccessories);
int highestTierInFamily = report.highestInFamily().tier();

//If the player hasn't collected any accessory in same family
if (collectedAccessoriesInTheSameFamily.isEmpty()) return Pair.of(AccessoryReport.MISSING, String.format("(%d/%d)", accessory.tier(), highestTierInFamily));
if (report.highestCollectedInFamily().isEmpty()) return Pair.of(AccessoryReport.MISSING, String.format("(%d/%d)", accessory.tier(), highestTierInFamily));

int highestTierCollectedInFamily = collectedAccessoriesInTheSameFamily.stream()
.mapToInt(ACCESSORY_TIER)
.max()
.getAsInt();
int highestTierCollectedInFamily = report.highestCollectedInFamily().get().tier();

//If this accessory is the highest tier, and the player has the highest tier accessory in this family
//This accounts for multiple accessories with the highest tier
Expand All @@ -133,31 +126,67 @@ public static Pair<AccessoryReport, String> calculateReport4Accessory(String acc
return Pair.of(AccessoryReport.MISSING, String.format("(%d/%d)", accessory.tier(), highestTierInFamily));
}

public static FamilyReport calculateFamilyReport(Accessory accessory, Set<Accessory> collectedAccessories) {
if (accessory.family().isEmpty()) throw new IllegalArgumentException("accessory family cannot be empty");
Predicate<Accessory> hasSameFamily = accessory::hasSameFamily;
return new FamilyReport(
ACCESSORY_DATA.values().stream()
.filter(HAS_FAMILY)
.filter(hasSameFamily)
.max(Comparator.comparingInt(ACCESSORY_TIER))
.orElse(accessory),
collectedAccessories.stream()
.filter(HAS_FAMILY)
.filter(hasSameFamily)
.max(Comparator.comparingInt(ACCESSORY_TIER))
);
}

public static Set<Accessory> getCollectedAccessories() {
return COLLECTED_ACCESSORIES.computeIfAbsent(ProfileAccessoryData::createDefault).pages().values().stream()
.flatMap(ObjectOpenHashSet::stream)
.filter(ACCESSORY_DATA::containsKey)
.map(ACCESSORY_DATA::get)
.collect(Collectors.toSet());
}

public static boolean hasAccessory(String accessoryId) {
return COLLECTED_ACCESSORIES.computeIfAbsent(ProfileAccessoryData::createDefault).pages().values().stream()
.anyMatch(set -> set.contains(accessoryId));
}

public static boolean isRecombobulated(String accessoryId) {
return hasAccessory(accessoryId) && COLLECTED_ACCESSORIES.computeIfAbsent(ProfileAccessoryData::createDefault).recombobulatedAccessories().contains(accessoryId);
}

public static void refreshData(Map<String, Accessory> data) {
ACCESSORY_DATA = data;
}

private record ProfileAccessoryData(Int2ObjectOpenHashMap<ObjectOpenHashSet<String>> pages) {
private record ProfileAccessoryData(Int2ObjectOpenHashMap<ObjectOpenHashSet<String>> pages, ObjectOpenHashSet<String> recombobulatedAccessories) {
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)
.xmap(Int2ObjectOpenHashMap::new, Int2ObjectOpenHashMap::new).fieldOf("pages").forGetter(ProfileAccessoryData::pages),
Codec.STRING.listOf().optionalFieldOf("recombobulatedAccessories", List.of()).xmap(ObjectOpenHashSet::new, List::copyOf).forGetter(ProfileAccessoryData::recombobulatedAccessories)
).apply(instance, ProfileAccessoryData::new));

private static ProfileAccessoryData createDefault() {
return new ProfileAccessoryData(new Int2ObjectOpenHashMap<>());
return new ProfileAccessoryData(new Int2ObjectOpenHashMap<>(), new ObjectOpenHashSet<>());
}
}

/**
* @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>
*/
public record Accessory(String id, Optional<String> family, int tier, Optional<String> origin) {
public record Accessory(String id, Optional<String> family, int tier, Optional<String> origin, boolean enrichable, boolean recombobulatable) {
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),
Codec.STRING.optionalFieldOf("origin").forGetter(Accessory::origin)
Codec.STRING.optionalFieldOf("origin").forGetter(Accessory::origin),
Codec.BOOL.optionalFieldOf("enrichable", true).forGetter(Accessory::enrichable),
Codec.BOOL.optionalFieldOf("recombobulatable", true).forGetter(Accessory::recombobulatable)
).apply(instance, Accessory::new));
public static final Codec<Map<String, Accessory>> MAP_CODEC = Codec.unboundedMap(Codec.STRING, CODEC);

Expand All @@ -170,6 +199,8 @@ private boolean hasSameFamily(Accessory other) {
}
}

public record FamilyReport(Accessory highestInFamily, Optional<Accessory> highestCollectedInFamily) {}

public 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
Expand Down
Loading