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