/* * Decompiled with CFR 0.152. * * Could not load the following classes: * com.google.common.collect.Maps * com.google.common.hash.Hashing * com.mojang.logging.LogUtils * org.apache.commons.lang3.mutable.MutableBoolean * org.jspecify.annotations.Nullable * org.slf4j.Logger */ package net.minecraft.client.gui.screens.packs; import com.google.common.collect.Maps; import com.google.common.hash.Hashing; import com.mojang.blaze3d.platform.NativeImage; import com.mojang.logging.LogUtils; import java.io.IOException; import java.io.InputStream; import java.nio.file.DirectoryStream; import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import java.util.stream.Collectors; import java.util.stream.Stream; import net.minecraft.ChatFormatting; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.StringWidget; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.gui.components.toasts.SystemToast; import net.minecraft.client.gui.layouts.HeaderAndFooterLayout; import net.minecraft.client.gui.layouts.LinearLayout; import net.minecraft.client.gui.screens.AlertScreen; import net.minecraft.client.gui.screens.ConfirmScreen; import net.minecraft.client.gui.screens.NoticeWithLinkScreen; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.packs.PackSelectionModel; import net.minecraft.client.gui.screens.packs.TransferableSelectionList; import net.minecraft.client.renderer.texture.DynamicTexture; import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.network.chat.CommonComponents; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; import net.minecraft.server.packs.PackResources; import net.minecraft.server.packs.repository.Pack; import net.minecraft.server.packs.repository.PackDetector; import net.minecraft.server.packs.repository.PackRepository; import net.minecraft.server.packs.resources.IoSupplier; import net.minecraft.util.Util; import net.minecraft.world.level.validation.ForbiddenSymlinkInfo; import org.apache.commons.lang3.mutable.MutableBoolean; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; public class PackSelectionScreen extends Screen { private static final Logger LOGGER = LogUtils.getLogger(); private static final Component AVAILABLE_TITLE = Component.translatable("pack.available.title"); private static final Component SELECTED_TITLE = Component.translatable("pack.selected.title"); private static final Component OPEN_PACK_FOLDER_TITLE = Component.translatable("pack.openFolder"); private static final Component SEARCH = Component.translatable("gui.packSelection.search").withStyle(EditBox.SEARCH_HINT_STYLE); private static final int LIST_WIDTH = 200; private static final int HEADER_ELEMENT_SPACING = 4; private static final int SEARCH_BOX_HEIGHT = 15; private static final Component DRAG_AND_DROP = Component.translatable("pack.dropInfo").withStyle(ChatFormatting.GRAY); private static final Component DIRECTORY_BUTTON_TOOLTIP = Component.translatable("pack.folderInfo"); private static final int RELOAD_COOLDOWN = 20; private static final Identifier DEFAULT_ICON = Identifier.withDefaultNamespace("textures/misc/unknown_pack.png"); private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this); private final PackSelectionModel model; private @Nullable Watcher watcher; private long ticksToReload; private @Nullable TransferableSelectionList availablePackList; private @Nullable TransferableSelectionList selectedPackList; private @Nullable EditBox search; private final Path packDir; private @Nullable Button doneButton; private final Map packIcons = Maps.newHashMap(); public PackSelectionScreen(PackRepository repository, Consumer output, Path packDir, Component title) { super(title); this.model = new PackSelectionModel(this::populateLists, this::getPackIcon, repository, output); this.packDir = packDir; this.watcher = Watcher.create(packDir); } @Override public void onClose() { this.model.commit(); this.closeWatcher(); } private void closeWatcher() { if (this.watcher != null) { try { this.watcher.close(); this.watcher = null; } catch (Exception exception) { // empty catch block } } } @Override protected void init() { this.layout.setHeaderHeight(4 + this.font.lineHeight + 4 + this.font.lineHeight + 4 + 15 + 4); LinearLayout header = this.layout.addToHeader(LinearLayout.vertical().spacing(4)); header.defaultCellSetting().alignHorizontallyCenter(); header.addChild(new StringWidget(this.getTitle(), this.font)); header.addChild(new StringWidget(DRAG_AND_DROP, this.font)); this.search = header.addChild(new EditBox(this.font, 0, 0, 200, 15, Component.empty())); this.search.setHint(SEARCH); this.search.setResponder(this::updateFilteredEntries); this.availablePackList = this.layout.addToContents(new TransferableSelectionList(this.minecraft, this, 200, this.height - 66, AVAILABLE_TITLE)); this.selectedPackList = this.layout.addToContents(new TransferableSelectionList(this.minecraft, this, 200, this.height - 66, SELECTED_TITLE)); LinearLayout footer = this.layout.addToFooter(LinearLayout.horizontal().spacing(8)); footer.addChild(Button.builder(OPEN_PACK_FOLDER_TITLE, button -> Util.getPlatform().openPath(this.packDir)).tooltip(Tooltip.create(DIRECTORY_BUTTON_TOOLTIP)).build()); this.doneButton = footer.addChild(Button.builder(CommonComponents.GUI_DONE, button -> this.onClose()).build()); this.layout.visitWidgets(x$0 -> { AbstractWidget cfr_ignored_0 = (AbstractWidget)this.addRenderableWidget(x$0); }); this.repositionElements(); this.setInitialFocus(this.search); this.reload(); } private void updateFilteredEntries(String value) { this.filterEntries(value, this.model.getSelected(), this.selectedPackList); this.filterEntries(value, this.model.getUnselected(), this.availablePackList); } private void filterEntries(String value, Stream oldEntries, @Nullable TransferableSelectionList listToUpdate) { if (listToUpdate == null) { return; } String lowerCaseValue = value.toLowerCase(Locale.ROOT); Stream filteredEntries = oldEntries.filter(packEntry -> value.isBlank() || packEntry.getId().toLowerCase(Locale.ROOT).contains(lowerCaseValue) || packEntry.getTitle().getString().toLowerCase(Locale.ROOT).contains(lowerCaseValue) || packEntry.getDescription().getString().toLowerCase(Locale.ROOT).contains(lowerCaseValue)); listToUpdate.updateList(filteredEntries, null); } @Override protected void repositionElements() { this.layout.arrangeElements(); if (this.availablePackList != null) { this.availablePackList.updateSizeAndPosition(200, this.layout.getContentHeight(), this.width / 2 - 15 - 200, this.layout.getHeaderHeight()); } if (this.selectedPackList != null) { this.selectedPackList.updateSizeAndPosition(200, this.layout.getContentHeight(), this.width / 2 + 15, this.layout.getHeaderHeight()); } } @Override public void tick() { if (this.watcher != null) { try { if (this.watcher.pollForChanges()) { this.ticksToReload = 20L; } } catch (IOException e) { LOGGER.warn("Failed to poll for directory {} changes, stopping", (Object)this.packDir); this.closeWatcher(); } } if (this.ticksToReload > 0L && --this.ticksToReload == 0L) { this.reload(); } } private void populateLists(@Nullable PackSelectionModel.EntryBase transferredEntry) { if (this.selectedPackList != null) { this.selectedPackList.updateList(this.model.getSelected(), transferredEntry); } if (this.availablePackList != null) { this.availablePackList.updateList(this.model.getUnselected(), transferredEntry); } if (this.search != null) { this.updateFilteredEntries(this.search.getValue()); } if (this.doneButton != null) { this.doneButton.active = !this.selectedPackList.children().isEmpty(); } } private void reload() { this.model.findNewPacks(); this.populateLists(null); this.ticksToReload = 0L; this.packIcons.clear(); } protected static void copyPacks(Minecraft minecraft, List files, Path targetDir) { MutableBoolean showErrorToast = new MutableBoolean(); files.forEach(pack -> { try (Stream contents = Files.walk(pack, new FileVisitOption[0]);){ contents.forEach(path -> { try { Util.copyBetweenDirs(pack.getParent(), targetDir, path); } catch (IOException e) { LOGGER.warn("Failed to copy datapack file from {} to {}", new Object[]{path, targetDir, e}); showErrorToast.setTrue(); } }); } catch (IOException e) { LOGGER.warn("Failed to copy datapack file from {} to {}", pack, (Object)targetDir); showErrorToast.setTrue(); } }); if (showErrorToast.isTrue()) { SystemToast.onPackCopyFailure(minecraft, targetDir.toString()); } } @Override public void onFilesDrop(List files) { String names = PackSelectionScreen.extractPackNames(files).collect(Collectors.joining(", ")); this.minecraft.setScreen(new ConfirmScreen(result -> { if (result) { ArrayList packCandidates = new ArrayList(files.size()); HashSet leftoverPacks = new HashSet(files); PackDetector packDetector = new PackDetector(this, this.minecraft.directoryValidator()){ @Override protected Path createZipPack(Path content) { return content; } @Override protected Path createDirectoryPack(Path content) { return content; } }; ArrayList issues = new ArrayList(); for (Path path : files) { try { Path candidate = (Path)packDetector.detectPackResources(path, issues); if (candidate == null) { LOGGER.warn("Path {} does not seem like pack", (Object)path); continue; } packCandidates.add(candidate); leftoverPacks.remove(candidate); } catch (IOException e) { LOGGER.warn("Failed to check {} for packs", (Object)path, (Object)e); } } if (!issues.isEmpty()) { this.minecraft.setScreen(NoticeWithLinkScreen.createPackSymlinkWarningScreen(() -> this.minecraft.setScreen(this))); return; } if (!packCandidates.isEmpty()) { PackSelectionScreen.copyPacks(this.minecraft, packCandidates, this.packDir); this.reload(); } if (!leftoverPacks.isEmpty()) { String leftoverNames = PackSelectionScreen.extractPackNames(leftoverPacks).collect(Collectors.joining(", ")); this.minecraft.setScreen(new AlertScreen(() -> this.minecraft.setScreen(this), Component.translatable("pack.dropRejected.title"), (Component)Component.translatable("pack.dropRejected.message", leftoverNames))); return; } } this.minecraft.setScreen(this); }, Component.translatable("pack.dropConfirm"), (Component)Component.literal(names))); } private static Stream extractPackNames(Collection files) { return files.stream().map(Path::getFileName).map(Path::toString); } /* * Enabled aggressive exception aggregation */ private Identifier loadPackIcon(TextureManager textureManager, Pack pack) { try (PackResources packResources = pack.open();){ Identifier identifier; block16: { IoSupplier resource = packResources.getRootResource("pack.png"); if (resource == null) { Identifier identifier2 = DEFAULT_ICON; return identifier2; } String id = pack.getId(); Identifier location = Identifier.withDefaultNamespace("pack/" + Util.sanitizeName(id, Identifier::validPathChar) + "/" + String.valueOf(Hashing.sha1().hashUnencodedChars((CharSequence)id)) + "/icon"); InputStream stream = resource.get(); try { NativeImage iconImage = NativeImage.read(stream); textureManager.register(location, new DynamicTexture(location::toString, iconImage)); identifier = location; if (stream == null) break block16; } catch (Throwable throwable) { if (stream != null) { try { stream.close(); } catch (Throwable throwable2) { throwable.addSuppressed(throwable2); } } throw throwable; } stream.close(); } return identifier; } catch (Exception e) { LOGGER.warn("Failed to load icon from pack {}", (Object)pack.getId(), (Object)e); return DEFAULT_ICON; } } private Identifier getPackIcon(Pack pack) { return this.packIcons.computeIfAbsent(pack.getId(), s -> this.loadPackIcon(this.minecraft.getTextureManager(), pack)); } private static class Watcher implements AutoCloseable { private final WatchService watcher; private final Path packPath; public Watcher(Path packPath) throws IOException { this.packPath = packPath; this.watcher = packPath.getFileSystem().newWatchService(); try { this.watchDir(packPath); try (DirectoryStream paths = Files.newDirectoryStream(packPath);){ for (Path path : paths) { if (!Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) continue; this.watchDir(path); } } } catch (Exception e) { this.watcher.close(); throw e; } } public static @Nullable Watcher create(Path packDir) { try { return new Watcher(packDir); } catch (IOException e) { LOGGER.warn("Failed to initialize pack directory {} monitoring", (Object)packDir, (Object)e); return null; } } private void watchDir(Path packPath) throws IOException { packPath.register(this.watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); } public boolean pollForChanges() throws IOException { WatchKey key; boolean hasChanges = false; while ((key = this.watcher.poll()) != null) { List> watchEvents = key.pollEvents(); for (WatchEvent watchEvent : watchEvents) { Path newPath; hasChanges = true; if (key.watchable() != this.packPath || watchEvent.kind() != StandardWatchEventKinds.ENTRY_CREATE || !Files.isDirectory(newPath = this.packPath.resolve((Path)watchEvent.context()), LinkOption.NOFOLLOW_LINKS)) continue; this.watchDir(newPath); } key.reset(); } return hasChanges; } @Override public void close() throws IOException { this.watcher.close(); } } }