2025-11-24 22:52:51 +03:00

418 lines
18 KiB
Java

/*
* 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<String, String> 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<UUID, DownloadQueue.DownloadRequest> requests, Consumer<DownloadQueue.BatchResult> 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<Pack> loadRequestedPacks(List<PackReloadConfig.IdAndPath> packsToLoad) {
ArrayList<Pack> packs = new ArrayList<Pack>(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<Pack> packs) {
if (packs.isEmpty()) {
return EMPTY_SOURCE;
}
return packs::forEach;
}
private void startReload(PackReloadConfig.Callbacks callbacks) {
this.pendingReload = callbacks;
List<PackReloadConfig.IdAndPath> normalPacks = callbacks.packsToLoad();
List<Pack> packs = this.loadRequestedPacks(normalPacks);
if (packs == null) {
callbacks.onFailure(false);
List<PackReloadConfig.IdAndPath> 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<Pack> 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<Void> waitForPackFeedback(final UUID packId) {
final CompletableFuture<Void> result = new CompletableFuture<Void>();
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();
}
}