426 lines
14 KiB
Java
426 lines
14 KiB
Java
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.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.Map;
|
|
import java.util.Set;
|
|
import java.util.function.Consumer;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Stream;
|
|
import net.fabricmc.api.EnvType;
|
|
import net.fabricmc.api.Environment;
|
|
import net.minecraft.ChatFormatting;
|
|
import net.minecraft.Util;
|
|
import net.minecraft.client.Minecraft;
|
|
import net.minecraft.client.gui.ComponentPath;
|
|
import net.minecraft.client.gui.components.AbstractWidget;
|
|
import net.minecraft.client.gui.components.Button;
|
|
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.Entry;
|
|
import net.minecraft.client.gui.screens.packs.TransferableSelectionList.PackEntry;
|
|
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.ResourceLocation;
|
|
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.world.level.validation.ForbiddenSymlinkInfo;
|
|
import org.apache.commons.lang3.mutable.MutableBoolean;
|
|
import org.jetbrains.annotations.Nullable;
|
|
import org.slf4j.Logger;
|
|
|
|
@Environment(EnvType.CLIENT)
|
|
public class PackSelectionScreen extends Screen {
|
|
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 int LIST_WIDTH = 200;
|
|
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 ResourceLocation DEFAULT_ICON = ResourceLocation.withDefaultNamespace("textures/misc/unknown_pack.png");
|
|
private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this);
|
|
private final PackSelectionModel model;
|
|
@Nullable
|
|
private PackSelectionScreen.Watcher watcher;
|
|
private long ticksToReload;
|
|
private TransferableSelectionList availablePackList;
|
|
private TransferableSelectionList selectedPackList;
|
|
private final Path packDir;
|
|
private Button doneButton;
|
|
private final Map<String, ResourceLocation> packIcons = Maps.<String, ResourceLocation>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 = PackSelectionScreen.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 var2) {
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void init() {
|
|
LinearLayout linearLayout = this.layout.addToHeader(LinearLayout.vertical().spacing(5));
|
|
linearLayout.defaultCellSetting().alignHorizontallyCenter();
|
|
linearLayout.addChild(new StringWidget(this.getTitle(), this.font));
|
|
linearLayout.addChild(new StringWidget(DRAG_AND_DROP, this.font));
|
|
this.availablePackList = this.addRenderableWidget(new TransferableSelectionList(this.minecraft, this, 200, this.height - 66, AVAILABLE_TITLE));
|
|
this.selectedPackList = this.addRenderableWidget(new TransferableSelectionList(this.minecraft, this, 200, this.height - 66, SELECTED_TITLE));
|
|
LinearLayout linearLayout2 = this.layout.addToFooter(LinearLayout.horizontal().spacing(8));
|
|
linearLayout2.addChild(
|
|
Button.builder(OPEN_PACK_FOLDER_TITLE, button -> Util.getPlatform().openPath(this.packDir)).tooltip(Tooltip.create(DIRECTORY_BUTTON_TOOLTIP)).build()
|
|
);
|
|
this.doneButton = linearLayout2.addChild(Button.builder(CommonComponents.GUI_DONE, button -> this.onClose()).build());
|
|
this.reload();
|
|
this.layout.visitWidgets(guiEventListener -> {
|
|
AbstractWidget var10000 = this.addRenderableWidget(guiEventListener);
|
|
});
|
|
this.repositionElements();
|
|
}
|
|
|
|
@Override
|
|
protected void repositionElements() {
|
|
this.layout.arrangeElements();
|
|
this.availablePackList.updateSize(200, this.layout);
|
|
this.availablePackList.setX(this.width / 2 - 15 - 200);
|
|
this.selectedPackList.updateSize(200, this.layout);
|
|
this.selectedPackList.setX(this.width / 2 + 15);
|
|
}
|
|
|
|
@Override
|
|
public void tick() {
|
|
if (this.watcher != null) {
|
|
try {
|
|
if (this.watcher.pollForChanges()) {
|
|
this.ticksToReload = 20L;
|
|
}
|
|
} catch (IOException var2) {
|
|
LOGGER.warn("Failed to poll for directory {} changes, stopping", this.packDir);
|
|
this.closeWatcher();
|
|
}
|
|
}
|
|
|
|
if (this.ticksToReload > 0L && --this.ticksToReload == 0L) {
|
|
this.reload();
|
|
}
|
|
}
|
|
|
|
private void populateLists() {
|
|
this.updateList(this.selectedPackList, this.model.getSelected());
|
|
this.updateList(this.availablePackList, this.model.getUnselected());
|
|
this.doneButton.active = !this.selectedPackList.children().isEmpty();
|
|
}
|
|
|
|
private void updateList(TransferableSelectionList selection, Stream<Entry> models) {
|
|
selection.children().clear();
|
|
PackEntry packEntry = selection.getSelected();
|
|
String string = packEntry == null ? "" : packEntry.getPackId();
|
|
selection.setSelected(null);
|
|
models.forEach(entry -> {
|
|
PackEntry packEntryx = new PackEntry(this.minecraft, selection, entry);
|
|
selection.children().add(packEntryx);
|
|
if (entry.getId().equals(string)) {
|
|
selection.setSelected(packEntryx);
|
|
}
|
|
});
|
|
}
|
|
|
|
public void updateFocus(TransferableSelectionList selection) {
|
|
TransferableSelectionList transferableSelectionList = this.selectedPackList == selection ? this.availablePackList : this.selectedPackList;
|
|
this.changeFocus(ComponentPath.path(transferableSelectionList.getFirstElement(), transferableSelectionList, this));
|
|
}
|
|
|
|
public void clearSelected() {
|
|
this.selectedPackList.setSelected(null);
|
|
this.availablePackList.setSelected(null);
|
|
}
|
|
|
|
private void reload() {
|
|
this.model.findNewPacks();
|
|
this.populateLists();
|
|
this.ticksToReload = 0L;
|
|
this.packIcons.clear();
|
|
}
|
|
|
|
protected static void copyPacks(Minecraft minecraft, List<Path> packs, Path outDir) {
|
|
MutableBoolean mutableBoolean = new MutableBoolean();
|
|
packs.forEach(path2 -> {
|
|
try {
|
|
Stream<Path> stream = Files.walk(path2);
|
|
|
|
try {
|
|
stream.forEach(path3 -> {
|
|
try {
|
|
Util.copyBetweenDirs(path2.getParent(), outDir, path3);
|
|
} catch (IOException var5) {
|
|
LOGGER.warn("Failed to copy datapack file from {} to {}", path3, outDir, var5);
|
|
mutableBoolean.setTrue();
|
|
}
|
|
});
|
|
} catch (Throwable var7) {
|
|
if (stream != null) {
|
|
try {
|
|
stream.close();
|
|
} catch (Throwable var6) {
|
|
var7.addSuppressed(var6);
|
|
}
|
|
}
|
|
|
|
throw var7;
|
|
}
|
|
|
|
if (stream != null) {
|
|
stream.close();
|
|
}
|
|
} catch (IOException var8) {
|
|
LOGGER.warn("Failed to copy datapack file from {} to {}", path2, outDir);
|
|
mutableBoolean.setTrue();
|
|
}
|
|
});
|
|
if (mutableBoolean.isTrue()) {
|
|
SystemToast.onPackCopyFailure(minecraft, outDir.toString());
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onFilesDrop(List<Path> packs) {
|
|
String string = (String)extractPackNames(packs).collect(Collectors.joining(", "));
|
|
this.minecraft
|
|
.setScreen(
|
|
new ConfirmScreen(
|
|
bl -> {
|
|
if (bl) {
|
|
List<Path> list2 = new ArrayList(packs.size());
|
|
Set<Path> set = new HashSet(packs);
|
|
PackDetector<Path> packDetector = new PackDetector<Path>(this.minecraft.directoryValidator()) {
|
|
protected Path createZipPack(Path path) {
|
|
return path;
|
|
}
|
|
|
|
protected Path createDirectoryPack(Path path) {
|
|
return path;
|
|
}
|
|
};
|
|
List<ForbiddenSymlinkInfo> list3 = new ArrayList();
|
|
|
|
for (Path path : packs) {
|
|
try {
|
|
Path path2 = packDetector.detectPackResources(path, list3);
|
|
if (path2 == null) {
|
|
LOGGER.warn("Path {} does not seem like pack", path);
|
|
} else {
|
|
list2.add(path2);
|
|
set.remove(path2);
|
|
}
|
|
} catch (IOException var10) {
|
|
LOGGER.warn("Failed to check {} for packs", path, var10);
|
|
}
|
|
}
|
|
|
|
if (!list3.isEmpty()) {
|
|
this.minecraft.setScreen(NoticeWithLinkScreen.createPackSymlinkWarningScreen(() -> this.minecraft.setScreen(this)));
|
|
return;
|
|
}
|
|
|
|
if (!list2.isEmpty()) {
|
|
copyPacks(this.minecraft, list2, this.packDir);
|
|
this.reload();
|
|
}
|
|
|
|
if (!set.isEmpty()) {
|
|
String stringx = (String)extractPackNames(set).collect(Collectors.joining(", "));
|
|
this.minecraft
|
|
.setScreen(
|
|
new AlertScreen(
|
|
() -> this.minecraft.setScreen(this),
|
|
Component.translatable("pack.dropRejected.title"),
|
|
Component.translatable("pack.dropRejected.message", stringx)
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
this.minecraft.setScreen(this);
|
|
},
|
|
Component.translatable("pack.dropConfirm"),
|
|
Component.literal(string)
|
|
)
|
|
);
|
|
}
|
|
|
|
private static Stream<String> extractPackNames(Collection<Path> paths) {
|
|
return paths.stream().map(Path::getFileName).map(Path::toString);
|
|
}
|
|
|
|
private ResourceLocation loadPackIcon(TextureManager textureManager, Pack pack) {
|
|
try {
|
|
ResourceLocation var9;
|
|
try (PackResources packResources = pack.open()) {
|
|
IoSupplier<InputStream> ioSupplier = packResources.getRootResource("pack.png");
|
|
if (ioSupplier == null) {
|
|
return DEFAULT_ICON;
|
|
}
|
|
|
|
String string = pack.getId();
|
|
ResourceLocation resourceLocation = ResourceLocation.withDefaultNamespace(
|
|
"pack/" + Util.sanitizeName(string, ResourceLocation::validPathChar) + "/" + Hashing.sha1().hashUnencodedChars(string) + "/icon"
|
|
);
|
|
InputStream inputStream = ioSupplier.get();
|
|
|
|
try {
|
|
NativeImage nativeImage = NativeImage.read(inputStream);
|
|
textureManager.register(resourceLocation, new DynamicTexture(resourceLocation::toString, nativeImage));
|
|
var9 = resourceLocation;
|
|
} catch (Throwable var12) {
|
|
if (inputStream != null) {
|
|
try {
|
|
inputStream.close();
|
|
} catch (Throwable var11) {
|
|
var12.addSuppressed(var11);
|
|
}
|
|
}
|
|
|
|
throw var12;
|
|
}
|
|
|
|
if (inputStream != null) {
|
|
inputStream.close();
|
|
}
|
|
}
|
|
|
|
return var9;
|
|
} catch (Exception var14) {
|
|
LOGGER.warn("Failed to load icon from pack {}", pack.getId(), var14);
|
|
return DEFAULT_ICON;
|
|
}
|
|
}
|
|
|
|
private ResourceLocation getPackIcon(Pack pack) {
|
|
return (ResourceLocation)this.packIcons.computeIfAbsent(pack.getId(), string -> this.loadPackIcon(this.minecraft.getTextureManager(), pack));
|
|
}
|
|
|
|
@Environment(EnvType.CLIENT)
|
|
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);
|
|
DirectoryStream<Path> directoryStream = Files.newDirectoryStream(packPath);
|
|
|
|
try {
|
|
for (Path path : directoryStream) {
|
|
if (Files.isDirectory(path, new LinkOption[]{LinkOption.NOFOLLOW_LINKS})) {
|
|
this.watchDir(path);
|
|
}
|
|
}
|
|
} catch (Throwable var6) {
|
|
if (directoryStream != null) {
|
|
try {
|
|
directoryStream.close();
|
|
} catch (Throwable var5) {
|
|
var6.addSuppressed(var5);
|
|
}
|
|
}
|
|
|
|
throw var6;
|
|
}
|
|
|
|
if (directoryStream != null) {
|
|
directoryStream.close();
|
|
}
|
|
} catch (Exception var7) {
|
|
this.watcher.close();
|
|
throw var7;
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
public static PackSelectionScreen.Watcher create(Path packPath) {
|
|
try {
|
|
return new PackSelectionScreen.Watcher(packPath);
|
|
} catch (IOException var2) {
|
|
PackSelectionScreen.LOGGER.warn("Failed to initialize pack directory {} monitoring", packPath, var2);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private void watchDir(Path path) throws IOException {
|
|
path.register(this.watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
|
|
}
|
|
|
|
public boolean pollForChanges() throws IOException {
|
|
boolean bl = false;
|
|
|
|
WatchKey watchKey;
|
|
while ((watchKey = this.watcher.poll()) != null) {
|
|
for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
|
|
bl = true;
|
|
if (watchKey.watchable() == this.packPath && watchEvent.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
|
|
Path path = this.packPath.resolve((Path)watchEvent.context());
|
|
if (Files.isDirectory(path, new LinkOption[]{LinkOption.NOFOLLOW_LINKS})) {
|
|
this.watchDir(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
watchKey.reset();
|
|
}
|
|
|
|
return bl;
|
|
}
|
|
|
|
public void close() throws IOException {
|
|
this.watcher.close();
|
|
}
|
|
}
|
|
}
|