Conversation
There was a problem hiding this comment.
Pull request overview
This PR continues the world-management UI refactor (following #5215) by making WorldManagePage reusable/singleton-like, moving the world lock responsibility into World, and introducing ArchiveWorld to represent zipped worlds/backups.
Changes:
- Refactors world management pages to be reusable across worlds and adds restore-backup support in the backups UI.
- Moves
session.lockhandling intoWorldvia an embeddedWorldLockand updates callers accordingly. - Introduces
ArchiveWorldfor zip-based worlds;Worldnow represents directory-based worlds only.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java | Adds getSafeWorldFolderName helper for Minecraft-like folder name sanitization. |
| HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java | Renames quick-play feature check to supportsQuickPlay. |
| HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java | Removes zip-world parsing, adds WorldLock, updates rename/copy/delete/export flows. |
| HMCL/src/main/resources/assets/lang/I18N.properties | Adds datapack-not-supported and world-restore/rename strings (EN). |
| HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | Adds datapack-not-supported and world-restore/rename strings (zh_CN). |
| HMCL/src/main/resources/assets/lang/I18N_zh.properties | Adds datapack-not-supported and world-restore/rename strings (zh). |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldRestoreTask.java | New task to restore a world from a backup zip. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java | Refactors world operations; adds rename dialog flow. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java | Makes page reusable via setWorld(...); updates lock/read-only/quick-play logic. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java | Navigates to singleton world management page; adds rename in popup menu; uses ArchiveWorld for installs. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java | Reworks read-only binding + integrates rename flow from world info. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java | Uses ArchiveWorld, sorts backups descending, adds restore action. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupTask.java | Uses new WorldLock.Guard instead of direct channel locking. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPageSkin.java | Refactors list binding/disabled states; wires failedReason to UI. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java | Rebuilds datapack model on refresh; shows “not supported” message when applicable. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ArchiveWorld.java | New representation of zip-based worlds and install logic. |
| HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java | Adds lazy singleton accessor for WorldManagePage. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java
Show resolved
Hide resolved
HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java
Outdated
Show resolved
Hide resolved
HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ArchiveWorld.java
Outdated
Show resolved
Hide resolved
HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java
Outdated
Show resolved
Hide resolved
HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java
Outdated
Show resolved
Hide resolved
| // Check if the world format is correct | ||
| new ArchiveWorld(backupZipPath); | ||
| try { | ||
| new Unzipper(backupZipPath, tempPath).setSubDirectory(world.getFileName()).unzip(); |
There was a problem hiding this comment.
Restore unzips using setSubDirectory(world.getFileName()), which assumes the backup zip’s root folder matches the current world folder name. If the world was renamed (folder renamed) after the backup was created, the backup root directory will differ and restore will fail with "Subdirectory ... does not exist". Consider parsing the backup via ArchiveWorld and using its root directory name (getFileName() / hasSubDir()) for setSubDirectory.
| // Check if the world format is correct | |
| new ArchiveWorld(backupZipPath); | |
| try { | |
| new Unzipper(backupZipPath, tempPath).setSubDirectory(world.getFileName()).unzip(); | |
| // Check if the world format is correct and determine archive root directory | |
| ArchiveWorld archiveWorld = new ArchiveWorld(backupZipPath); | |
| try { | |
| Unzipper unzipper = new Unzipper(backupZipPath, tempPath); | |
| if (archiveWorld.hasSubDir()) { | |
| unzipper.setSubDirectory(archiveWorld.getFileName()); | |
| } | |
| unzipper.unzip(); |
There was a problem hiding this comment.
如果不匹配的话根本就检测不到,或者说就不是其备份
# Conflicts: # HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java # HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java # HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java # HMCL/src/main/resources/assets/lang/I18N.properties # HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java
There was a problem hiding this comment.
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.
HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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.
| Controllers.dialog(new InputDialogPane(i18n("world.duplicate.prompt"), world.getWorldName(), (newWorldName, handler) -> { | ||
| if (StringUtils.isBlank(newWorldName)) { | ||
| newWorldName = i18n("world.name.default"); | ||
| } | ||
| String finalNewWorldName = newWorldName; |
There was a problem hiding this comment.
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.
| String finalNewWorldName = StringUtils.isBlank(newWorldName) ? i18n("world.name.default") : newWorldName; | ||
| boolean renameFolder = ((PromptDialogPane.Builder.BooleanQuestion) res.get(1)).getValue(); | ||
|
|
||
| if (finalNewWorldName.equals(world.getWorldName())) { |
There was a problem hiding this comment.
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”.
| if (finalNewWorldName.equals(world.getWorldName())) { | |
| if (finalNewWorldName.equals(world.getWorldName()) && !renameFolder) { |
| 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); |
There was a problem hiding this comment.
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.
| } | ||
| toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> { | ||
| advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty()); | ||
| advancedListItem.visibleProperty().bind(getSkinnable().currentWorldSupportQuickPlay); |
There was a problem hiding this comment.
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).
| advancedListItem.visibleProperty().bind(getSkinnable().currentWorldSupportQuickPlay); | |
| advancedListItem.visibleProperty().bind(getSkinnable().currentWorldSupportQuickPlay); | |
| advancedListItem.managedProperty().bind(getSkinnable().currentWorldSupportQuickPlay); |
| 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"); |
There was a problem hiding this comment.
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.
| } | ||
| checkAndLoadLevelData(levelDatPath); | ||
| } else { | ||
| throw new IOException("Path " + sourcePath + " cannot be recognized as a archive Minecraft world"); |
There was a problem hiding this comment.
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.
| 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"); |
继续 #5215 对世界管理界面进行的优化第三期
用户可感知的更新
复用页面:
Controllers.getWorldManagePage().setWorld(World world, Profile profile, String instanceId)来获取和设置页面世界锁机制:
World类本身持有锁,而不是在其它类中。World类本身都不知道当前的锁是不是被自己锁定的,导致执行各种操作时需要协调WorldManagePage各种先释放锁再执行最后加锁,尽管某些操作根本不需要释放锁,现在World类本身知道自己是否持有锁,因此可以不再需要操作锁或者可以到内聚到World类中,简化了状态管理。ImportableWorld类:ImportableWorld类,用来表示在外部,即将被安装的世界。可以是压缩包或文件夹。World类仅表示在世界文件夹,以文件夹格式存储的世界。世界备份页面:
重命名/复制世界:
level.dat中的名称,也能修改文件夹名称。其它:
WorldManagePage页面,而是变为只读模式。