394 lines
17 KiB
Java
394 lines
17 KiB
Java
/*
|
|
* 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<String, Identifier> packIcons = Maps.newHashMap();
|
|
|
|
public PackSelectionScreen(PackRepository repository, Consumer<PackRepository> 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<PackSelectionModel.Entry> oldEntries, @Nullable TransferableSelectionList listToUpdate) {
|
|
if (listToUpdate == null) {
|
|
return;
|
|
}
|
|
String lowerCaseValue = value.toLowerCase(Locale.ROOT);
|
|
Stream<PackSelectionModel.Entry> 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<Path> files, Path targetDir) {
|
|
MutableBoolean showErrorToast = new MutableBoolean();
|
|
files.forEach(pack -> {
|
|
try (Stream<Path> 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<Path> files) {
|
|
String names = PackSelectionScreen.extractPackNames(files).collect(Collectors.joining(", "));
|
|
this.minecraft.setScreen(new ConfirmScreen(result -> {
|
|
if (result) {
|
|
ArrayList<Path> packCandidates = new ArrayList<Path>(files.size());
|
|
HashSet<Path> leftoverPacks = new HashSet<Path>(files);
|
|
PackDetector<Path> packDetector = new PackDetector<Path>(this, this.minecraft.directoryValidator()){
|
|
|
|
@Override
|
|
protected Path createZipPack(Path content) {
|
|
return content;
|
|
}
|
|
|
|
@Override
|
|
protected Path createDirectoryPack(Path content) {
|
|
return content;
|
|
}
|
|
};
|
|
ArrayList<ForbiddenSymlinkInfo> issues = new ArrayList<ForbiddenSymlinkInfo>();
|
|
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<String> extractPackNames(Collection<Path> 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<InputStream> 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<Path> 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<WatchEvent<?>> 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();
|
|
}
|
|
}
|
|
}
|
|
|