Skip to content

feat: 优化世界管理#5842

Open
Mine-diamond wants to merge 46 commits intoHMCL-dev:mainfrom
Mine-diamond:one-world-manage-page
Open

feat: 优化世界管理#5842
Mine-diamond wants to merge 46 commits intoHMCL-dev:mainfrom
Mine-diamond:one-world-manage-page

Conversation

@Mine-diamond
Copy link
Copy Markdown
Contributor

@Mine-diamond Mine-diamond commented Mar 24, 2026

继续 #5215 对世界管理界面进行的优化第三期


用户可感知的更新

  • 现在支持拖动文件夹安装世界。
  • 现在安装/重命名/复制世界可以使用任意名字,或者已经存在的名字,会模拟minecraft的行为,该名字将会作为level.dat中的名字,文件夹名称将剔除非法字符或添加后缀。
  • 现在备份可以被还原了。
  • 现在备份页面新的备份在上
  • 现在快速启动后世界管理页面将变为只读模式,而不是退出。
image image

复用页面:

  • 世界管理页面现在是单例的了,就像其它页面一样。
    • 使用Controllers.getWorldManagePage().setWorld(World world, Profile profile, String instanceId)来获取和设置页面
  • 世界管理页面会在不同世界时复用。
  • 相应的,世界信息页面,世界备份页面,数据包管理页面也经过了相应重构。

世界锁机制:

  • 现在World类本身持有锁,而不是在其它类中。
  • 原因是之前World类本身都不知道当前的锁是不是被自己锁定的,导致执行各种操作时需要协调WorldManagePage各种先释放锁再执行最后加锁,尽管某些操作根本不需要释放锁,现在World类本身知道自己是否持有锁,因此可以不再需要操作锁或者可以到内聚到World类中,简化了状态管理。
  • 虽然这可能导致无法完成一些情况比如一个持有锁的类不希望此时执行其它操作,但是这种情况应该是非常少见的。
  • 这修复了无法在世界管理页面复制世界的bug。

ImportableWorld类:

  • 添加了ImportableWorld类,用来表示在外部,即将被安装的世界。可以是压缩包或文件夹。
  • 现在World类仅表示在世界文件夹,以文件夹格式存储的世界。
  • 现在支持安装文件夹格式的世界了。
  • 现在安装世界对于文件名称不合法的名称会模拟minecraft的行为转换为合法的名称,而不是显示错误。

世界备份页面:

  • 添加了还原备份功能。
  • 现在备份以降序排列,即更新的在上。

重命名/复制世界:

  • 优化了重命名世界功能,现在不仅能修改level.dat中的名称,也能修改文件夹名称。
  • 重命名/复制世界功能输入对于文件名称不合法的名称会模拟minecraft的行为转换为合法的名称,而不是显示错误。
  • 重命名/复制弹窗现在会默认填充世界名称

其它:

  • 快速启动不在会退出WorldManagePage页面,而是变为只读模式。

@Mine-diamond Mine-diamond mentioned this pull request Mar 24, 2026
@Mine-diamond Mine-diamond changed the title feat: 优化世界管理页面 feat: 优化世界管理 Mar 24, 2026
@Mine-diamond Mine-diamond marked this pull request as ready for review March 25, 2026 12:24
@Glavo Glavo requested a review from Copilot March 25, 2026 12:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

