minecraft-src/net/minecraft/client/gui/screens/packs/PackSelectionScreen.java
2025-07-04 03:45:38 +03:00

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();
}
}
}