minecraft-src/com/mojang/realmsclient/gui/screens/RealmsUploadScreen.java
2025-07-04 01:41:11 +03:00

457 lines
16 KiB
Java

package com.mojang.realmsclient.gui.screens;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.RateLimiter;
import com.mojang.logging.LogUtils;
import com.mojang.realmsclient.RealmsMainScreen;
import com.mojang.realmsclient.Unit;
import com.mojang.realmsclient.client.FileUpload;
import com.mojang.realmsclient.client.RealmsClient;
import com.mojang.realmsclient.client.UploadStatus;
import com.mojang.realmsclient.dto.UploadInfo;
import com.mojang.realmsclient.exception.RealmsServiceException;
import com.mojang.realmsclient.exception.RetryCallException;
import com.mojang.realmsclient.util.UploadTokenCache;
import com.mojang.realmsclient.util.task.LongRunningTask;
import com.mojang.realmsclient.util.task.RealmCreationTask;
import com.mojang.realmsclient.util.task.SwitchSlotTask;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.GZIPOutputStream;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.SharedConstants;
import net.minecraft.Util;
import net.minecraft.client.GameNarrator;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.AbstractWidget;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.layouts.HeaderAndFooterLayout;
import net.minecraft.client.gui.screens.TitleScreen;
import net.minecraft.network.chat.CommonComponents;
import net.minecraft.network.chat.Component;
import net.minecraft.realms.RealmsScreen;
import net.minecraft.world.level.storage.LevelSummary;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
@Environment(EnvType.CLIENT)
public class RealmsUploadScreen extends RealmsScreen {
private static final Logger LOGGER = LogUtils.getLogger();
private static final ReentrantLock UPLOAD_LOCK = new ReentrantLock();
private static final int BAR_WIDTH = 200;
private static final int BAR_TOP = 80;
private static final int BAR_BOTTOM = 95;
private static final int BAR_BORDER = 1;
private static final String[] DOTS = new String[]{"", ".", ". .", ". . ."};
private static final Component VERIFYING_TEXT = Component.translatable("mco.upload.verifying");
private final RealmsResetWorldScreen lastScreen;
private final LevelSummary selectedLevel;
@Nullable
private final RealmCreationTask realmCreationTask;
private final long realmId;
private final int slotId;
private final UploadStatus uploadStatus;
private final RateLimiter narrationRateLimiter;
@Nullable
private volatile Component[] errorMessage;
private volatile Component status = Component.translatable("mco.upload.preparing");
@Nullable
private volatile String progress;
private volatile boolean cancelled;
private volatile boolean uploadFinished;
private volatile boolean showDots = true;
private volatile boolean uploadStarted;
@Nullable
private Button backButton;
@Nullable
private Button cancelButton;
private int tickCount;
@Nullable
private Long previousWrittenBytes;
@Nullable
private Long previousTimeSnapshot;
private long bytesPersSecond;
private final HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this);
public RealmsUploadScreen(
@Nullable RealmCreationTask realmCreationTask, long realmId, int slotId, RealmsResetWorldScreen lastScreen, LevelSummary selectedLevel
) {
super(GameNarrator.NO_TITLE);
this.realmCreationTask = realmCreationTask;
this.realmId = realmId;
this.slotId = slotId;
this.lastScreen = lastScreen;
this.selectedLevel = selectedLevel;
this.uploadStatus = new UploadStatus();
this.narrationRateLimiter = RateLimiter.create(0.1F);
}
@Override
public void init() {
this.backButton = this.layout.addToFooter(Button.builder(CommonComponents.GUI_BACK, button -> this.onBack()).build());
this.backButton.visible = false;
this.cancelButton = this.layout.addToFooter(Button.builder(CommonComponents.GUI_CANCEL, button -> this.onCancel()).build());
if (!this.uploadStarted) {
if (this.lastScreen.slot == -1) {
this.uploadStarted = true;
this.upload();
} else {
List<LongRunningTask> list = new ArrayList();
if (this.realmCreationTask != null) {
list.add(this.realmCreationTask);
}
list.add(new SwitchSlotTask(this.realmId, this.lastScreen.slot, () -> {
if (!this.uploadStarted) {
this.uploadStarted = true;
this.minecraft.execute(() -> {
this.minecraft.setScreen(this);
this.upload();
});
}
}));
this.minecraft.setScreen(new RealmsLongRunningMcoTaskScreen(this.lastScreen, (LongRunningTask[])list.toArray(new LongRunningTask[0])));
}
}
this.layout.visitWidgets(guiEventListener -> {
AbstractWidget var10000 = this.addRenderableWidget(guiEventListener);
});
this.repositionElements();
}
@Override
protected void repositionElements() {
this.layout.arrangeElements();
}
private void onBack() {
this.minecraft.setScreen(new RealmsConfigureWorldScreen(new RealmsMainScreen(new TitleScreen()), this.realmId));
}
private void onCancel() {
this.cancelled = true;
this.minecraft.setScreen(this.lastScreen);
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (keyCode == 256) {
if (this.showDots) {
this.onCancel();
} else {
this.onBack();
}
return true;
} else {
return super.keyPressed(keyCode, scanCode, modifiers);
}
}
@Override
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
super.render(guiGraphics, mouseX, mouseY, partialTick);
if (!this.uploadFinished
&& this.uploadStatus.bytesWritten != 0L
&& this.uploadStatus.bytesWritten == this.uploadStatus.totalBytes
&& this.cancelButton != null) {
this.status = VERIFYING_TEXT;
this.cancelButton.active = false;
}
guiGraphics.drawCenteredString(this.font, this.status, this.width / 2, 50, -1);
if (this.showDots) {
guiGraphics.drawString(this.font, DOTS[this.tickCount / 10 % DOTS.length], this.width / 2 + this.font.width(this.status) / 2 + 5, 50, -1, false);
}
if (this.uploadStatus.bytesWritten != 0L && !this.cancelled) {
this.drawProgressBar(guiGraphics);
this.drawUploadSpeed(guiGraphics);
}
Component[] components = this.errorMessage;
if (components != null) {
for (int i = 0; i < components.length; i++) {
guiGraphics.drawCenteredString(this.font, components[i], this.width / 2, 110 + 12 * i, -65536);
}
}
}
private void drawProgressBar(GuiGraphics guiGraphics) {
double d = Math.min((double)this.uploadStatus.bytesWritten / this.uploadStatus.totalBytes, 1.0);
this.progress = String.format(Locale.ROOT, "%.1f", d * 100.0);
int i = (this.width - 200) / 2;
int j = i + (int)Math.round(200.0 * d);
guiGraphics.fill(i - 1, 79, j + 1, 96, -1);
guiGraphics.fill(i, 80, j, 95, -8355712);
guiGraphics.drawCenteredString(this.font, Component.translatable("mco.upload.percent", this.progress), this.width / 2, 84, -1);
}
private void drawUploadSpeed(GuiGraphics guiGraphics) {
if (this.tickCount % 20 == 0) {
if (this.previousWrittenBytes != null && this.previousTimeSnapshot != null) {
long l = Util.getMillis() - this.previousTimeSnapshot;
if (l == 0L) {
l = 1L;
}
this.bytesPersSecond = 1000L * (this.uploadStatus.bytesWritten - this.previousWrittenBytes) / l;
this.drawUploadSpeed0(guiGraphics, this.bytesPersSecond);
}
this.previousWrittenBytes = this.uploadStatus.bytesWritten;
this.previousTimeSnapshot = Util.getMillis();
} else {
this.drawUploadSpeed0(guiGraphics, this.bytesPersSecond);
}
}
private void drawUploadSpeed0(GuiGraphics guiGraphics, long bytesPerSecond) {
String string = this.progress;
if (bytesPerSecond > 0L && string != null) {
int i = this.font.width(string);
String string2 = "(" + Unit.humanReadable(bytesPerSecond) + "/s)";
guiGraphics.drawString(this.font, string2, this.width / 2 + i / 2 + 15, 84, -1, false);
}
}
@Override
public void tick() {
super.tick();
this.tickCount++;
if (this.narrationRateLimiter.tryAcquire(1)) {
Component component = this.createProgressNarrationMessage();
this.minecraft.getNarrator().sayNow(component);
}
}
private Component createProgressNarrationMessage() {
List<Component> list = Lists.<Component>newArrayList();
list.add(this.status);
if (this.progress != null) {
list.add(Component.translatable("mco.upload.percent", this.progress));
}
Component[] components = this.errorMessage;
if (components != null) {
list.addAll(Arrays.asList(components));
}
return CommonComponents.joinLines(list);
}
private void upload() {
new Thread(
() -> {
File file = null;
RealmsClient realmsClient = RealmsClient.create();
try {
if (!UPLOAD_LOCK.tryLock(1L, TimeUnit.SECONDS)) {
this.status = Component.translatable("mco.upload.close.failure");
} else {
UploadInfo uploadInfo = null;
for (int i = 0; i < 20; i++) {
try {
if (this.cancelled) {
this.uploadCancelled();
return;
}
uploadInfo = realmsClient.requestUploadInfo(this.realmId, UploadTokenCache.get(this.realmId));
if (uploadInfo != null) {
break;
}
} catch (RetryCallException var18) {
Thread.sleep(var18.delaySeconds * 1000);
}
}
if (uploadInfo == null) {
this.status = Component.translatable("mco.upload.close.failure");
} else {
UploadTokenCache.put(this.realmId, uploadInfo.getToken());
if (!uploadInfo.isWorldClosed()) {
this.status = Component.translatable("mco.upload.close.failure");
} else if (this.cancelled) {
this.uploadCancelled();
} else {
File file2 = new File(this.minecraft.gameDirectory.getAbsolutePath(), "saves");
file = this.tarGzipArchive(new File(file2, this.selectedLevel.getLevelId()));
if (this.cancelled) {
this.uploadCancelled();
} else if (this.verify(file)) {
this.status = Component.translatable("mco.upload.uploading", this.selectedLevel.getLevelName());
FileUpload fileUpload = new FileUpload(
file,
this.realmId,
this.slotId,
uploadInfo,
this.minecraft.getUser(),
SharedConstants.getCurrentVersion().getName(),
this.selectedLevel.levelVersion().minecraftVersionName(),
this.uploadStatus
);
fileUpload.upload(uploadResult -> {
if (uploadResult.statusCode >= 200 && uploadResult.statusCode < 300) {
this.uploadFinished = true;
this.status = Component.translatable("mco.upload.done");
if (this.backButton != null) {
this.backButton.setMessage(CommonComponents.GUI_DONE);
}
UploadTokenCache.invalidate(this.realmId);
} else if (uploadResult.statusCode == 400 && uploadResult.errorMessage != null) {
this.setErrorMessage(Component.translatable("mco.upload.failed", uploadResult.errorMessage));
} else {
this.setErrorMessage(Component.translatable("mco.upload.failed", uploadResult.statusCode));
}
});
while (!fileUpload.isFinished()) {
if (this.cancelled) {
fileUpload.cancel();
this.uploadCancelled();
return;
}
try {
Thread.sleep(500L);
} catch (InterruptedException var17) {
LOGGER.error("Failed to check Realms file upload status");
}
}
} else {
long l = file.length();
Unit unit = Unit.getLargest(l);
Unit unit2 = Unit.getLargest(5368709120L);
if (Unit.humanReadable(l, unit).equals(Unit.humanReadable(5368709120L, unit2)) && unit != Unit.B) {
Unit unit3 = Unit.values()[unit.ordinal() - 1];
this.setErrorMessage(
Component.translatable("mco.upload.size.failure.line1", this.selectedLevel.getLevelName()),
Component.translatable("mco.upload.size.failure.line2", Unit.humanReadable(l, unit3), Unit.humanReadable(5368709120L, unit3))
);
} else {
this.setErrorMessage(
Component.translatable("mco.upload.size.failure.line1", this.selectedLevel.getLevelName()),
Component.translatable("mco.upload.size.failure.line2", Unit.humanReadable(l, unit), Unit.humanReadable(5368709120L, unit2))
);
}
}
}
}
}
} catch (IOException var19) {
this.setErrorMessage(Component.translatable("mco.upload.failed", var19.getMessage()));
} catch (RealmsServiceException var20) {
this.setErrorMessage(Component.translatable("mco.upload.failed", var20.realmsError.errorMessage()));
} catch (InterruptedException var21) {
LOGGER.error("Could not acquire upload lock");
} finally {
this.uploadFinished = true;
if (UPLOAD_LOCK.isHeldByCurrentThread()) {
UPLOAD_LOCK.unlock();
this.showDots = false;
if (this.backButton != null) {
this.backButton.visible = true;
}
if (this.cancelButton != null) {
this.cancelButton.visible = false;
}
if (file != null) {
LOGGER.debug("Deleting file {}", file.getAbsolutePath());
file.delete();
}
} else {
return;
}
}
}
)
.start();
}
private void setErrorMessage(Component... errorMessage) {
this.errorMessage = errorMessage;
}
private void uploadCancelled() {
this.status = Component.translatable("mco.upload.cancelled");
LOGGER.debug("Upload was cancelled");
}
private boolean verify(File file) {
return file.length() < 5368709120L;
}
private File tarGzipArchive(File file) throws IOException {
TarArchiveOutputStream tarArchiveOutputStream = null;
File var4;
try {
File file2 = File.createTempFile("realms-upload-file", ".tar.gz");
tarArchiveOutputStream = new TarArchiveOutputStream(new GZIPOutputStream(new FileOutputStream(file2)));
tarArchiveOutputStream.setLongFileMode(3);
this.addFileToTarGz(tarArchiveOutputStream, file.getAbsolutePath(), "world", true);
tarArchiveOutputStream.finish();
var4 = file2;
} finally {
if (tarArchiveOutputStream != null) {
tarArchiveOutputStream.close();
}
}
return var4;
}
private void addFileToTarGz(TarArchiveOutputStream tarArchiveOutputStream, String pathname, String name, boolean rootDirectory) throws IOException {
if (!this.cancelled) {
File file = new File(pathname);
String string = rootDirectory ? name : name + file.getName();
TarArchiveEntry tarArchiveEntry = new TarArchiveEntry(file, string);
tarArchiveOutputStream.putArchiveEntry(tarArchiveEntry);
if (file.isFile()) {
InputStream inputStream = new FileInputStream(file);
try {
inputStream.transferTo(tarArchiveOutputStream);
} catch (Throwable var14) {
try {
inputStream.close();
} catch (Throwable var13) {
var14.addSuppressed(var13);
}
throw var14;
}
inputStream.close();
tarArchiveOutputStream.closeArchiveEntry();
} else {
tarArchiveOutputStream.closeArchiveEntry();
File[] files = file.listFiles();
if (files != null) {
for (File file2 : files) {
this.addFileToTarGz(tarArchiveOutputStream, file2.getAbsolutePath(), string + "/", false);
}
}
}
}
}
}