优化世界管理页面 (#3711)

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update

* update
This commit is contained in:
Glavo
2025-03-07 19:19:04 +08:00
committed by GitHub
parent cda2f6fb82
commit e7ffb0b271
18 changed files with 665 additions and 134 deletions

View File

@@ -518,6 +518,14 @@ public class DefaultGameRepository implements GameRepository {
return new ModManager(this, version);
}
public Path getSavesDirectory(String id) {
return getRunDirectory(id).toPath().resolve("saves");
}
public Path getBackupsDirectory(String id) {
return getRunDirectory(id).toPath().resolve("backups");
}
@Override
public String toString() {
return new ToStringBuilder(this)

View File

@@ -23,14 +23,15 @@ import com.github.steveice10.opennbt.tag.builtin.LongTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.Unzipper;
import org.jackhuang.hmcl.util.io.Zipper;
import org.jackhuang.hmcl.util.io.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
import java.util.stream.Collectors;
@@ -48,6 +49,7 @@ public class World {
private String gameVersion;
private long lastPlayed;
private Image icon;
private boolean isLocked;
public World(Path file) throws IOException {
this.file = file;
@@ -64,6 +66,7 @@ public class World {
fileName = FileUtils.getName(file);
Path levelDat = file.resolve("level.dat");
getWorldName(levelDat);
isLocked = isLocked(getSessionLockFile());
Path iconFile = file.resolve("icon.png");
if (Files.isRegularFile(iconFile)) {
@@ -93,6 +96,10 @@ public class World {
return file.resolve("level.dat");
}
public Path getSessionLockFile() {
return file.resolve("session.lock");
}
public long getLastPlayed() {
return lastPlayed;
}
@@ -105,6 +112,10 @@ public class World {
return icon;
}
public boolean isLocked() {
return isLocked;
}
private void loadFromZipImpl(Path root) throws IOException {
Path levelDat = root.resolve("level.dat");
if (!Files.exists(levelDat))
@@ -125,6 +136,7 @@ public class World {
}
private void loadFromZip() throws IOException {
isLocked = false;
try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) {
Path cur = fs.getPath("/level.dat");
if (Files.isRegularFile(cur)) {
@@ -237,6 +249,26 @@ public class World {
return parseLevelDat(getLevelDatFile());
}
public FileChannel lock() throws WorldLockedException {
Path lockFile = getSessionLockFile();
FileChannel channel = null;
try {
channel = FileChannel.open(lockFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE);
channel.write(ByteBuffer.wrap("\u2603".getBytes(StandardCharsets.UTF_8)));
channel.force(true);
FileLock fileLock = channel.tryLock();
if (fileLock != null) {
return channel;
} else {
IOUtils.closeQuietly(channel);
throw new WorldLockedException("The world " + getFile() + " has been locked");
}
} catch (IOException e) {
IOUtils.closeQuietly(channel);
throw new WorldLockedException(e);
}
}
public void writeLevelDat(CompoundTag nbt) throws IOException {
if (!Files.isDirectory(file))
throw new IOException("Not a valid world directory");
@@ -258,12 +290,25 @@ public class World {
}
}
private static boolean isLocked(Path sessionLockFile) {
try (FileChannel fileChannel = FileChannel.open(sessionLockFile, StandardOpenOption.WRITE)) {
return fileChannel.tryLock() == null;
} catch (AccessDeniedException accessDeniedException) {
return true;
} catch (NoSuchFileException noSuchFileException) {
return false;
} catch (IOException e) {
LOG.warning("Failed to open the lock file " + sessionLockFile, e);
return false;
}
}
public static Stream<World> getWorlds(Path savesDir) {
try {
if (Files.exists(savesDir)) {
return Files.list(savesDir).flatMap(world -> {
try {
return Stream.of(new World(world));
return Stream.of(new World(world.toAbsolutePath()));
} catch (IOException e) {
LOG.warning("Failed to read world " + world, e);
return Stream.empty();

View File

@@ -0,0 +1,40 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import java.io.IOException;
/**
* @author Glavo
*/
public final class WorldLockedException extends IOException {
public WorldLockedException() {
}
public WorldLockedException(String message) {
super(message);
}
public WorldLockedException(String message, Throwable cause) {
super(message, cause);
}
public WorldLockedException(Throwable cause) {
super(cause);
}
}