aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorPeyton Brown <81496880+PeytonBrown@users.noreply.github.com>2025-07-24 15:58:56 -0400
committerGitHub <noreply@github.com>2025-07-24 15:58:56 -0400
commit4126dccbb67f4afd2eae791e61fde6526a9ca7c5 (patch)
tree3ef1aef9e5401ae19b91f74bec1de712607b1889 /src/main/java
parent29937337ec8e00aaebda1f8a3e283b81ae40506a (diff)
downloadSkyblocker-4126dccbb67f4afd2eae791e61fde6526a9ca7c5.tar.gz
Skyblocker-4126dccbb67f4afd2eae791e61fde6526a9ca7c5.tar.bz2
Skyblocker-4126dccbb67f4afd2eae791e61fde6526a9ca7c5.zip
Customize player heads (#1423)
* Initial Implementation * improve ux * Fix ui bugs * Only actually render what is on screen * Add all player heads from neuRepo/items * Load player heads from existing neu repo * Address PR comments * Use ItemRepository * Fix merge conflict * Refactor ItemRepository for sync/async post-import tasks with thread-safe list
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java13
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomHelmetTextures.java75
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/CustomizeArmorScreen.java40
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/HeadSelectionWidget.java249
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java60
6 files changed, 428 insertions, 11 deletions
diff --git a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
index da90f3fb..0c05f960 100644
--- a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java
@@ -52,6 +52,8 @@ public class GeneralConfig {
public Object2ObjectOpenHashMap<String, CustomArmorAnimatedDyes.AnimatedDye> customAnimatedDyes = new Object2ObjectOpenHashMap<>();
+ public Object2ObjectOpenHashMap<String, String> customHelmetTextures = new Object2ObjectOpenHashMap<>();
+
public static class SpeedPresets {
public boolean enableSpeedPresets = true;
}
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java
index eab413e3..b914b9ea 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java
@@ -9,7 +9,9 @@ import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.item.custom.CustomArmorTrims;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
+import net.minecraft.item.Items;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import de.hysky.skyblocker.skyblock.item.custom.CustomHelmetTextures;
import net.minecraft.component.ComponentHolder;
import net.minecraft.component.ComponentType;
import net.minecraft.component.DataComponentTypes;
@@ -21,16 +23,21 @@ public interface ComponentHolderMixin {
@SuppressWarnings("unchecked")
@ModifyReturnValue(method = "get", at = @At("RETURN"))
- private <T> T skyblocker$customArmorTrims(T original, ComponentType<? extends T> dataComponentType) {
+ private <T> T skyblocker$customComponents(T original, ComponentType<? extends T> dataComponentType) {
if (Utils.isOnSkyblock() && ((Object) this) instanceof ItemStack stack) {
+ String itemUuid = ItemUtils.getItemUuid(stack);
if (dataComponentType == DataComponentTypes.TRIM) {
Object2ObjectOpenHashMap<String, CustomArmorTrims.ArmorTrimId> customTrims = SkyblockerConfigManager.get().general.customArmorTrims;
- String itemUuid = ItemUtils.getItemUuid(stack);
-
if (customTrims.containsKey(itemUuid)) {
CustomArmorTrims.ArmorTrimId trimKey = customTrims.get(itemUuid);
return (T) CustomArmorTrims.TRIMS_CACHE.getOrDefault(trimKey, (ArmorTrim) original);
}
+ } else if (dataComponentType == DataComponentTypes.PROFILE && stack.isOf(Items.PLAYER_HEAD)) {
+ Object2ObjectOpenHashMap<String, String> customTextures = SkyblockerConfigManager.get().general.customHelmetTextures;
+ if (customTextures.containsKey(itemUuid)) {
+ String tex = customTextures.get(itemUuid);
+ return (T) CustomHelmetTextures.getProfile(tex);
+ }
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomHelmetTextures.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomHelmetTextures.java
new file mode 100644
index 00000000..8a8beabc
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomHelmetTextures.java
@@ -0,0 +1,75 @@
+package de.hysky.skyblocker.skyblock.item.custom;
+
+import com.mojang.logging.LogUtils;
+import de.hysky.skyblocker.annotations.Init;
+import de.hysky.skyblocker.skyblock.itemlist.ItemRepository;
+import de.hysky.skyblocker.utils.ItemUtils;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ObjectSet;
+import net.minecraft.component.type.ProfileComponent;
+import net.minecraft.item.Items;
+import org.slf4j.Logger;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+/**
+ * Caches generated ProfileComponents for custom player head textures.
+ */
+public class CustomHelmetTextures {
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ public static final List<NamedTexture> TEXTURES = new ArrayList<>();
+ public static final Object2ObjectOpenHashMap<String, ProfileComponent> PROFILE_CACHE = new Object2ObjectOpenHashMap<>();
+
+ public record NamedTexture(String name, String texture, String internalName) {}
+
+ private static final Pattern LEVEL_PATTERN = Pattern.compile("\\[Lvl[^\\]]*\\]");
+
+ @Init
+ public static void init() {
+ ItemRepository.runAsyncAfterImport(CustomHelmetTextures::loadTextures);
+ }
+
+ private static void loadTextures() {
+ try {
+ if (!ItemRepository.filesImported()) return;
+
+ TEXTURES.clear();
+ ObjectSet<String> seen = new ObjectOpenHashSet<>();
+ ItemRepository.getItemsStream()
+ .filter(stack -> stack.isOf(Items.PLAYER_HEAD))
+ .forEach(stack -> {
+ String texture = ItemUtils.getHeadTexture(stack);
+ if (texture.isEmpty() || !seen.add(texture)) return;
+ String name = cleanName(stack.getName().getString());
+ TEXTURES.add(new NamedTexture(name, texture, stack.getNeuName()));
+ });
+
+ TEXTURES.sort(java.util.Comparator.comparing(NamedTexture::internalName));
+ LOGGER.info("[Skyblocker] Loaded and sorted {} helmet textures from repo", TEXTURES.size());
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to load helmet textures from repo", e);
+ }
+ }
+
+ private static String cleanName(String name) {
+ return LEVEL_PATTERN.matcher(name).replaceAll("").trim();
+ }
+
+ public static List<NamedTexture> getTextures() {
+ return TEXTURES;
+ }
+
+ public static ProfileComponent getProfile(String texture) {
+ return PROFILE_CACHE.computeIfAbsent(texture, (String t) ->
+ new ProfileComponent(Optional.of("custom"),
+ Optional.of(UUID.nameUUIDFromBytes(t.getBytes(StandardCharsets.UTF_8))),
+ ItemUtils.propertyMapWithTexture(t)));
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/CustomizeArmorScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/CustomizeArmorScreen.java
index 0cf4b844..b01e9950 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/CustomizeArmorScreen.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/CustomizeArmorScreen.java
@@ -51,6 +51,7 @@ public class CustomizeArmorScreen extends Screen {
public boolean isInvisibleTo(PlayerEntity player) {
return true;
}
+
@Override
public void onEquipStack(EquipmentSlot slot, ItemStack oldStack, ItemStack newStack) {}
};
@@ -59,6 +60,7 @@ public class CustomizeArmorScreen extends Screen {
private int selectedSlot = 0;
private TrimSelectionWidget trimSelectionWidget;
private ColorSelectionWidget colorSelectionWidget;
+ private HeadSelectionWidget headSelectionWidget;
private final Screen previousScreen;
@@ -84,12 +86,16 @@ public class CustomizeArmorScreen extends Screen {
}
});
}
+
static boolean canEdit(ItemStack stack) {
- return stack.isIn(ItemTags.TRIMMABLE_ARMOR) && !ItemUtils.getItemUuid(stack).isEmpty();
+ boolean hasUuid = !ItemUtils.getItemUuid(stack).isEmpty();
+ if (stack.isOf(Items.PLAYER_HEAD)) return hasUuid;
+ return stack.isIn(ItemTags.TRIMMABLE_ARMOR) && hasUuid;
}
private final boolean nothingCustomizable;
+
protected CustomizeArmorScreen(Screen previousScreen) {
super(Math.random() < 0.01 ? Text.translatable("skyblocker.armorCustomization.titleSecret") : Text.translatable("skyblocker.armorCustomization.title"));
List<ItemStack> list = ItemUtils.getArmor(CLIENT.player);
@@ -109,7 +115,8 @@ public class CustomizeArmorScreen extends Screen {
builder.put(uuid, new PreviousConfig(
SkyblockerConfigManager.get().general.customArmorTrims.containsKey(uuid) ? Optional.of(SkyblockerConfigManager.get().general.customArmorTrims.get(uuid)) : Optional.empty(),
SkyblockerConfigManager.get().general.customDyeColors.containsKey(uuid) ? OptionalInt.of(SkyblockerConfigManager.get().general.customDyeColors.getInt(uuid)) : OptionalInt.empty(),
- SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(uuid) ? Optional.of(SkyblockerConfigManager.get().general.customAnimatedDyes.get(uuid)) : Optional.empty()
+ SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(uuid) ? Optional.of(SkyblockerConfigManager.get().general.customAnimatedDyes.get(uuid)) : Optional.empty(),
+ SkyblockerConfigManager.get().general.customHelmetTextures.containsKey(uuid) ? Optional.of(SkyblockerConfigManager.get().general.customHelmetTextures.get(uuid)) : Optional.empty()
));
}
}
@@ -134,16 +141,18 @@ public class CustomizeArmorScreen extends Screen {
addDrawableChild(pieceSelectionWidget);
-
if (!nothingCustomizable) {
+ headSelectionWidget = new HeadSelectionWidget(x + 105, y, w - 105 - 5, 165);
+ addDrawableChild(headSelectionWidget);
+
trimSelectionWidget = new TrimSelectionWidget(x + 105, y, w - 105 - 5, 80);
addDrawableChild(trimSelectionWidget);
- trimSelectionWidget.setCurrentItem(armor[selectedSlot]);
if (colorSelectionWidget != null) colorSelectionWidget.close();
colorSelectionWidget = new ColorSelectionWidget(trimSelectionWidget.getX(), trimSelectionWidget.getBottom() + 10, trimSelectionWidget.getWidth(), 100, textRenderer);
addDrawableChild(colorSelectionWidget);
- colorSelectionWidget.setCurrentItem(armor[selectedSlot]);
+
+ updateWidgets();
}
addDrawableChild(ButtonWidget.builder(Text.translatable("gui.cancel"), b -> cancel()).position(width / 2 - 155, height - 25).build());
@@ -164,6 +173,10 @@ public class CustomizeArmorScreen extends Screen {
animatedDye -> SkyblockerConfigManager.get().general.customAnimatedDyes.put(uuid, animatedDye),
() -> SkyblockerConfigManager.get().general.customAnimatedDyes.remove(uuid)
);
+ previousConfig.helmetTexture().ifPresentOrElse(
+ tex -> SkyblockerConfigManager.get().general.customHelmetTextures.put(uuid, tex),
+ () -> SkyblockerConfigManager.get().general.customHelmetTextures.remove(uuid)
+ );
});
close();
}
@@ -193,6 +206,18 @@ public class CustomizeArmorScreen extends Screen {
client.setScreen(previousScreen);
}
+ private void updateWidgets() {
+ if (nothingCustomizable) return;
+ ItemStack item = armor[selectedSlot];
+ boolean isPlayerHead = item.isOf(Items.PLAYER_HEAD);
+ headSelectionWidget.setCurrentItem(item);
+ trimSelectionWidget.setCurrentItem(item);
+ colorSelectionWidget.setCurrentItem(item);
+ headSelectionWidget.visible = isPlayerHead;
+ trimSelectionWidget.visible = !isPlayerHead;
+ colorSelectionWidget.visible = !isPlayerHead;
+ }
+
private class PieceSelectionWidget extends ClickableWidget {
private static final Identifier HOTBAR_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "armor_customization_screen/mini_hotbar");
@@ -242,8 +267,7 @@ public class CustomizeArmorScreen extends Screen {
if (i < 0 || i >= armor.length || !selectable[i]) return;
if (i != selectedSlot) {
selectedSlot = i;
- trimSelectionWidget.setCurrentItem(armor[selectedSlot]);
- colorSelectionWidget.setCurrentItem(armor[selectedSlot]);
+ updateWidgets();
}
}
@@ -256,7 +280,7 @@ public class CustomizeArmorScreen extends Screen {
protected void appendClickableNarrations(NarrationMessageBuilder builder) {}
}
- private record PreviousConfig(Optional<CustomArmorTrims.ArmorTrimId> armorTrimId, OptionalInt color, Optional<CustomArmorAnimatedDyes.AnimatedDye> animatedDye) {}
+ private record PreviousConfig(Optional<CustomArmorTrims.ArmorTrimId> armorTrimId, OptionalInt color, Optional<CustomArmorAnimatedDyes.AnimatedDye> animatedDye, Optional<String> helmetTexture) {}
private static class CustomizeButton extends ClickableWidget {
// thanks to @yuflow
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/HeadSelectionWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/HeadSelectionWidget.java
new file mode 100644
index 00000000..0b0a848f
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/HeadSelectionWidget.java
@@ -0,0 +1,249 @@
+package de.hysky.skyblocker.skyblock.item.custom.screen;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.skyblock.item.custom.CustomHelmetTextures;
+import de.hysky.skyblocker.skyblock.profileviewer.utils.ProfileViewerUtils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder;
+import net.minecraft.client.gui.widget.ClickableWidget;
+import net.minecraft.client.gui.widget.ContainerWidget;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class HeadSelectionWidget extends ContainerWidget {
+
+ private static final Identifier INNER_SPACE_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "menu_inner_space");
+
+
+ private final List<HeadButton> allButtons = new ArrayList<>();
+ private final List<HeadButton> visibleButtons = new ArrayList<>();
+ private final TextFieldWidget searchField;
+ private final HeadButton noneButton;
+ private int buttonsPerRow = 1;
+
+ private ItemStack currentItem;
+ private String selectedTexture;
+
+ public HeadSelectionWidget(int x, int y, int width, int height) {
+ super(x, y, width, height, Text.of("HeadSelection"));
+ searchField = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, x + 3, y + 3, width - 6, 12, Text.translatable("gui.recipebook.search_hint"));
+ searchField.setChangedListener(this::filterButtons);
+
+ for (CustomHelmetTextures.NamedTexture tex : CustomHelmetTextures.getTextures()) {
+ ItemStack head = ProfileViewerUtils.createSkull(tex.texture());
+ HeadButton button = new HeadButton(tex.name(), tex.texture(), head, () -> onClick(tex.texture()));
+ allButtons.add(button);
+ }
+ noneButton = new HeadButton("", null, new ItemStack(Items.BARRIER), () -> onClick(null));
+
+ filterButtons("");
+ }
+
+ private void layoutButtons() {
+ buttonsPerRow = Math.max(1, (getWidth() - 6) / 20);
+ int startY = searchField.getBottom() + 3;
+ for (int i = 0; i < visibleButtons.size(); i++) {
+ HeadButton button = visibleButtons.get(i);
+ button.setPosition(getX() + 3 + (i % buttonsPerRow) * 20, startY + (i / buttonsPerRow) * 20);
+ }
+ }
+
+ private void onClick(String texture) {
+ selectedTexture = texture;
+ updateConfig();
+ updateButtons();
+ }
+
+ private void updateConfig() {
+ if (currentItem == null) return;
+ String uuid = ItemUtils.getItemUuid(currentItem);
+ if (selectedTexture == null) {
+ SkyblockerConfigManager.get().general.customHelmetTextures.remove(uuid);
+ } else {
+ SkyblockerConfigManager.get().general.customHelmetTextures.put(uuid, selectedTexture);
+ }
+ }
+
+ private void updateButtons() {
+ for (HeadButton b : allButtons) {
+ b.selected = Objects.equals(b.texture, selectedTexture);
+ }
+ noneButton.selected = selectedTexture == null;
+ }
+
+ private void filterButtons(String search) {
+ setScrollY(0);
+ String s = search.toLowerCase();
+ visibleButtons.clear();
+ visibleButtons.add(noneButton);
+ for (HeadButton b : allButtons) {
+ if (b.name.toLowerCase().contains(s)) {
+ visibleButtons.add(b);
+ }
+ }
+ layoutButtons();
+ updateButtons();
+ }
+
+ @Override
+ public List<? extends Element> children() {
+ int startY = searchField.getBottom() + 3;
+ int endY = getY() + getHeight() - 2;
+ int scrollY = (int) getScrollY();
+ List<Element> list = new ArrayList<>();
+ for (HeadButton b : visibleButtons) {
+ int y = b.getY() - scrollY;
+ if (y + b.getHeight() > startY && y < endY) {
+ list.add(b);
+ }
+ }
+ list.add(searchField);
+ return list;
+ }
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ context.drawGuiTexture(RenderLayer::getGuiTextured, INNER_SPACE_TEXTURE, getX(), getY(), getWidth(), getHeight());
+
+ searchField.render(context, mouseX, mouseY, delta);
+
+ int startY = searchField.getBottom() + 3;
+ int startX = getX() + 2;
+ int endX = getX() + getWidth() - 2;
+ int endY = getY() + getHeight() - 2;
+ context.enableScissor(startX, startY, endX, endY);
+ int scrollY = (int) getScrollY();
+ HeadButton hovered = null;
+ for (HeadButton b : visibleButtons) {
+ int originalY = b.getY();
+ int y = originalY - scrollY;
+ if (y + b.getHeight() <= startY || y >= endY) {
+ continue;
+ }
+ b.setY(y);
+ b.render(context, mouseX, mouseY, delta);
+ if (b.isMouseOver(mouseX, mouseY) && mouseX >= startX && mouseX < endX && mouseY >= startY && mouseY < endY) {
+ hovered = b;
+ }
+ b.setY(originalY);
+ }
+ drawScrollbar(context);
+ context.disableScissor();
+
+ if (hovered != null && !hovered.name.isEmpty()) {
+ context.drawTooltip(MinecraftClient.getInstance().textRenderer, Text.of(hovered.name), mouseX, mouseY);
+ }
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (searchField.mouseClicked(mouseX, mouseY, button)) {
+ setFocused(searchField);
+ return true;
+ }
+
+ double adjustedMouseY = mouseY + getScrollY();
+ if (overflows()) {
+ int scrollbarX = getScrollbarX();
+ // Default scrollbar width is 6 pixels
+ if (mouseX >= scrollbarX && mouseX < scrollbarX + 6) {
+ int thumbY = getScrollbarThumbY();
+ int thumbHeight = getScrollbarThumbHeight();
+ if (mouseY >= thumbY && mouseY < thumbY + thumbHeight) {
+ adjustedMouseY = mouseY;
+ }
+ }
+ }
+
+ return super.mouseClicked(mouseX, adjustedMouseY, button);
+ }
+
+ @Override
+ public boolean charTyped(char chr, int modifiers) {
+ if (searchField.isFocused() && searchField.charTyped(chr, modifiers)) {
+ return true;
+ }
+ return super.charTyped(chr, modifiers);
+ }
+
+ @Override
+ public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
+ if (searchField.isFocused() && searchField.keyPressed(keyCode, scanCode, modifiers)) {
+ return true;
+ }
+ return super.keyPressed(keyCode, scanCode, modifiers);
+ }
+
+ @Override
+ protected int getContentsHeightWithPadding() {
+ int rows = Math.ceilDiv(visibleButtons.size(), buttonsPerRow);
+ // 3px top padding + search bar height + 3px gap before the grid +
+ // button rows + 3px bottom padding
+ return rows * 20 + searchField.getHeight() + 9;
+ }
+
+ @Override
+ protected double getDeltaYPerScroll() {
+ return 10;
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {}
+
+ public void setCurrentItem(@NotNull ItemStack item) {
+ currentItem = item;
+ String uuid = ItemUtils.getItemUuid(item);
+ selectedTexture = SkyblockerConfigManager.get().general.customHelmetTextures.get(uuid);
+ updateButtons();
+ filterButtons(searchField.getText());
+ }
+
+ private static class HeadButton extends ClickableWidget {
+ private final String name;
+ private final String texture;
+ private final ItemStack head;
+ private boolean selected = false;
+
+ HeadButton(String name, String texture, ItemStack head, Runnable onPress) {
+ super(0, 0, 20, 20, Text.empty());
+ this.name = name;
+ this.texture = texture;
+ this.head = head;
+ this.onPress = onPress;
+ }
+
+ private final Runnable onPress;
+
+ @Override
+ protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) {
+ context.drawItem(head, getX() + 2, getY() + 2);
+ if (selected) {
+ context.fill(getX(), getY(), getX() + getWidth(), getY() + getHeight(), 0x3000FF00);
+ }
+ if (isHovered()) {
+ context.fill(getX(), getY(), getX() + getWidth(), getY() + getHeight(), 0x20FFFFFF);
+ }
+ }
+
+ @Override
+ public void onClick(double mouseX, double mouseY) {
+ onPress.run();
+ }
+
+ @Override
+ protected void appendClickableNarrations(NarrationMessageBuilder builder) {}
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java
index 07c56525..9ea7f06b 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/itemlist/ItemRepository.java
@@ -18,6 +18,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Stream;
public class ItemRepository {
@@ -28,6 +30,14 @@ public class ItemRepository {
private static final List<SkyblockRecipe> recipes = new ArrayList<>();
private static final HashMap<String, @NEUId String> bazaarStocks = new HashMap<>();
/**
+ * Store callbacks so we can execute them each time the item repository
+ * finishes loading.
+ */
+ private static final List<AfterImportTask> afterImportTasks = new CopyOnWriteArrayList<>();
+
+ private record AfterImportTask(Runnable runnable, boolean async) {}
+
+ /**
* Consumers must check this field when accessing `items` and `itemsMap`, or else thread safety is not guaranteed.
*/
private static boolean itemsImported = false;
@@ -60,6 +70,21 @@ public class ItemRepository {
NEURepoManager.forEachItem(ItemRepository::loadRecipes);
filesImported = true;
+
+ afterImportTasks.forEach(task -> {
+ if (task.async) {
+ CompletableFuture.runAsync(task.runnable).exceptionally(e -> {
+ LOGGER.error("[Skyblocker Item Repo Loader] Encountered unknown exception while running after import tasks", e);
+ return null;
+ });
+ } else {
+ try {
+ task.runnable.run();
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Item Repo Loader] Encountered unknown exception while running after import tasks", e);
+ }
+ }
+ });
}
private static void loadItem(NEUItem item) {
@@ -158,4 +183,39 @@ public class ItemRepository {
case null, default -> null;
};
}
+
+ /**
+ * Runs the given runnable after the item repository has finished loading.
+ * If the repository is already loaded the runnable is executed immediately.
+ *
+ * @param runnable the runnable to run
+ */
+ public static void runAsyncAfterImport(Runnable runnable) {
+ runAfterImport(runnable, true);
+ }
+
+ /**
+ * Runs the given runnable after the item repository has finished loading.
+ * If the repository is already loaded the runnable is executed immediately.
+ *
+ * @param runnable the runnable to run
+ * @param async whether to run the runnable asynchronously
+ */
+ public static void runAfterImport(Runnable runnable, boolean async) {
+ if (filesImported) {
+ if (async) {
+ CompletableFuture.runAsync(runnable).exceptionally(e -> {
+ LOGGER.error("[Skyblocker Item Repo Loader] Encountered unknown exception while running after import task", e);
+ return null;
+ });
+ } else {
+ try {
+ runnable.run();
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Item Repo Loader] Encountered unknown exception while running after import task", e);
+ }
+ }
+ }
+ afterImportTasks.add(new AfterImportTask(runnable, async));
+ }
}