package net.minecraft.client.resources.server; import com.google.common.collect.Lists; import com.google.common.hash.HashCode; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; import com.mojang.logging.LogUtils; import com.mojang.realmsclient.Unit; import com.mojang.util.UndashedUuid; import java.io.IOException; import java.io.UncheckedIOException; import java.net.Proxy; import java.net.URL; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.OptionalLong; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.regex.Pattern; import javax.annotation.Nullable; import net.minecraft.SharedConstants; import net.minecraft.WorldVersion; import net.minecraft.client.Minecraft; import net.minecraft.client.User; import net.minecraft.client.gui.components.toasts.SystemToast; import net.minecraft.client.main.GameConfig; import net.minecraft.network.Connection; import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.common.ServerboundResourcePackPacket; import net.minecraft.server.packs.DownloadQueue; import net.minecraft.server.packs.FilePackResources; import net.minecraft.server.packs.PackLocationInfo; import net.minecraft.server.packs.PackSelectionConfig; import net.minecraft.server.packs.PackType; import net.minecraft.server.packs.repository.Pack; import net.minecraft.server.packs.repository.PackSource; import net.minecraft.server.packs.repository.RepositorySource; import net.minecraft.util.HttpUtil; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import org.slf4j.Logger; @OnlyIn(Dist.CLIENT) public class DownloadedPackSource implements AutoCloseable { private static final Component SERVER_NAME = Component.translatable("resourcePack.server.name"); private static final Pattern SHA1 = Pattern.compile("^[a-fA-F0-9]{40}$"); static final Logger LOGGER = LogUtils.getLogger(); private static final RepositorySource EMPTY_SOURCE = p_313076_ -> { }; private static final PackSelectionConfig DOWNLOADED_PACK_SELECTION = new PackSelectionConfig(true, Pack.Position.TOP, true); private static final PackLoadFeedback LOG_ONLY_FEEDBACK = new PackLoadFeedback() { @Override public void reportUpdate(UUID p_310776_, PackLoadFeedback.Update p_309862_) { DownloadedPackSource.LOGGER.debug("Downloaded pack {} changed state to {}", p_310776_, p_309862_); } @Override public void reportFinalResult(UUID p_310730_, PackLoadFeedback.FinalResult p_311165_) { DownloadedPackSource.LOGGER.debug("Downloaded pack {} finished with state {}", p_310730_, p_311165_); } }; final Minecraft minecraft; private RepositorySource packSource = EMPTY_SOURCE; @Nullable private PackReloadConfig.Callbacks pendingReload; final ServerPackManager manager; private final DownloadQueue downloadQueue; private PackSource packType = PackSource.SERVER; PackLoadFeedback packFeedback = LOG_ONLY_FEEDBACK; private int packIdSerialNumber; public DownloadedPackSource(Minecraft p_310367_, Path p_311926_, GameConfig.UserData p_313017_) { this.minecraft = p_310367_; try { this.downloadQueue = new DownloadQueue(p_311926_); } catch (IOException ioexception) { throw new UncheckedIOException("Failed to open download queue in directory " + p_311926_, ioexception); } Executor executor = p_310367_::schedule; this.manager = new ServerPackManager(this.createDownloader(this.downloadQueue, executor, p_313017_.user, p_313017_.proxy), new PackLoadFeedback() { @Override public void reportUpdate(UUID p_311063_, PackLoadFeedback.Update p_310840_) { DownloadedPackSource.this.packFeedback.reportUpdate(p_311063_, p_310840_); } @Override public void reportFinalResult(UUID p_311502_, PackLoadFeedback.FinalResult p_310552_) { DownloadedPackSource.this.packFeedback.reportFinalResult(p_311502_, p_310552_); } }, this.createReloadConfig(), this.createUpdateScheduler(executor), ServerPackManager.PackPromptStatus.PENDING); } HttpUtil.DownloadProgressListener createDownloadNotifier(final int p_313003_) { return new HttpUtil.DownloadProgressListener() { private final SystemToast.SystemToastId toastId = new SystemToast.SystemToastId(); private Component title = Component.empty(); @Nullable private Component message = null; private int count; private int failCount; private OptionalLong totalBytes = OptionalLong.empty(); private void updateToast() { DownloadedPackSource.this.minecraft .execute(() -> SystemToast.addOrUpdate(DownloadedPackSource.this.minecraft.getToastManager(), this.toastId, this.title, this.message)); } private void updateProgress(long p_310910_) { if (this.totalBytes.isPresent()) { this.message = Component.translatable("download.pack.progress.percent", p_310910_ * 100L / this.totalBytes.getAsLong()); } else { this.message = Component.translatable("download.pack.progress.bytes", Unit.humanReadable(p_310910_)); } this.updateToast(); } @Override public void requestStart() { this.count++; this.title = Component.translatable("download.pack.title", this.count, p_313003_); this.updateToast(); DownloadedPackSource.LOGGER.debug("Starting pack {}/{} download", this.count, p_313003_); } @Override public void downloadStart(OptionalLong p_309831_) { DownloadedPackSource.LOGGER.debug("File size = {} bytes", p_309831_); this.totalBytes = p_309831_; this.updateProgress(0L); } @Override public void downloadedBytes(long p_313004_) { DownloadedPackSource.LOGGER.debug("Progress for pack {}: {} bytes", this.count, p_313004_); this.updateProgress(p_313004_); } @Override public void requestFinished(boolean p_311561_) { if (!p_311561_) { DownloadedPackSource.LOGGER.info("Pack {} failed to download", this.count); this.failCount++; } else { DownloadedPackSource.LOGGER.debug("Download ended for pack {}", this.count); } if (this.count == p_313003_) { if (this.failCount > 0) { this.title = Component.translatable("download.pack.failed", this.failCount, p_313003_); this.message = null; this.updateToast(); } else { SystemToast.forceHide(DownloadedPackSource.this.minecraft.getToastManager(), this.toastId); } } } }; } private PackDownloader createDownloader(final DownloadQueue p_310017_, final Executor p_312902_, final User p_312845_, final Proxy p_312022_) { return new PackDownloader() { private static final int MAX_PACK_SIZE_BYTES = 262144000; private static final HashFunction CACHE_HASHING_FUNCTION = Hashing.sha1(); private Map createDownloadHeaders() { WorldVersion worldversion = SharedConstants.getCurrentVersion(); return Map.of( "X-Minecraft-Username", p_312845_.getName(), "X-Minecraft-UUID", UndashedUuid.toString(p_312845_.getProfileId()), "X-Minecraft-Version", worldversion.getName(), "X-Minecraft-Version-ID", worldversion.getId(), "X-Minecraft-Pack-Format", String.valueOf(worldversion.getPackVersion(PackType.CLIENT_RESOURCES)), "User-Agent", "Minecraft Java/" + worldversion.getName() ); } @Override public void download(Map p_310177_, Consumer p_310806_) { p_310017_.downloadBatch( new DownloadQueue.BatchConfig(CACHE_HASHING_FUNCTION, 262144000, this.createDownloadHeaders(), p_312022_, DownloadedPackSource.this.createDownloadNotifier(p_310177_.size())), p_310177_ ) .thenAcceptAsync(p_310806_, p_312902_); } }; } private Runnable createUpdateScheduler(final Executor p_312638_) { return new Runnable() { private boolean scheduledInMainExecutor; private boolean hasUpdates; @Override public void run() { this.hasUpdates = true; if (!this.scheduledInMainExecutor) { this.scheduledInMainExecutor = true; p_312638_.execute(this::runAllUpdates); } } private void runAllUpdates() { while (this.hasUpdates) { this.hasUpdates = false; DownloadedPackSource.this.manager.tick(); } this.scheduledInMainExecutor = false; } }; } private PackReloadConfig createReloadConfig() { return this::startReload; } @Nullable private List loadRequestedPacks(List p_313161_) { List list = new ArrayList<>(p_313161_.size()); for (PackReloadConfig.IdAndPath packreloadconfig$idandpath : Lists.reverse(p_313161_)) { String s = String.format(Locale.ROOT, "server/%08X/%s", this.packIdSerialNumber++, packreloadconfig$idandpath.id()); Path path = packreloadconfig$idandpath.path(); PackLocationInfo packlocationinfo = new PackLocationInfo(s, SERVER_NAME, this.packType, Optional.empty()); Pack.ResourcesSupplier pack$resourcessupplier = new FilePackResources.FileResourcesSupplier(path); int i = SharedConstants.getCurrentVersion().getPackVersion(PackType.CLIENT_RESOURCES); Pack.Metadata pack$metadata = Pack.readPackMetadata(packlocationinfo, pack$resourcessupplier, i); if (pack$metadata == null) { LOGGER.warn("Invalid pack metadata in {}, ignoring all", path); return null; } list.add(new Pack(packlocationinfo, pack$resourcessupplier, pack$metadata, DOWNLOADED_PACK_SELECTION)); } return list; } public RepositorySource createRepositorySource() { return p_311800_ -> this.packSource.loadPacks(p_311800_); } private static RepositorySource configureSource(List p_310649_) { return p_310649_.isEmpty() ? EMPTY_SOURCE : p_310649_::forEach; } private void startReload(PackReloadConfig.Callbacks p_310818_) { this.pendingReload = p_310818_; List list = p_310818_.packsToLoad(); List list1 = this.loadRequestedPacks(list); if (list1 == null) { p_310818_.onFailure(false); List list2 = p_310818_.packsToLoad(); list1 = this.loadRequestedPacks(list2); if (list1 == null) { LOGGER.warn("Double failure in loading server packs"); list1 = List.of(); } } this.packSource = configureSource(list1); this.minecraft.reloadResourcePacks(); } public void onRecovery() { if (this.pendingReload != null) { this.pendingReload.onFailure(false); List list = this.loadRequestedPacks(this.pendingReload.packsToLoad()); if (list == null) { LOGGER.warn("Double failure in loading server packs"); list = List.of(); } this.packSource = configureSource(list); } } public void onRecoveryFailure() { if (this.pendingReload != null) { this.pendingReload.onFailure(true); this.pendingReload = null; this.packSource = EMPTY_SOURCE; } } public void onReloadSuccess() { if (this.pendingReload != null) { this.pendingReload.onSuccess(); this.pendingReload = null; } } @Nullable private static HashCode tryParseSha1Hash(@Nullable String p_312783_) { return p_312783_ != null && SHA1.matcher(p_312783_).matches() ? HashCode.fromString(p_312783_.toLowerCase(Locale.ROOT)) : null; } public void pushPack(UUID p_312781_, URL p_312716_, @Nullable String p_312757_) { HashCode hashcode = tryParseSha1Hash(p_312757_); this.manager.pushPack(p_312781_, p_312716_, hashcode); } public void pushLocalPack(UUID p_310453_, Path p_312255_) { this.manager.pushLocalPack(p_310453_, p_312255_); } public void popPack(UUID p_312698_) { this.manager.popPack(p_312698_); } public void popAll() { this.manager.popAll(); } private static PackLoadFeedback createPackResponseSender(final Connection p_312565_) { return new PackLoadFeedback() { @Override public void reportUpdate(UUID p_310120_, PackLoadFeedback.Update p_313074_) { DownloadedPackSource.LOGGER.debug("Pack {} changed status to {}", p_310120_, p_313074_); ServerboundResourcePackPacket.Action serverboundresourcepackpacket$action = switch (p_313074_) { case ACCEPTED -> ServerboundResourcePackPacket.Action.ACCEPTED; case DOWNLOADED -> ServerboundResourcePackPacket.Action.DOWNLOADED; }; p_312565_.send(new ServerboundResourcePackPacket(p_310120_, serverboundresourcepackpacket$action)); } @Override public void reportFinalResult(UUID p_310323_, PackLoadFeedback.FinalResult p_312396_) { DownloadedPackSource.LOGGER.debug("Pack {} changed status to {}", p_310323_, p_312396_); ServerboundResourcePackPacket.Action serverboundresourcepackpacket$action = switch (p_312396_) { case APPLIED -> ServerboundResourcePackPacket.Action.SUCCESSFULLY_LOADED; case DOWNLOAD_FAILED -> ServerboundResourcePackPacket.Action.FAILED_DOWNLOAD; case DECLINED -> ServerboundResourcePackPacket.Action.DECLINED; case DISCARDED -> ServerboundResourcePackPacket.Action.DISCARDED; case ACTIVATION_FAILED -> ServerboundResourcePackPacket.Action.FAILED_RELOAD; }; p_312565_.send(new ServerboundResourcePackPacket(p_310323_, serverboundresourcepackpacket$action)); } }; } public void configureForServerControl(Connection p_310083_, ServerPackManager.PackPromptStatus p_309566_) { this.packType = PackSource.SERVER; this.packFeedback = createPackResponseSender(p_310083_); switch (p_309566_) { case ALLOWED: this.manager.allowServerPacks(); break; case DECLINED: this.manager.rejectServerPacks(); break; case PENDING: this.manager.resetPromptStatus(); } } public void configureForLocalWorld() { this.packType = PackSource.WORLD; this.packFeedback = LOG_ONLY_FEEDBACK; this.manager.allowServerPacks(); } public void allowServerPacks() { this.manager.allowServerPacks(); } public void rejectServerPacks() { this.manager.rejectServerPacks(); } public CompletableFuture waitForPackFeedback(final UUID p_309645_) { final CompletableFuture completablefuture = new CompletableFuture<>(); final PackLoadFeedback packloadfeedback = this.packFeedback; this.packFeedback = new PackLoadFeedback() { @Override public void reportUpdate(UUID p_312518_, PackLoadFeedback.Update p_310008_) { packloadfeedback.reportUpdate(p_312518_, p_310008_); } @Override public void reportFinalResult(UUID p_310518_, PackLoadFeedback.FinalResult p_310501_) { if (p_309645_.equals(p_310518_)) { DownloadedPackSource.this.packFeedback = packloadfeedback; if (p_310501_ == PackLoadFeedback.FinalResult.APPLIED) { completablefuture.complete(null); } else { completablefuture.completeExceptionally(new IllegalStateException("Failed to apply pack " + p_310518_ + ", reason: " + p_310501_)); } } packloadfeedback.reportFinalResult(p_310518_, p_310501_); } }; return completablefuture; } public void cleanupAfterDisconnect() { this.manager.popAll(); this.packFeedback = LOG_ONLY_FEEDBACK; this.manager.resetPromptStatus(); } @Override public void close() throws IOException { this.downloadQueue.close(); } }