@3gf8jv4dv 3gf8jv4dv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议统一一下「存档」和「世界」的用法。

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +69 to +73
Controllers.dialog(new InputDialogPane(i18n("world.duplicate.prompt"), world.getWorldName(), (newWorldName, handler) -> {
if (StringUtils.isBlank(newWorldName)) {
newWorldName = i18n("world.name.default");
}
String finalNewWorldName = newWorldName;
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyWorld reassigns the lambda parameter newWorldName (line 71). Lambda parameters are effectively final in Java, so this won’t compile. Use a separate local variable (e.g., String name = ...) before computing finalNewWorldName.

Copilot uses AI. Check for mistakes.
String finalNewWorldName = StringUtils.isBlank(newWorldName) ? i18n("world.name.default") : newWorldName;
boolean renameFolder = ((PromptDialogPane.Builder.BooleanQuestion) res.get(1)).getValue();

if (finalNewWorldName.equals(world.getWorldName())) {
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early-return if (finalNewWorldName.equals(world.getWorldName())) { ... return; } prevents renaming the folder when the user checks “重命名世界文件夹” but keeps the same displayed world name. Consider only skipping when both the name is unchanged and renameFolder is false, or separately handling “rename folder only”.

Suggested change
if (finalNewWorldName.equals(world.getWorldName())) {
if (finalNewWorldName.equals(world.getWorldName()) && !renameFolder) {

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +62
private final BooleanProperty currentWorldSupportQuickPlay = new SimpleBooleanProperty(false);
private final BooleanProperty currentWorldSupportDataPack = new SimpleBooleanProperty(false);
private final BooleanProperty currentWorldSupportChunkBase = new SimpleBooleanProperty(false);
private final BooleanProperty currentWorldSupportEndCity = new SimpleBooleanProperty(false);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currentWorldSupportDataPack is set in setWorld(...) but never read anywhere in this class/skin. Either remove it or bind it to something (e.g., control the visibility/managed state of the datapack tab entry) to avoid dead state that can drift out of sync.

Copilot uses AI. Check for mistakes.
}
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> {
advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty());
advancedListItem.visibleProperty().bind(getSkinnable().currentWorldSupportQuickPlay);
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The launch drawer item binds visibleProperty() but not managedProperty(). In JavaFX layouts, an invisible-but-managed node can still reserve space, which may leave a gap when quick play isn’t supported. Bind managedProperty() to the same condition as visibleProperty() (similar to how other menu items handle this below).

Suggested change
advancedListItem.visibleProperty().bind(getSkinnable().currentWorldSupportQuickPlay);
advancedListItem.visibleProperty().bind(getSkinnable().currentWorldSupportQuickPlay);
advancedListItem.managedProperty().bind(getSkinnable().currentWorldSupportQuickPlay);

Copilot uses AI. Check for mistakes.
Comment on lines +64 to +66
List<Path> files = Files.list(fs.getPath("/")).toList();
if (files.size() != 1 || !Files.isDirectory(files.get(0))) {
throw new IOException("Not a valid world zip file");
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files.list(fs.getPath("/")) returns a Stream that should be closed; here it’s consumed via toList() without try-with-resources. This can leak directory handles inside the zip filesystem. Wrap the Files.list(...) call in a try-with-resources and collect within it.

Copilot uses AI. Check for mistakes.
}
checkAndLoadLevelData(levelDatPath);
} else {
throw new IOException("Path " + sourcePath + " cannot be recognized as a archive Minecraft world");
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception message grammar: "a archive Minecraft world" should be "an archive Minecraft world" (or rephrase to "a Minecraft world archive"). This message may surface in logs/UI, so it’s worth correcting.

Suggested change
throw new IOException("Path " + sourcePath + " cannot be recognized as a archive Minecraft world");
throw new IOException("Path " + sourcePath + " cannot be recognized as an archive Minecraft world");

Copilot uses AI. Check for mistakes.
@Glavo
Copy link
Copy Markdown
Member

Glavo commented Apr 4, 2026

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request overhauls the world management system by introducing a WorldLock mechanism for session control and an ImportableWorld class for handling world imports. It adds functionality for restoring backups, renaming world folders, and refactors the UI to better handle read-only states and loading failures. Feedback highlights a logic error in the restoration task that could cause failures after renaming, a potential resource leak in the world import logic, and a regression in the file lock state detection for overlapping locks.

Comment on lines +46 to +48
new ImportableWorld(backupZipPath);
try {
new Unzipper(backupZipPath, tempPath).setSubDirectory(world.getFileName()).unzip();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

这里创建了 ImportableWorld 实例但没有使用。更重要的是,Unzipper 使用 world.getFileName() 作为子目录名,这在世界文件夹被重命名后会导致还原失败(因为备份压缩包内的目录名仍是旧的)。应该使用 ImportableWorld 检测到的实际目录名。

        ImportableWorld importable = new ImportableWorld(backupZipPath);
        try {
            Unzipper unzipper = new Unzipper(backupZipPath, tempPath);
            if (importable.hasTopLevelDirectory()) {
                unzipper.setSubDirectory("/" + importable.getFileName() + "/");
            }
            unzipper.unzip();

Comment on lines +394 to +395
} catch (OverlappingFileLockException | NoSuchFileException overlappingFileLockException) {
return false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

OverlappingFileLockException 表示当前 JVM 已持有该文件的锁(虽然不是由当前 World 实例持有),因此在这种情况下应该返回 true(已锁定),而不是 false。这在旧版代码中是正确的,此处似乎是重构引入的回归。

Suggested change
} catch (OverlappingFileLockException | NoSuchFileException overlappingFileLockException) {
return false;
} catch (OverlappingFileLockException overlappingFileLockException) {
return true;
} catch (NoSuchFileException noSuchFileException) {
return false;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我觉得这个类的设计就很奇怪,尤其是 ImportableWorld 这个名字我认为非常不恰当。

我认为你可以把它改名叫 WorldInfo 之类的名字,或者修改 World 使其不可变,然后用新的可变类来管理对世界的修改。

Copy link
Copy Markdown
Contributor Author

@Mine-diamond Mine-diamond Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个不是表达世界信息的类的,而是表达“即将被导入的世界的”,Import:导入,Importable:可导入的,为什么会认为是扩展的意思呢?
这个类的作用就是表示安装世界或在世界备份页面时的静态世界文件的,而World类现在仅表示在世界文件夹,可被世界管理页面管理的类的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants