/* * Decompiled with CFR 0.152. * * Could not load the following classes: * com.google.common.collect.Lists * com.google.common.hash.HashCode * com.google.common.hash.HashFunction * com.google.common.hash.Hashing * com.mojang.logging.LogUtils * com.mojang.util.UndashedUuid * org.jspecify.annotations.Nullable * org.slf4j.Logger */ 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 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.client.resources.server.PackDownloader; import net.minecraft.client.resources.server.PackLoadFeedback; import net.minecraft.client.resources.server.PackReloadConfig; import net.minecraft.client.resources.server.ServerPackManager; 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.metadata.pack.PackFormat; 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 org.jspecify.annotations.Nullable; import org.slf4j.Logger; 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}$"); private static final Logger LOGGER = LogUtils.getLogger(); private static final RepositorySource EMPTY_SOURCE = result -> {}; 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 id, PackLoadFeedback.Update update) { LOGGER.debug("Downloaded pack {} changed state to {}", (Object)id, (Object)update); } @Override public void reportFinalResult(UUID id, PackLoadFeedback.FinalResult result) { LOGGER.debug("Downloaded pack {} finished with state {}", (Object)id, (Object)result); } }; private final Minecraft minecraft; private RepositorySource packSource = EMPTY_SOURCE; private @Nullable PackReloadConfig.Callbacks pendingReload; private final ServerPackManager manager; private final DownloadQueue downloadQueue; private PackSource packType = PackSource.SERVER; private PackLoadFeedback packFeedback = LOG_ONLY_FEEDBACK; private int packIdSerialNumber; public DownloadedPackSource(Minecraft minecraft, Path packCache, GameConfig.UserData user) { this.minecraft = minecraft; try { this.downloadQueue = new DownloadQueue(packCache); } catch (IOException e) { throw new UncheckedIOException("Failed to open download queue in directory " + String.valueOf(packCache), e); } Executor executor = minecraft::schedule; this.manager = new ServerPackManager(this.createDownloader(this.downloadQueue, executor, user.user, user.proxy), new PackLoadFeedback(){ @Override public void reportUpdate(UUID id, PackLoadFeedback.Update result) { DownloadedPackSource.this.packFeedback.reportUpdate(id, result); } @Override public void reportFinalResult(UUID id, PackLoadFeedback.FinalResult result) { DownloadedPackSource.this.packFeedback.reportFinalResult(id, result); } }, this.createReloadConfig(), this.createUpdateScheduler(executor), ServerPackManager.PackPromptStatus.PENDING); } private HttpUtil.DownloadProgressListener createDownloadNotifier(final int totalCount) { return new HttpUtil.DownloadProgressListener(){ private final SystemToast.SystemToastId toastId = new SystemToast.SystemToastId(); private Component title = Component.empty(); private @Nullable 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 bytesSoFar) { this.message = this.totalBytes.isPresent() ? Component.translatable("download.pack.progress.percent", bytesSoFar * 100L / this.totalBytes.getAsLong()) : Component.translatable("download.pack.progress.bytes", Unit.humanReadable(bytesSoFar)); this.updateToast(); } @Override public void requestStart() { ++this.count; this.title = Component.translatable("download.pack.title", this.count, totalCount); this.updateToast(); LOGGER.debug("Starting pack {}/{} download", (Object)this.count, (Object)totalCount); } @Override public void downloadStart(OptionalLong sizeBytes) { LOGGER.debug("File size = {} bytes", (Object)sizeBytes); this.totalBytes = sizeBytes; this.updateProgress(0L); } @Override public void downloadedBytes(long bytesSoFar) { LOGGER.debug("Progress for pack {}: {} bytes", (Object)this.count, (Object)bytesSoFar); this.updateProgress(bytesSoFar); } @Override public void requestFinished(boolean success) { if (!success) { LOGGER.info("Pack {} failed to download", (Object)this.count); ++this.failCount; } else { LOGGER.debug("Download ended for pack {}", (Object)this.count); } if (this.count == totalCount) { if (this.failCount > 0) { this.title = Component.translatable("download.pack.failed", this.failCount, totalCount); this.message = null; this.updateToast(); } else { SystemToast.forceHide(DownloadedPackSource.this.minecraft.getToastManager(), this.toastId); } } } }; } private PackDownloader createDownloader(final DownloadQueue downloadQueue, final Executor mainThreadExecutor, final User user, final Proxy proxy) { return new PackDownloader(){ private static final int MAX_PACK_SIZE_BYTES = 0xFA00000; private static final HashFunction CACHE_HASHING_FUNCTION = Hashing.sha1(); private Map createDownloadHeaders() { WorldVersion version = SharedConstants.getCurrentVersion(); return Map.of("X-Minecraft-Username", user.getName(), "X-Minecraft-UUID", UndashedUuid.toString((UUID)user.getProfileId()), "X-Minecraft-Version", version.name(), "X-Minecraft-Version-ID", version.id(), "X-Minecraft-Pack-Format", String.valueOf(version.packVersion(PackType.CLIENT_RESOURCES)), "User-Agent", "Minecraft Java/" + version.name()); } @Override public void download(Map requests, Consumer output) { downloadQueue.downloadBatch(new DownloadQueue.BatchConfig(CACHE_HASHING_FUNCTION, 0xFA00000, this.createDownloadHeaders(), proxy, DownloadedPackSource.this.createDownloadNotifier(requests.size())), requests).thenAcceptAsync((Consumer)output, mainThreadExecutor); } }; } private Runnable createUpdateScheduler(final Executor mainThreadExecutor) { return new Runnable(){ private boolean scheduledInMainExecutor; private boolean hasUpdates; @Override public void run() { this.hasUpdates = true; if (!this.scheduledInMainExecutor) { this.scheduledInMainExecutor = true; mainThreadExecutor.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; } private @Nullable List loadRequestedPacks(List packsToLoad) { ArrayList packs = new ArrayList(packsToLoad.size()); for (PackReloadConfig.IdAndPath idAndPath : Lists.reverse(packsToLoad)) { PackFormat currentPackVersion; FilePackResources.FileResourcesSupplier resources; String name = String.format(Locale.ROOT, "server/%08X/%s", this.packIdSerialNumber++, idAndPath.id()); Path path = idAndPath.path(); PackLocationInfo packLocationInfo = new PackLocationInfo(name, SERVER_NAME, this.packType, Optional.empty()); Pack.Metadata metadata = Pack.readPackMetadata(packLocationInfo, resources = new FilePackResources.FileResourcesSupplier(path), currentPackVersion = SharedConstants.getCurrentVersion().packVersion(PackType.CLIENT_RESOURCES), PackType.CLIENT_RESOURCES); if (metadata == null) { LOGGER.warn("Invalid pack metadata in {}, ignoring all", (Object)path); return null; } packs.add(new Pack(packLocationInfo, resources, metadata, DOWNLOADED_PACK_SELECTION)); } return packs; } public RepositorySource createRepositorySource() { return output -> this.packSource.loadPacks(output); } private static RepositorySource configureSource(List packs) { if (packs.isEmpty()) { return EMPTY_SOURCE; } return packs::forEach; } private void startReload(PackReloadConfig.Callbacks callbacks) { this.pendingReload = callbacks; List normalPacks = callbacks.packsToLoad(); List packs = this.loadRequestedPacks(normalPacks); if (packs == null) { callbacks.onFailure(false); List recoveryPacks = callbacks.packsToLoad(); packs = this.loadRequestedPacks(recoveryPacks); if (packs == null) { LOGGER.warn("Double failure in loading server packs"); packs = List.of(); } } this.packSource = DownloadedPackSource.configureSource(packs); this.minecraft.reloadResourcePacks(); } public void onRecovery() { if (this.pendingReload != null) { this.pendingReload.onFailure(false); List packs = this.loadRequestedPacks(this.pendingReload.packsToLoad()); if (packs == null) { LOGGER.warn("Double failure in loading server packs"); packs = List.of(); } this.packSource = DownloadedPackSource.configureSource(packs); } } 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; } } private static @Nullable HashCode tryParseSha1Hash(@Nullable String hash) { if (hash != null && SHA1.matcher(hash).matches()) { return HashCode.fromString((String)hash.toLowerCase(Locale.ROOT)); } return null; } public void pushPack(UUID id, URL url, @Nullable String hash) { HashCode parsedHash = DownloadedPackSource.tryParseSha1Hash(hash); this.manager.pushPack(id, url, parsedHash); } public void pushLocalPack(UUID id, Path path) { this.manager.pushLocalPack(id, path); } public void popPack(UUID id) { this.manager.popPack(id); } public void popAll() { this.manager.popAll(); } private static PackLoadFeedback createPackResponseSender(final Connection connection) { return new PackLoadFeedback(){ @Override public void reportUpdate(UUID id, PackLoadFeedback.Update result) { LOGGER.debug("Pack {} changed status to {}", (Object)id, (Object)result); ServerboundResourcePackPacket.Action response = switch (result) { default -> throw new MatchException(null, null); case PackLoadFeedback.Update.ACCEPTED -> ServerboundResourcePackPacket.Action.ACCEPTED; case PackLoadFeedback.Update.DOWNLOADED -> ServerboundResourcePackPacket.Action.DOWNLOADED; }; connection.send(new ServerboundResourcePackPacket(id, response)); } @Override public void reportFinalResult(UUID id, PackLoadFeedback.FinalResult result) { LOGGER.debug("Pack {} changed status to {}", (Object)id, (Object)result); ServerboundResourcePackPacket.Action response = switch (result) { default -> throw new MatchException(null, null); case PackLoadFeedback.FinalResult.APPLIED -> ServerboundResourcePackPacket.Action.SUCCESSFULLY_LOADED; case PackLoadFeedback.FinalResult.DOWNLOAD_FAILED -> ServerboundResourcePackPacket.Action.FAILED_DOWNLOAD; case PackLoadFeedback.FinalResult.DECLINED -> ServerboundResourcePackPacket.Action.DECLINED; case PackLoadFeedback.FinalResult.DISCARDED -> ServerboundResourcePackPacket.Action.DISCARDED; case PackLoadFeedback.FinalResult.ACTIVATION_FAILED -> ServerboundResourcePackPacket.Action.FAILED_RELOAD; }; connection.send(new ServerboundResourcePackPacket(id, response)); } }; } public void configureForServerControl(Connection connection, ServerPackManager.PackPromptStatus packPromptStatus) { this.packType = PackSource.SERVER; this.packFeedback = DownloadedPackSource.createPackResponseSender(connection); switch (packPromptStatus) { 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 packId) { final CompletableFuture result = new CompletableFuture(); final PackLoadFeedback original = this.packFeedback; this.packFeedback = new PackLoadFeedback(){ @Override public void reportUpdate(UUID id, PackLoadFeedback.Update result2) { original.reportUpdate(id, result2); } @Override public void reportFinalResult(UUID id, PackLoadFeedback.FinalResult status) { if (packId.equals(id)) { DownloadedPackSource.this.packFeedback = original; if (status == PackLoadFeedback.FinalResult.APPLIED) { result.complete(null); } else { result.completeExceptionally(new IllegalStateException("Failed to apply pack " + String.valueOf(id) + ", reason: " + String.valueOf((Object)status))); } } original.reportFinalResult(id, status); } }; return result; } public void cleanupAfterDisconnect() { this.manager.popAll(); this.packFeedback = LOG_ONLY_FEEDBACK; this.manager.resetPromptStatus(); } @Override public void close() throws IOException { this.downloadQueue.close(); } }