Code/net/minecraft/client/gui/screens/packs/PackSelectionScreen.java

382 lines
17 KiB
Java
Raw Permalink Normal View History

2025-07-01 06:20:03 +00:00
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 javax.annotation.Nullable;
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.events.GuiEventListener;
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.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 net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.apache.commons.lang3.mutable.MutableBoolean;
import org.slf4j.Logger;
@OnlyIn(Dist.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.newHashMap();
public PackSelectionScreen(PackRepository p_275398_, Consumer<PackRepository> p_275659_, Path p_275522_, Component p_275337_) {
super(p_275337_);
this.model = new PackSelectionModel(this::populateLists, this::getPackIcon, p_275398_, p_275659_);
this.packDir = p_275522_;
this.watcher = PackSelectionScreen.Watcher.create(p_275522_);
}
@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) {
}
}
}
@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 linearlayout1 = this.layout.addToFooter(LinearLayout.horizontal().spacing(8));
linearlayout1.addChild(
Button.builder(OPEN_PACK_FOLDER_TITLE, p_340814_ -> Util.getPlatform().openPath(this.packDir)).tooltip(Tooltip.create(DIRECTORY_BUTTON_TOOLTIP)).build()
);
this.doneButton = linearlayout1.addChild(Button.builder(CommonComponents.GUI_DONE, p_100036_ -> this.onClose()).build());
this.reload();
this.layout.visitWidgets(p_325392_ -> {
AbstractWidget abstractwidget = this.addRenderableWidget(p_325392_);
});
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 ioexception) {
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 p_100014_, Stream<PackSelectionModel.Entry> p_100015_) {
p_100014_.children().clear();
TransferableSelectionList.PackEntry transferableselectionlist$packentry = p_100014_.getSelected();
String s = transferableselectionlist$packentry == null ? "" : transferableselectionlist$packentry.getPackId();
p_100014_.setSelected(null);
p_100015_.forEach(
p_389318_ -> {
TransferableSelectionList.PackEntry transferableselectionlist$packentry1 = new TransferableSelectionList.PackEntry(
this.minecraft, p_100014_, p_389318_
);
p_100014_.children().add(transferableselectionlist$packentry1);
if (p_389318_.getId().equals(s)) {
p_100014_.setSelected(transferableselectionlist$packentry1);
}
}
);
}
public void updateFocus(TransferableSelectionList p_265419_) {
TransferableSelectionList transferableselectionlist = this.selectedPackList == p_265419_ ? 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 p_100000_, List<Path> p_100001_, Path p_100002_) {
MutableBoolean mutableboolean = new MutableBoolean();
p_100001_.forEach(p_170009_ -> {
try (Stream<Path> stream = Files.walk(p_170009_)) {
stream.forEach(p_170005_ -> {
try {
Util.copyBetweenDirs(p_170009_.getParent(), p_100002_, p_170005_);
} catch (IOException ioexception1) {
LOGGER.warn("Failed to copy datapack file from {} to {}", p_170005_, p_100002_, ioexception1);
mutableboolean.setTrue();
}
});
} catch (IOException ioexception) {
LOGGER.warn("Failed to copy datapack file from {} to {}", p_170009_, p_100002_);
mutableboolean.setTrue();
}
});
if (mutableboolean.isTrue()) {
SystemToast.onPackCopyFailure(p_100000_, p_100002_.toString());
}
}
@Override
public void onFilesDrop(List<Path> p_100029_) {
String s = extractPackNames(p_100029_).collect(Collectors.joining(", "));
this.minecraft
.setScreen(
new ConfirmScreen(
p_296193_ -> {
if (p_296193_) {
List<Path> list = new ArrayList<>(p_100029_.size());
Set<Path> set = new HashSet<>(p_100029_);
PackDetector<Path> packdetector = new PackDetector<Path>(this.minecraft.directoryValidator()) {
protected Path createZipPack(Path p_298689_) {
return p_298689_;
}
protected Path createDirectoryPack(Path p_298650_) {
return p_298650_;
}
};
List<ForbiddenSymlinkInfo> list1 = new ArrayList<>();
for (Path path : p_100029_) {
try {
Path path1 = packdetector.detectPackResources(path, list1);
if (path1 == null) {
LOGGER.warn("Path {} does not seem like pack", path);
} else {
list.add(path1);
set.remove(path1);
}
} catch (IOException ioexception) {
LOGGER.warn("Failed to check {} for packs", path, ioexception);
}
}
if (!list1.isEmpty()) {
this.minecraft.setScreen(NoticeWithLinkScreen.createPackSymlinkWarningScreen(() -> this.minecraft.setScreen(this)));
return;
}
if (!list.isEmpty()) {
copyPacks(this.minecraft, list, this.packDir);
this.reload();
}
if (!set.isEmpty()) {
String s1 = extractPackNames(set).collect(Collectors.joining(", "));
this.minecraft
.setScreen(
new AlertScreen(
() -> this.minecraft.setScreen(this),
Component.translatable("pack.dropRejected.title"),
Component.translatable("pack.dropRejected.message", s1)
)
);
return;
}
}
this.minecraft.setScreen(this);
},
Component.translatable("pack.dropConfirm"),
Component.literal(s)
)
);
}
private static Stream<String> extractPackNames(Collection<Path> p_300507_) {
return p_300507_.stream().map(Path::getFileName).map(Path::toString);
}
private ResourceLocation loadPackIcon(TextureManager p_100017_, Pack p_100018_) {
try {
ResourceLocation resourcelocation1;
try (PackResources packresources = p_100018_.open()) {
IoSupplier<InputStream> iosupplier = packresources.getRootResource("pack.png");
if (iosupplier == null) {
return DEFAULT_ICON;
}
String s = p_100018_.getId();
ResourceLocation resourcelocation = ResourceLocation.withDefaultNamespace(
"pack/" + Util.sanitizeName(s, ResourceLocation::validPathChar) + "/" + Hashing.sha1().hashUnencodedChars(s) + "/icon"
);
try (InputStream inputstream = iosupplier.get()) {
NativeImage nativeimage = NativeImage.read(inputstream);
p_100017_.register(resourcelocation, new DynamicTexture(resourcelocation::toString, nativeimage));
resourcelocation1 = resourcelocation;
}
}
return resourcelocation1;
} catch (Exception exception) {
LOGGER.warn("Failed to load icon from pack {}", p_100018_.getId(), exception);
return DEFAULT_ICON;
}
}
private ResourceLocation getPackIcon(Pack p_99990_) {
return this.packIcons.computeIfAbsent(p_99990_.getId(), p_280879_ -> this.loadPackIcon(this.minecraft.getTextureManager(), p_99990_));
}
@OnlyIn(Dist.CLIENT)
static class Watcher implements AutoCloseable {
private final WatchService watcher;
private final Path packPath;
public Watcher(Path p_250327_) throws IOException {
this.packPath = p_250327_;
this.watcher = p_250327_.getFileSystem().newWatchService();
try {
this.watchDir(p_250327_);
try (DirectoryStream<Path> directorystream = Files.newDirectoryStream(p_250327_)) {
for (Path path : directorystream) {
if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
this.watchDir(path);
}
}
}
} catch (Exception exception) {
this.watcher.close();
throw exception;
}
}
@Nullable
public static PackSelectionScreen.Watcher create(Path p_252119_) {
try {
return new PackSelectionScreen.Watcher(p_252119_);
} catch (IOException ioexception) {
PackSelectionScreen.LOGGER.warn("Failed to initialize pack directory {} monitoring", p_252119_, ioexception);
return null;
}
}
private void watchDir(Path p_100050_) throws IOException {
p_100050_.register(this.watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
}
public boolean pollForChanges() throws IOException {
boolean flag = false;
WatchKey watchkey;
while ((watchkey = this.watcher.poll()) != null) {
for (WatchEvent<?> watchevent : watchkey.pollEvents()) {
flag = true;
if (watchkey.watchable() == this.packPath && watchevent.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
Path path = this.packPath.resolve((Path)watchevent.context());
if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
this.watchDir(path);
}
}
}
watchkey.reset();
}
return flag;
}
@Override
public void close() throws IOException {
this.watcher.close();
}
}
}