From 0700e2ca6d3395fda3609f5206e7b8179aa55e4a Mon Sep 17 00:00:00 2001 From: yoima-jp <28yoima72@gmail.com> Date: Fri, 14 Aug 2026 22:27:12 +0900 Subject: [PATCH] =?UTF-8?q?=E5=85=AC=E9=96=8BForge=E3=83=96=E3=83=A9?= =?UTF-8?q?=E3=83=B3=E3=83=81=E3=82=92=E6=9C=80=E6=96=B0=E3=82=BD=E3=83=BC?= =?UTF-8?q?=E3=82=B9=E3=81=B8=E5=90=8C=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 1.12.2/NOTICE | 4 + 1.12.2/build.gradle | 5 +- 1.12.2/licenses/Apache-2.0.txt | 175 ++++++++ .../core/CurrentProcessRestartService.java | 145 ++++--- .../core/FileOperationLogger.java | 141 ++++++- .../core/ProfileCreateOptions.java | 1 + .../universalconfig/core/ProfileService.java | 137 +++++- .../core/UniversalConfigPaths.java | 11 +- .../core/UniversalConfigSettings.java | 20 + .../core/ZipArchiveReader.java | 34 +- .../core/ZipArchiveWriter.java | 3 + .../universalconfig/core/ZipSecurity.java | 47 +++ .../forgelegacy/LegacyScreens.java | 293 +++++++++++-- .../forgelegacy/LegacyTitleButtonHandler.java | 33 +- .../forgelegacy/UniversalConfigLegacyMod.java | 23 +- .../assets/universal_config/icon.png | Bin 32658 -> 3907 bytes .../assets/universal_config/lang/en_us.json | 38 +- .../assets/universal_config/lang/en_us.lang | 30 +- .../assets/universal_config/lang/ja_jp.json | 38 +- .../assets/universal_config/lang/ja_jp.lang | 30 +- .../assets/universal_config/lang/ko_kr.json | 38 +- .../assets/universal_config/lang/ko_kr.lang | 30 +- .../assets/universal_config/lang/zh_cn.json | 36 +- .../assets/universal_config/lang/zh_cn.lang | 28 +- .../assets/universal_config/lang/zh_tw.json | 36 +- .../assets/universal_config/lang/zh_tw.lang | 28 +- .../universal_config/title_screen_button.png | Bin 24132 -> 240 bytes 1.12.2/src/main/resources/mcmod.info | 2 +- .../src/main/resources/universal_config.png | Bin 32658 -> 3907 bytes .../universalconfig/core/ZipSecurityTest.java | 69 +++ 1.16.5/NOTICE | 5 + 1.16.5/build.gradle | 5 + 1.16.5/gradle.properties | 2 +- 1.16.5/licenses/Apache-2.0.txt | 175 ++++++++ .../core/CurrentProcessRestartService.java | 152 ++++--- .../core/FileOperationLogger.java | 141 ++++++- .../core/ProfileCreateOptions.java | 1 + .../universalconfig/core/ProfileService.java | 137 +++++- .../core/UniversalConfigPaths.java | 11 +- .../core/UniversalConfigSettings.java | 20 + .../core/ZipArchiveReader.java | 34 +- .../core/ZipArchiveWriter.java | 3 + .../universalconfig/core/ZipSecurity.java | 47 +++ .../forge/ForgeRestartService.java | 19 +- .../forge/UniversalConfigMod.java | 64 ++- .../universalconfig/forge/screen/Button.java | 102 ++++- .../forge/screen/ProfileCreateScreen.java | 2 +- .../forge/screen/ProfileListScreen.java | 209 +++++++++- .../forge/screen/ProfileRenameScreen.java | 73 ++++ 1.16.5/src/main/resources/META-INF/mods.toml | 2 +- .../assets/universal_config/icon.png | Bin 32658 -> 3907 bytes .../assets/universal_config/lang/en_us.json | 32 +- .../assets/universal_config/lang/ja_jp.json | 32 +- .../assets/universal_config/lang/ko_kr.json | 32 +- .../assets/universal_config/lang/zh_cn.json | 30 +- .../assets/universal_config/lang/zh_tw.json | 30 +- .../universal_config/title_screen_button.png | Bin 24132 -> 240 bytes .../src/main/resources/universal_config.png | Bin 32658 -> 3907 bytes .../CurrentProcessRestartServiceTest.java | 22 + .../core/FileOperationLoggerTest.java | 32 ++ .../core/ProfileOrderingTest.java | 78 ++++ .../universalconfig/core/ZipSecurityTest.java | 33 ++ 1.18.2/NOTICE | 5 + 1.18.2/build.gradle | 17 +- 1.18.2/gradle.properties | 2 +- 1.18.2/licenses/Apache-2.0.txt | 175 ++++++++ .../core/CurrentProcessRestartService.java | 152 ++++--- .../core/FileOperationLogger.java | 141 ++++++- .../core/ProfileCreateOptions.java | 1 + .../universalconfig/core/ProfileService.java | 137 +++++- .../core/UniversalConfigPaths.java | 11 +- .../core/UniversalConfigSettings.java | 20 + .../core/ZipArchiveReader.java | 34 +- .../core/ZipArchiveWriter.java | 3 + .../universalconfig/core/ZipSecurity.java | 47 +++ .../forge/ForgeRestartService.java | 19 +- .../forge/UniversalConfigMod.java | 46 +- .../universalconfig/forge/screen/Button.java | 98 ++++- .../forge/screen/ProfileCreateScreen.java | 2 +- .../forge/screen/ProfileListScreen.java | 271 ++++++++++-- .../forge/screen/ProfileRenameScreen.java | 72 ++++ .../forge/screen/ScreenUtil.java | 10 +- 1.18.2/src/main/resources/META-INF/mods.toml | 2 +- .../assets/universal_config/icon.png | Bin 32658 -> 3907 bytes .../assets/universal_config/lang/en_us.json | 32 +- .../assets/universal_config/lang/ja_jp.json | 32 +- .../assets/universal_config/lang/ko_kr.json | 32 +- .../assets/universal_config/lang/zh_cn.json | 30 +- .../assets/universal_config/lang/zh_tw.json | 30 +- .../universal_config/title_screen_button.png | Bin 24132 -> 240 bytes .../src/main/resources/universal_config.png | Bin 32658 -> 3907 bytes .../CurrentProcessRestartServiceTest.java | 280 +++++++++++++ .../core/FileOperationLoggerTest.java | 58 +++ .../core/ProfileOrderingTest.java | 98 +++++ .../universalconfig/core/ZipSecurityTest.java | 32 ++ 1.19.2/NOTICE | 5 + 1.19.2/build.gradle | 17 +- 1.19.2/gradle.properties | 2 +- 1.19.2/licenses/Apache-2.0.txt | 175 ++++++++ .../core/CurrentProcessRestartService.java | 152 ++++--- .../core/FileOperationLogger.java | 141 ++++++- .../core/ProfileCreateOptions.java | 1 + .../universalconfig/core/ProfileService.java | 137 +++++- .../core/UniversalConfigPaths.java | 11 +- .../core/UniversalConfigSettings.java | 20 + .../core/ZipArchiveReader.java | 34 +- .../core/ZipArchiveWriter.java | 3 + .../universalconfig/core/ZipSecurity.java | 47 +++ .../forge/ForgeRestartService.java | 17 +- .../forge/UniversalConfigMod.java | 64 +-- .../universalconfig/forge/screen/Button.java | 96 ++++- .../forge/screen/ProfileCreateScreen.java | 2 +- .../forge/screen/ProfileListScreen.java | 274 ++++++++++-- .../forge/screen/ProfileRenameScreen.java | 72 ++++ .../forge/screen/ScreenUtil.java | 10 +- 1.19.2/src/main/resources/META-INF/mods.toml | 2 +- .../assets/universal_config/icon.png | Bin 32658 -> 3907 bytes .../assets/universal_config/lang/en_us.json | 32 +- .../assets/universal_config/lang/ja_jp.json | 32 +- .../assets/universal_config/lang/ko_kr.json | 32 +- .../assets/universal_config/lang/zh_cn.json | 30 +- .../assets/universal_config/lang/zh_tw.json | 30 +- .../universal_config/title_screen_button.png | Bin 24132 -> 240 bytes .../src/main/resources/universal_config.png | Bin 32658 -> 3907 bytes .../CurrentProcessRestartServiceTest.java | 280 +++++++++++++ .../core/FileOperationLoggerTest.java | 58 +++ .../core/ProfileOrderingTest.java | 98 +++++ .../universalconfig/core/ZipSecurityTest.java | 32 ++ 1.20.1/NOTICE | 5 + 1.20.1/build.gradle | 10 +- 1.20.1/gradle.properties | 2 +- 1.20.1/licenses/Apache-2.0.txt | 175 ++++++++ .../core/CurrentProcessRestartService.java | 152 ++++--- .../core/FileOperationLogger.java | 141 ++++++- .../core/ProfileCreateOptions.java | 1 + .../universalconfig/core/ProfileService.java | 137 +++++- .../core/UniversalConfigPaths.java | 11 +- .../core/UniversalConfigSettings.java | 20 + .../core/ZipArchiveReader.java | 34 +- .../core/ZipArchiveWriter.java | 3 + .../universalconfig/core/ZipSecurity.java | 47 +++ .../forge/ForgeRestartService.java | 10 +- .../forge/UniversalConfigMod.java | 62 ++- .../forge/screen/ProfileCreateScreen.java | 2 +- .../forge/screen/ProfileListScreen.java | 219 +++++++++- .../forge/screen/ProfileRenameScreen.java | 72 ++++ .../forge/screen/ScreenUtil.java | 9 + 1.20.1/src/main/resources/META-INF/mods.toml | 2 +- .../assets/universal_config/icon.png | Bin 32658 -> 3907 bytes .../assets/universal_config/lang/en_us.json | 32 +- .../assets/universal_config/lang/ja_jp.json | 32 +- .../assets/universal_config/lang/ko_kr.json | 32 +- .../assets/universal_config/lang/zh_cn.json | 30 +- .../assets/universal_config/lang/zh_tw.json | 30 +- .../universal_config/title_screen_button.png | Bin 24132 -> 240 bytes .../src/main/resources/universal_config.png | Bin 32658 -> 3907 bytes .../CurrentProcessRestartServiceTest.java | 112 ++--- .../core/FileOperationLoggerTest.java | 58 +++ .../core/ProfileOrderingTest.java | 98 +++++ .../universalconfig/core/ZipSecurityTest.java | 17 + 1.21.1/NOTICE | 5 + 1.21.1/build.gradle | 4 +- 1.21.1/licenses/Apache-2.0.txt | 175 ++++++++ .../core/CurrentProcessRestartService.java | 390 +++++++---------- .../core/FileOperationLogger.java | 116 +++++- .../core/ProfileCreateOptions.java | 1 + .../universalconfig/core/ProfileService.java | 186 +++++++-- .../core/UniversalConfigPaths.java | 32 +- .../core/UniversalConfigSettings.java | 19 + .../core/ZipArchiveReader.java | 23 +- .../core/ZipArchiveWriter.java | 6 +- .../universalconfig/core/ZipSecurity.java | 52 ++- .../forge/ForgeRestartService.java | 12 +- .../forge/UniversalConfigMod.java | 60 ++- .../forge/screen/ProfileCreateScreen.java | 2 +- .../forge/screen/ProfileListScreen.java | 206 ++++++++- .../forge/screen/ProfileRenameScreen.java | 72 ++++ 1.21.1/src/main/resources/META-INF/mods.toml | 2 +- .../assets/universal_config/icon.png | Bin 32658 -> 3907 bytes .../assets/universal_config/lang/en_us.json | 32 +- .../assets/universal_config/lang/ja_jp.json | 32 +- .../assets/universal_config/lang/ko_kr.json | 32 +- .../assets/universal_config/lang/zh_cn.json | 30 +- .../assets/universal_config/lang/zh_tw.json | 30 +- .../universal_config/title_screen_button.png | Bin 24132 -> 240 bytes .../src/main/resources/universal_config.png | Bin 32658 -> 3907 bytes .../CurrentProcessRestartServiceTest.java | 112 ++--- .../core/FileOperationLoggerTest.java | 58 +++ .../core/ProfileOrderingTest.java | 98 +++++ .../universalconfig/core/ZipSecurityTest.java | 18 + 1.7.10/NOTICE | 4 + 1.7.10/build.gradle | 5 +- 1.7.10/licenses/Apache-2.0.txt | 175 ++++++++ .../core/CurrentProcessRestartService.java | 145 ++++--- .../core/FileOperationLogger.java | 141 ++++++- .../core/ProfileCreateOptions.java | 1 + .../universalconfig/core/ProfileService.java | 137 +++++- .../core/UniversalConfigPaths.java | 11 +- .../core/UniversalConfigSettings.java | 20 + .../core/ZipArchiveReader.java | 34 +- .../core/ZipArchiveWriter.java | 3 + .../universalconfig/core/ZipSecurity.java | 47 +++ .../forgelegacy/LegacyScreens.java | 293 +++++++++++-- .../forgelegacy/LegacyTitleButtonHandler.java | 28 +- .../forgelegacy/UniversalConfigLegacyMod.java | 23 +- .../assets/universal_config/icon.png | Bin 32658 -> 3907 bytes .../assets/universal_config/lang/en_US.lang | 30 +- .../assets/universal_config/lang/en_us.json | 38 +- .../assets/universal_config/lang/ja_JP.lang | 30 +- .../assets/universal_config/lang/ja_jp.json | 38 +- .../assets/universal_config/lang/ko_KR.lang | 30 +- .../assets/universal_config/lang/ko_kr.json | 38 +- .../assets/universal_config/lang/zh_CN.lang | 28 +- .../assets/universal_config/lang/zh_TW.lang | 28 +- .../assets/universal_config/lang/zh_cn.json | 36 +- .../assets/universal_config/lang/zh_tw.json | 36 +- .../universal_config/title_screen_button.png | Bin 24132 -> 240 bytes 1.7.10/src/main/resources/mcmod.info | 2 +- .../src/main/resources/universal_config.png | Bin 32658 -> 3907 bytes .../universalconfig/core/ZipSecurityTest.java | 69 +++ 1.8.9/NOTICE | 4 + 1.8.9/build.gradle | 5 +- 1.8.9/licenses/Apache-2.0.txt | 175 ++++++++ .../core/CurrentProcessRestartService.java | 145 ++++--- .../core/FileOperationLogger.java | 141 ++++++- .../core/ProfileCreateOptions.java | 1 + .../universalconfig/core/ProfileService.java | 137 +++++- .../core/UniversalConfigPaths.java | 11 +- .../core/UniversalConfigSettings.java | 20 + .../core/ZipArchiveReader.java | 34 +- .../core/ZipArchiveWriter.java | 3 + .../universalconfig/core/ZipSecurity.java | 47 +++ .../forgelegacy/LegacyScreens.java | 293 +++++++++++-- .../forgelegacy/LegacyTitleButtonHandler.java | 33 +- .../forgelegacy/UniversalConfigLegacyMod.java | 23 +- .../assets/universal_config/icon.png | Bin 32658 -> 3907 bytes .../assets/universal_config/lang/en_US.lang | 30 +- .../assets/universal_config/lang/en_us.json | 38 +- .../assets/universal_config/lang/ja_JP.lang | 30 +- .../assets/universal_config/lang/ja_jp.json | 38 +- .../assets/universal_config/lang/ko_KR.lang | 30 +- .../assets/universal_config/lang/ko_kr.json | 38 +- .../assets/universal_config/lang/zh_CN.lang | 28 +- .../assets/universal_config/lang/zh_TW.lang | 28 +- .../assets/universal_config/lang/zh_cn.json | 36 +- .../assets/universal_config/lang/zh_tw.json | 36 +- .../universal_config/title_screen_button.png | Bin 24132 -> 240 bytes 1.8.9/src/main/resources/mcmod.info | 2 +- 1.8.9/src/main/resources/universal_config.png | Bin 32658 -> 3907 bytes .../universalconfig/core/ZipSecurityTest.java | 69 +++ 26.1.x/NOTICE | 5 + 26.1.x/build.gradle | 4 +- 26.1.x/licenses/Apache-2.0.txt | 175 ++++++++ .../core/CurrentProcessRestartService.java | 394 +++++++----------- .../core/FileOperationLogger.java | 116 +++++- .../core/ProfileCreateOptions.java | 1 + .../universalconfig/core/ProfileService.java | 186 +++++++-- .../core/UniversalConfigPaths.java | 32 +- .../core/UniversalConfigSettings.java | 19 + .../core/ZipArchiveReader.java | 23 +- .../core/ZipArchiveWriter.java | 6 +- .../universalconfig/core/ZipSecurity.java | 52 ++- .../forge/ForgeRestartService.java | 10 +- .../forge/UniversalConfigMod.java | 60 ++- .../forge/screen/ProfileCreateScreen.java | 2 +- .../forge/screen/ProfileListScreen.java | 175 +++++++- .../forge/screen/ProfileRenameScreen.java | 71 ++++ 26.1.x/src/main/resources/META-INF/mods.toml | 2 +- .../assets/universal_config/icon.png | Bin 32658 -> 3907 bytes .../assets/universal_config/lang/en_us.json | 32 +- .../assets/universal_config/lang/ja_jp.json | 32 +- .../assets/universal_config/lang/ko_kr.json | 32 +- .../assets/universal_config/lang/zh_cn.json | 30 +- .../assets/universal_config/lang/zh_tw.json | 30 +- .../universal_config/title_screen_button.png | Bin 24132 -> 240 bytes .../src/main/resources/universal_config.png | Bin 32658 -> 3907 bytes .../CurrentProcessRestartServiceTest.java | 114 ++--- .../core/FileOperationLoggerTest.java | 58 +++ .../core/ProfileOrderingTest.java | 98 +++++ .../universalconfig/core/ZipSecurityTest.java | 18 + 26.2/NOTICE | 5 + 26.2/build.gradle | 4 +- 26.2/licenses/Apache-2.0.txt | 175 ++++++++ .../core/CurrentProcessRestartService.java | 394 +++++++----------- .../core/FileOperationLogger.java | 116 +++++- .../core/ProfileCreateOptions.java | 1 + .../universalconfig/core/ProfileService.java | 186 +++++++-- .../core/UniversalConfigPaths.java | 32 +- .../core/UniversalConfigSettings.java | 19 + .../core/ZipArchiveReader.java | 23 +- .../core/ZipArchiveWriter.java | 6 +- .../universalconfig/core/ZipSecurity.java | 52 ++- .../forge/ForgeRestartService.java | 10 +- .../forge/UniversalConfigMod.java | 60 ++- .../forge/screen/ProfileCreateScreen.java | 2 +- .../forge/screen/ProfileListScreen.java | 185 +++++++- .../forge/screen/ProfileRenameScreen.java | 71 ++++ 26.2/src/main/resources/META-INF/mods.toml | 2 +- .../assets/universal_config/icon.png | Bin 32658 -> 3907 bytes .../assets/universal_config/lang/en_us.json | 32 +- .../assets/universal_config/lang/ja_jp.json | 32 +- .../assets/universal_config/lang/ko_kr.json | 32 +- .../assets/universal_config/lang/zh_cn.json | 30 +- .../assets/universal_config/lang/zh_tw.json | 30 +- .../universal_config/title_screen_button.png | Bin 24132 -> 240 bytes 26.2/src/main/resources/universal_config.png | Bin 32658 -> 3907 bytes .../CurrentProcessRestartServiceTest.java | 134 +++--- .../core/FileOperationLoggerTest.java | 58 +++ .../core/ProfileOrderingTest.java | 98 +++++ .../universalconfig/core/ZipSecurityTest.java | 18 + 310 files changed, 13307 insertions(+), 3377 deletions(-) create mode 100644 1.12.2/NOTICE create mode 100644 1.12.2/licenses/Apache-2.0.txt create mode 100644 1.12.2/src/test/java/com/example/universalconfig/core/ZipSecurityTest.java create mode 100644 1.16.5/NOTICE create mode 100644 1.16.5/licenses/Apache-2.0.txt create mode 100644 1.16.5/src/main/java/com/example/universalconfig/forge/screen/ProfileRenameScreen.java create mode 100644 1.16.5/src/test/java/com/example/universalconfig/core/CurrentProcessRestartServiceTest.java create mode 100644 1.16.5/src/test/java/com/example/universalconfig/core/FileOperationLoggerTest.java create mode 100644 1.16.5/src/test/java/com/example/universalconfig/core/ProfileOrderingTest.java create mode 100644 1.16.5/src/test/java/com/example/universalconfig/core/ZipSecurityTest.java create mode 100644 1.18.2/NOTICE create mode 100644 1.18.2/licenses/Apache-2.0.txt create mode 100644 1.18.2/src/main/java/com/example/universalconfig/forge/screen/ProfileRenameScreen.java create mode 100644 1.18.2/src/test/java/com/example/universalconfig/core/CurrentProcessRestartServiceTest.java create mode 100644 1.18.2/src/test/java/com/example/universalconfig/core/FileOperationLoggerTest.java create mode 100644 1.18.2/src/test/java/com/example/universalconfig/core/ProfileOrderingTest.java create mode 100644 1.18.2/src/test/java/com/example/universalconfig/core/ZipSecurityTest.java create mode 100644 1.19.2/NOTICE create mode 100644 1.19.2/licenses/Apache-2.0.txt create mode 100644 1.19.2/src/main/java/com/example/universalconfig/forge/screen/ProfileRenameScreen.java create mode 100644 1.19.2/src/test/java/com/example/universalconfig/core/CurrentProcessRestartServiceTest.java create mode 100644 1.19.2/src/test/java/com/example/universalconfig/core/FileOperationLoggerTest.java create mode 100644 1.19.2/src/test/java/com/example/universalconfig/core/ProfileOrderingTest.java create mode 100644 1.19.2/src/test/java/com/example/universalconfig/core/ZipSecurityTest.java create mode 100644 1.20.1/NOTICE create mode 100644 1.20.1/licenses/Apache-2.0.txt create mode 100644 1.20.1/src/main/java/com/example/universalconfig/forge/screen/ProfileRenameScreen.java create mode 100644 1.20.1/src/test/java/com/example/universalconfig/core/FileOperationLoggerTest.java create mode 100644 1.20.1/src/test/java/com/example/universalconfig/core/ProfileOrderingTest.java create mode 100644 1.21.1/NOTICE create mode 100644 1.21.1/licenses/Apache-2.0.txt create mode 100644 1.21.1/src/main/java/com/example/universalconfig/forge/screen/ProfileRenameScreen.java create mode 100644 1.21.1/src/test/java/com/example/universalconfig/core/FileOperationLoggerTest.java create mode 100644 1.21.1/src/test/java/com/example/universalconfig/core/ProfileOrderingTest.java create mode 100644 1.7.10/NOTICE create mode 100644 1.7.10/licenses/Apache-2.0.txt create mode 100644 1.7.10/src/test/java/com/example/universalconfig/core/ZipSecurityTest.java create mode 100644 1.8.9/NOTICE create mode 100644 1.8.9/licenses/Apache-2.0.txt create mode 100644 1.8.9/src/test/java/com/example/universalconfig/core/ZipSecurityTest.java create mode 100644 26.1.x/NOTICE create mode 100644 26.1.x/licenses/Apache-2.0.txt create mode 100644 26.1.x/src/main/java/com/example/universalconfig/forge/screen/ProfileRenameScreen.java create mode 100644 26.1.x/src/test/java/com/example/universalconfig/core/FileOperationLoggerTest.java create mode 100644 26.1.x/src/test/java/com/example/universalconfig/core/ProfileOrderingTest.java create mode 100644 26.2/NOTICE create mode 100644 26.2/licenses/Apache-2.0.txt create mode 100644 26.2/src/main/java/com/example/universalconfig/forge/screen/ProfileRenameScreen.java create mode 100644 26.2/src/test/java/com/example/universalconfig/core/FileOperationLoggerTest.java create mode 100644 26.2/src/test/java/com/example/universalconfig/core/ProfileOrderingTest.java diff --git a/1.12.2/NOTICE b/1.12.2/NOTICE new file mode 100644 index 0000000..ee6501d --- /dev/null +++ b/1.12.2/NOTICE @@ -0,0 +1,4 @@ +Universal Config uses Gson from Google. + +Gson is licensed under the Apache License, Version 2.0. +The applicable license text is included in licenses/Apache-2.0.txt. diff --git a/1.12.2/build.gradle b/1.12.2/build.gradle index 2e56c52..41f4bff 100644 --- a/1.12.2/build.gradle +++ b/1.12.2/build.gradle @@ -13,7 +13,7 @@ apply plugin: 'net.minecraftforge.gradle' apply plugin: 'eclipse' apply plugin: 'maven-publish' -version = '1.0.0-mc1.12.2-forge' +version = '1.1.0-mc1.12.2-forge' group = 'com.example.universalconfig' archivesBaseName = 'universal_config' @@ -60,6 +60,7 @@ dependencies { // that the dep is a ForgeGradle 'patcher' dependency. And it's patches will be applied. // The userdev artifact is a special name and will get all sorts of transformations applied to it. minecraft 'net.minecraftforge:forge:1.12.2-14.23.5.2864' + testCompile 'junit:junit:4.13.2' // You may put jars on which you depend on in ./libs or you may define them like so.. // compile "some.group:artifact:version:classifier" @@ -134,4 +135,6 @@ jar { into 'META-INF' rename { 'LICENSE_universal_config' } } + from('NOTICE') { into 'META-INF' } + from('licenses') { into 'META-INF/licenses' } } diff --git a/1.12.2/licenses/Apache-2.0.txt b/1.12.2/licenses/Apache-2.0.txt new file mode 100644 index 0000000..b84f370 --- /dev/null +++ b/1.12.2/licenses/Apache-2.0.txt @@ -0,0 +1,175 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You meet + the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works + a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that + You distribute, all copyright, patent, trademark, and attribution + notices from the Source form of the Work, excluding those notices + that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing + the origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/1.12.2/src/main/java/com/example/universalconfig/core/CurrentProcessRestartService.java b/1.12.2/src/main/java/com/example/universalconfig/core/CurrentProcessRestartService.java index 439899d..8f119c1 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/core/CurrentProcessRestartService.java +++ b/1.12.2/src/main/java/com/example/universalconfig/core/CurrentProcessRestartService.java @@ -28,7 +28,15 @@ private CurrentProcessRestartService() { } public static void scheduleRestartAfterCurrentProcessExit() throws UniversalConfigException { - scheduleRestartAfterCurrentProcessExit(currentWorkingDirectory(), Collections.emptyList()); + scheduleRestartAfterCurrentProcessExit(currentWorkingDirectory(), currentProcessArguments()); + } + + public static List currentProcessArguments() { + try { + return ProcessDiscovery.current().arguments(); + } catch (RuntimeException ignored) { + return Collections.emptyList(); + } } /** @@ -38,7 +46,7 @@ public static void scheduleRestartAfterCurrentProcessExit() throws UniversalConf */ public static void scheduleRestartAfterCurrentProcessExit(Path workingDirectory) throws UniversalConfigException { - scheduleRestartAfterCurrentProcessExit(workingDirectory, Collections.emptyList()); + scheduleRestartAfterCurrentProcessExit(workingDirectory, currentProcessArguments()); } /** @@ -52,12 +60,12 @@ public static void scheduleRestartAfterCurrentProcessExit( Path helperClasspath ) throws UniversalConfigException { scheduleRestartAfterCurrentProcessExit( - workingDirectory, Collections.emptyList(), helperClasspath); + workingDirectory, currentProcessArguments(), helperClasspath); } /** - * Schedules the current Minecraft process to be replaced, using loader-provided arguments when the operating - * system does not expose the current process arguments through its process metadata API. + * Schedules the current Minecraft process to be replaced. Unsupported launchers use the loader-resolved Java + * arguments supplied by the loader adapter instead of reconstructing a command line from process metadata. */ public static void scheduleRestartAfterCurrentProcessExit( Path workingDirectory, @@ -78,7 +86,6 @@ private static void scheduleRestartAfterCurrentProcessExit( } Path normalizedWorkingDirectory = normalizeWorkingDirectory(workingDirectory); List ancestors = ancestorCommands(current); - Optional> currentArguments = currentJavaArguments(current.arguments(), loaderResolvedArguments); // Modrinth's documented launch URL requires a database-only internal ID that is not inherited by the game. // Guessing it from the folder name could launch the wrong profile, so only self-identifying launchers are used. Optional replacementCandidate = prismFamilyLauncherCommand( @@ -86,13 +93,9 @@ private static void scheduleRestartAfterCurrentProcessExit( if (!replacementCandidate.isPresent()) { replacementCandidate = atLauncherCommand(normalizedWorkingDirectory, ancestors); } - if (!replacementCandidate.isPresent()) { - replacementCandidate = gdLauncherCommand( - normalizedWorkingDirectory, ancestors, executable, currentArguments); - } LaunchCommand replacement = replacementCandidate.isPresent() ? replacementCandidate.get() - : currentArguments.isPresent() ? new LaunchCommand(executable, currentArguments.get()) : null; + : unsupportedLauncherCommand(executable, loaderResolvedArguments).orElse(null); if (replacement == null) { throw new UniversalConfigException("Could not determine how to restart this launcher instance."); } @@ -125,23 +128,79 @@ public static List buildJavaLaunchArguments( } } - private static Optional> currentJavaArguments( - List processArguments, + /** + * Builds the default restart command for a launcher without a dedicated integration. + * + *

The launcher is intentionally not inspected here. Every launcher that is not recognized by a dedicated + * detector follows this path, including GDLauncher and the official launcher.

+ */ + static Optional unsupportedLauncherCommand( + String currentExecutable, List loaderResolvedArguments - ) throws UniversalConfigException { + ) { + if (!isJavaExecutable(currentExecutable)) { + return Optional.empty(); + } + return validJavaLaunchArguments(loaderResolvedArguments) + .map(new java.util.function.Function, LaunchCommand>() { + @Override + public LaunchCommand apply(List arguments) { + return new LaunchCommand(currentExecutable, arguments); + } + }); + } + + private static Optional> validJavaLaunchArguments(List arguments) { + if (arguments == null || arguments.isEmpty()) { + return Optional.empty(); + } try { - if (processArguments != null && !processArguments.isEmpty()) { - return Optional.of(immutableCopy(processArguments)); + List copied = immutableCopy(arguments); + for (String value : copied) { + if (value == null || value.trim().isEmpty()) { + return Optional.empty(); + } } - if (loaderResolvedArguments == null || loaderResolvedArguments.isEmpty()) { - return Optional.empty(); + for (int index = 0; index + 2 < copied.size(); index++) { + if (("-cp".equals(copied.get(index)) || "-classpath".equals(copied.get(index))) + && !copied.get(index + 1).trim().isEmpty()) { + return hasMainClassAfter(copied, index + 2) ? Optional.of(copied) : Optional.>empty(); + } } - return Optional.of(immutableCopy(loaderResolvedArguments)); + return Optional.empty(); } catch (RuntimeException ex) { - throw new UniversalConfigException("Could not determine the current Java arguments.", ex); + return Optional.empty(); } } + private static boolean hasMainClassAfter(List arguments, int startIndex) { + int index = startIndex; + while (index < arguments.size()) { + String value = arguments.get(index); + if (!value.startsWith("-")) return true; + if (isInlineJvmOption(value)) { + index++; + } else if (isJvmOptionWithSeparateValue(value) && index + 1 < arguments.size()) { + index += 2; + } else { + return false; + } + } + return false; + } + + private static boolean isInlineJvmOption(String value) { + return value.startsWith("-D") || value.startsWith("-X") + || (value.startsWith("--") && value.indexOf('=') > 2); + } + + private static boolean isJvmOptionWithSeparateValue(String value) { + return "--add-modules".equals(value) || "--add-exports".equals(value) + || "--add-opens".equals(value) || "--add-reads".equals(value) + || "--limit-modules".equals(value) || "-p".equals(value) || "--module-path".equals(value) + || "--upgrade-module-path".equals(value) || "--patch-module".equals(value); + } + private static void scheduleJavaHelper( long currentPid, String helperJavaExecutable, @@ -348,42 +407,6 @@ static Optional atLauncherCommand( return Optional.empty(); } - static Optional gdLauncherCommand( - Path workingDirectory, - List ancestorCommands, - String currentExecutable, - Optional> currentArguments - ) { - if (!isGdLauncherGameDirectory(workingDirectory) - || !ancestorCommands.stream().anyMatch(command -> isGdLauncherExecutable(command.executable())) - || !ancestorCommands.stream().anyMatch(command -> isGdCoreModuleExecutable(command.executable())) - || !isJavaExecutable(currentExecutable)) { - return Optional.empty(); - } - - // Carbon does not provide a stable command-line launch interface for an instance. The core module supervises - // the Java child, so reusing the already resolved Java command is the only way to preserve the selected - // loader, natives, assets, account, and instance-specific arguments without guessing launcher internals. - return currentArguments.map(arguments -> new LaunchCommand(currentExecutable, arguments)); - } - - private static boolean isGdLauncherGameDirectory(Path workingDirectory) { - try { - Path gameDirectory = workingDirectory.toAbsolutePath().normalize(); - Path instanceDirectory = gameDirectory.getParent(); - Path instancesDirectory = instanceDirectory == null ? null : instanceDirectory.getParent(); - return gameDirectory.getFileName() != null - && gameDirectory.getFileName().toString().equalsIgnoreCase("instance") - && instanceDirectory != null - && Files.isRegularFile(instanceDirectory.resolve("instance.json")) - && instancesDirectory != null - && instancesDirectory.getFileName() != null - && instancesDirectory.getFileName().toString().equalsIgnoreCase("instances"); - } catch (RuntimeException ex) { - return false; - } - } - private static Optional> atLauncherJarArguments( List ancestorArguments, List launchArguments @@ -575,16 +598,6 @@ private static boolean isAtLauncherExecutable(String command) { return fileName.equalsIgnoreCase("atlauncher.exe") || fileName.equalsIgnoreCase("atlauncher"); } - private static boolean isGdLauncherExecutable(String command) { - String fileName = fileName(command); - return fileName.equalsIgnoreCase("gdlauncher.exe") || fileName.equalsIgnoreCase("gdlauncher"); - } - - private static boolean isGdCoreModuleExecutable(String command) { - String fileName = fileName(command); - return fileName.equalsIgnoreCase("core_module.exe") || fileName.equalsIgnoreCase("core_module"); - } - private static boolean isJavaExecutable(String command) { String fileName = fileName(command); return fileName.equalsIgnoreCase("java.exe") diff --git a/1.12.2/src/main/java/com/example/universalconfig/core/FileOperationLogger.java b/1.12.2/src/main/java/com/example/universalconfig/core/FileOperationLogger.java index 8db6802..9be027b 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/core/FileOperationLogger.java +++ b/1.12.2/src/main/java/com/example/universalconfig/core/FileOperationLogger.java @@ -7,14 +7,29 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Pattern; public final class FileOperationLogger { + private static final Pattern ABSOLUTE_WINDOWS_PATH = Pattern.compile( + "(?i)(?])/(?:[^\\s\\t,;:/]+/)*[^\\s\\t,;:/]+" + ); private static final String LAUNCH_ID = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss-SSS") .format(java.time.LocalDateTime.now()) + "-pid" + currentProcessId(); private static Path configuredRoot; + private static Path instanceRoot; + private static Path userHomeRoot; + private static Path appDataRoot; private static Path launchLogFile; private static Path latestLogFile; @@ -22,11 +37,23 @@ private FileOperationLogger() { } public static synchronized void configure(UniversalConfigSettings settings) { + configure(settings, null); + } + + public static synchronized void configure(UniversalConfigSettings settings, Path minecraftRunDirectory) { Path root = settings.rootDirectory().toAbsolutePath().normalize(); - if (root.equals(configuredRoot) && launchLogFile != null && latestLogFile != null) { + Path instance = normalizeOrNull(minecraftRunDirectory); + if (root.equals(configuredRoot) + && equalsPath(instance, instanceRoot) + && launchLogFile != null + && latestLogFile != null) { return; } configuredRoot = root; + instanceRoot = instance; + userHomeRoot = normalizeOrNull(Paths.get(System.getProperty("user.home", "."))); + String appData = System.getenv("APPDATA"); + appDataRoot = appData == null || appData.trim().isEmpty() ? null : normalizeOrNull(Paths.get(appData)); Path logsRoot = root.resolve(UniversalConfigFormat.LOGS_DIRECTORY_NAME); launchLogFile = logsRoot.resolve(UniversalConfigFormat.LAUNCH_LOGS_DIRECTORY_NAME) .resolve(UniversalConfigFormat.LAUNCH_LOG_FILE_PREFIX + LAUNCH_ID + ".log"); @@ -70,12 +97,12 @@ private static synchronized void write(String level, String operation, Path path .append(LAUNCH_ID).append('\t') .append(level).append('\t') .append(operation).append('\t') - .append(path == null ? "-" : path.toAbsolutePath().normalize()).append('\t') - .append(detail == null ? "" : detail.replace('\n', ' ').replace('\r', ' ')); + .append(sanitizePath(path)).append('\t') + .append(sanitizeText(detail)); if (throwable != null) { StringWriter writer = new StringWriter(); throwable.printStackTrace(new PrintWriter(writer)); - line.append('\t').append(writer.toString().replace('\n', ' ').replace('\r', ' ')); + line.append('\t').append(sanitizeText(writer.toString())); } line.append(System.lineSeparator()); byte[] encoded = line.toString().getBytes(StandardCharsets.UTF_8); @@ -88,6 +115,112 @@ private static synchronized void write(String level, String operation, Path path } } + static synchronized String sanitizePath(Path path) { + if (path == null) { + return "-"; + } + Path normalized = normalizeOrNull(path); + if (normalized == null) { + return ""; + } + List labels = new ArrayList(); + labels.add(new PathLabel(instanceRoot, "")); + labels.add(new PathLabel(configuredRoot, "")); + labels.add(new PathLabel(appDataRoot, "")); + labels.add(new PathLabel(userHomeRoot, "")); + List filtered = new ArrayList(); + for (PathLabel label : labels) { + if (label.root() != null) { + filtered.add(label); + } + } + // 深いパスほど先に一致させる。ファイル名キーは相対パス全体を含むため、 + // ルートが他のルートの接頭辞になる場合でもより具体的なラベルが優先される。 + Collections.sort(filtered, new Comparator() { + @Override + public int compare(PathLabel left, PathLabel right) { + return Integer.compare(right.root().getNameCount(), left.root().getNameCount()); + } + }); + for (PathLabel label : filtered) { + if (normalized.startsWith(label.root())) { + Path relative = label.root().relativize(normalized); + return relative.getNameCount() == 0 + ? label.label() + : label.label() + "/" + relative.toString().replace('\\', '/'); + } + } + Path fileName = normalized.getFileName(); + return fileName == null ? "" : "/" + fileName; + } + + static synchronized String sanitizeText(String value) { + if (value == null || value.trim().isEmpty()) { + return value == null ? "" : value; + } + String sanitized = value.replace('\n', ' ').replace('\r', ' '); + List labels = new ArrayList(); + labels.add(new PathLabel(instanceRoot, "")); + labels.add(new PathLabel(configuredRoot, "")); + labels.add(new PathLabel(appDataRoot, "")); + labels.add(new PathLabel(userHomeRoot, "")); + List filtered = new ArrayList(); + for (PathLabel label : labels) { + if (label.root() != null) { + filtered.add(label); + } + } + // 長いルート文字列ほど先に置換する。短いルートが先だと、共通接頭辞を持つ深いパスの + // 一部だけが置換され、残った相対部分が絶対パス正規表現に誤判定される可能性がある。 + Collections.sort(filtered, new Comparator() { + @Override + public int compare(PathLabel left, PathLabel right) { + return Integer.compare(right.root().toString().length(), left.root().toString().length()); + } + }); + for (PathLabel label : filtered) { + String nativeRoot = label.root().toString(); + sanitized = sanitized.replace(nativeRoot, label.label()) + .replace(nativeRoot.replace('\\', '/'), label.label()); + } + return ABSOLUTE_UNIX_PATH.matcher( + ABSOLUTE_WINDOWS_PATH.matcher(sanitized).replaceAll("") + ).replaceAll("").replace('\\', '/'); + } + + private static Path normalizeOrNull(Path path) { + if (path == null) { + return null; + } + try { + return path.toAbsolutePath().normalize(); + } catch (RuntimeException ex) { + return null; + } + } + + private static boolean equalsPath(Path left, Path right) { + return left == null ? right == null : left.equals(right); + } + + static final class PathLabel { + private final Path root; + private final String label; + + PathLabel(Path root, String label) { + this.root = root; + this.label = label; + } + + Path root() { + return root; + } + + String label() { + return label; + } + } + private static long currentProcessId() { String runtimeName = ManagementFactory.getRuntimeMXBean().getName(); int separator = runtimeName.indexOf('@'); diff --git a/1.12.2/src/main/java/com/example/universalconfig/core/ProfileCreateOptions.java b/1.12.2/src/main/java/com/example/universalconfig/core/ProfileCreateOptions.java index ffa7fc9..591bb34 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/core/ProfileCreateOptions.java +++ b/1.12.2/src/main/java/com/example/universalconfig/core/ProfileCreateOptions.java @@ -7,6 +7,7 @@ public final class ProfileCreateOptions { public String name; public String description; public String icon = ProfileIcon.GRASS_BLOCK; + // 画面の初期選択はUI側で管理する。非UIの呼び出し元では従来どおり全項目を保存対象にする。 public boolean includeKeybinds = true; public boolean includeClientOptions = true; public boolean includeModConfigs = true; diff --git a/1.12.2/src/main/java/com/example/universalconfig/core/ProfileService.java b/1.12.2/src/main/java/com/example/universalconfig/core/ProfileService.java index a26512c..e786749 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/core/ProfileService.java +++ b/1.12.2/src/main/java/com/example/universalconfig/core/ProfileService.java @@ -19,6 +19,8 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -172,14 +174,145 @@ public List listProfiles() throws UniversalConfigException { } } summaries.sort(Comparator.comparing((ProfileSummary summary) -> safeUpdatedAt(summary.manifest())).reversed()); - FileOperationLogger.info("LIST_PROFILES", profiles, "count=" + summaries.size()); - return summaries; + List ordered = applyProfileOrder(summaries); + FileOperationLogger.info("LIST_PROFILES", profiles, "count=" + ordered.size()); + return ordered; } catch (IOException ex) { FileOperationLogger.failure("LIST_PROFILES", profiles, "failed", ex); throw new UniversalConfigException("Failed to list profiles.", ex); } } + public void renameProfile(Path profilePath, String name) throws UniversalConfigException { + Path normalized = validateProfilePath(profilePath); + String requestedName = name == null ? "" : name.trim(); + if (requestedName.trim().isEmpty()) { + throw new UniversalConfigException("Profile name is required."); + } + if (requestedName.length() > 128) { + throw new UniversalConfigException("Profile name is too long."); + } + + try (ProfileDirectoryLock ignored = lockProfileDirectory()) { + ProfileManifest manifest = readManifest(normalized); + String uniqueName = uniqueProfileName(requestedName, normalized); + if (Objects.equals(manifest.name, uniqueName)) { + return; + } + rewriteProfileName(normalized, uniqueName); + FileOperationLogger.info("RENAME_PROFILE", normalized, + "from=" + manifest.name + " to=" + uniqueName); + } + } + + public void moveProfile(Path instancePath, Path profilePath, int targetIndex) throws UniversalConfigException { + Path normalized = validateProfilePath(profilePath); + try (ProfileDirectoryLock ignored = lockProfileDirectory()) { + List summaries = listProfiles(); + int currentIndex = indexOfProfile(summaries, normalized); + if (currentIndex < 0) { + throw new UniversalConfigException("The selected profile is unavailable."); + } + if (targetIndex < 0 || targetIndex >= summaries.size()) { + throw new UniversalConfigException("The selected profile position is invalid."); + } + if (currentIndex == targetIndex) { + return; + } + + List order = new ArrayList(); + for (ProfileSummary summary : summaries) { + String key = profileOrderKey(summary); + if (key != null) { + order.add(key); + } + } + String movedKey = profileOrderKey(summaries.get(currentIndex)); + order.remove(movedKey); + order.add(targetIndex, movedKey); + settings.setProfileOrder(order); + UniversalConfigPaths.saveSettings(instancePath, settings); + FileOperationLogger.info("MOVE_PROFILE", normalized, "targetIndex=" + targetIndex); + } + } + + private List applyProfileOrder(List summaries) { + List configuredOrder = settings.profileOrder(); + if (configuredOrder.isEmpty()) { + return summaries; + } + + Map byKey = new HashMap(); + for (ProfileSummary summary : summaries) { + String key = profileOrderKey(summary); + if (key != null) { + byKey.putIfAbsent(key, summary); + } + } + + // 過去バージョンで id ベースのキーを保存済みの設定との互換性のため、id も補助インデックスとして残す。 + Map byLegacyIdKey = new HashMap(); + for (ProfileSummary summary : summaries) { + String legacyIdKey = legacyProfileOrderKey(summary); + if (legacyIdKey != null) { + byLegacyIdKey.putIfAbsent(legacyIdKey, summary); + } + } + + List ordered = new ArrayList(summaries.size()); + Set included = new HashSet(); + for (String key : configuredOrder) { + // ファイル名キーを優先し、無ければ旧 id キーで解決する。 + ProfileSummary summary = byKey.get(key); + if (summary == null) { + summary = byLegacyIdKey.get(key); + } + if (summary != null) { + String fileKey = profileOrderKey(summary); + if (fileKey != null && included.add(fileKey)) { + ordered.add(summary); + } + } + } + for (ProfileSummary summary : summaries) { + String key = profileOrderKey(summary); + if (key == null || included.add(key)) { + ordered.add(summary); + } + } + return ordered; + } + + private int indexOfProfile(List summaries, Path profilePath) { + for (int index = 0; index < summaries.size(); index++) { + if (summaries.get(index).path().toAbsolutePath().normalize().equals(profilePath)) { + return index; + } + } + return -1; + } + + private String profileOrderKey(ProfileSummary summary) { + if (summary == null) { + return null; + } + // ファイル名を並び順キーに使う。manifest.id は複製が元 id を保持するため一意性が保証されず、 + // id ベースの重複排除で複製が一覧から消える問題がある(PR #43 P1)。 + // rename や duplicate でもファイル名は変化しないため、ファイル名は安定した識別子になる。 + Path fileName = summary.path() == null ? null : summary.path().getFileName(); + return fileName == null ? null : "file:" + fileName; + } + + // 過去バージョンで settings.profileOrder に id ベースキーを保存していた設定との互換用。 + // 新規保存では使わない。applyProfileOrder のフォールバック解決でのみ参照する。 + private String legacyProfileOrderKey(ProfileSummary summary) { + if (summary == null || summary.manifest() == null) { + return null; + } + String id = summary.manifest().id; + return id == null || id.trim().isEmpty() ? null : "id:" + id; + } + public List listBackups() throws UniversalConfigException { Path backups = UniversalConfigPaths.backupsDirectory(settings); FileOperationLogger.info("LIST_BACKUPS", backups, "start"); diff --git a/1.12.2/src/main/java/com/example/universalconfig/core/UniversalConfigPaths.java b/1.12.2/src/main/java/com/example/universalconfig/core/UniversalConfigPaths.java index ef2767a..a76b4a4 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/core/UniversalConfigPaths.java +++ b/1.12.2/src/main/java/com/example/universalconfig/core/UniversalConfigPaths.java @@ -12,6 +12,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Locale; +import java.util.List; public final class UniversalConfigPaths { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); @@ -40,14 +41,16 @@ public static UniversalConfigSettings loadOrCreateSettings(Path minecraftRunDire UniversalConfigSettings settings = new UniversalConfigSettings(Paths.get(dto.rootDirectory)); boolean invalidDefaultProfilePath = hasInvalidDefaultProfilePath(dto.defaultProfilePath); settings.setDefaultProfilePath(parseDefaultProfilePath(dto.defaultProfilePath)); + settings.setProfileOrder(dto.profileOrder); ensureDirectories(settings); - FileOperationLogger.configure(settings); + FileOperationLogger.configure(settings, minecraftRunDirectory); Path sharedSettingsFile = rootSettingsFile(settings); if (Files.isRegularFile(sharedSettingsFile)) { SettingsDto sharedDto = readSettingsDto(sharedSettingsFile); invalidDefaultProfilePath = hasInvalidDefaultProfilePath(sharedDto == null ? null : sharedDto.defaultProfilePath); settings.setDefaultProfilePath(parseDefaultProfilePath( sharedDto == null ? null : sharedDto.defaultProfilePath)); + settings.setProfileOrder(sharedDto == null ? null : sharedDto.profileOrder); } else { // Older versions stored the default only inside one instance. Migrate it to the shared root. saveRootSettings(settings); @@ -65,12 +68,13 @@ public static UniversalConfigSettings loadOrCreateSettings(Path minecraftRunDire UniversalConfigSettings settings = new UniversalConfigSettings(defaultRootDirectory()); ensureDirectories(settings); - FileOperationLogger.configure(settings); + FileOperationLogger.configure(settings, minecraftRunDirectory); Path sharedSettingsFile = rootSettingsFile(settings); if (Files.isRegularFile(sharedSettingsFile)) { SettingsDto sharedDto = readSettingsDto(sharedSettingsFile); settings.setDefaultProfilePath(parseDefaultProfilePath( sharedDto == null ? null : sharedDto.defaultProfilePath)); + settings.setProfileOrder(sharedDto == null ? null : sharedDto.profileOrder); } saveSettings(minecraftRunDirectory, settings); return settings; @@ -85,6 +89,7 @@ public static void saveSettings(Path minecraftRunDirectory, UniversalConfigSetti dto.defaultProfilePath = settings.defaultProfilePath() == null ? null : settings.defaultProfilePath().toAbsolutePath().normalize().toString(); + dto.profileOrder = settings.profileOrder(); try (Writer writer = Files.newBufferedWriter(localSettings, StandardCharsets.UTF_8)) { GSON.toJson(dto, writer); } @@ -160,6 +165,7 @@ public static String safeFileSlug(String value) { private static final class SettingsDto { String rootDirectory; String defaultProfilePath; + List profileOrder; } private static SettingsDto readSettingsDto(Path path) throws UniversalConfigException { @@ -179,6 +185,7 @@ private static void saveRootSettings(UniversalConfigSettings settings) throws Un dto.defaultProfilePath = settings.defaultProfilePath() == null ? null : settings.defaultProfilePath().toAbsolutePath().normalize().toString(); + dto.profileOrder = settings.profileOrder(); try (Writer writer = Files.newBufferedWriter(rootSettings, StandardCharsets.UTF_8)) { GSON.toJson(dto, writer); } diff --git a/1.12.2/src/main/java/com/example/universalconfig/core/UniversalConfigSettings.java b/1.12.2/src/main/java/com/example/universalconfig/core/UniversalConfigSettings.java index 174087c..0e94647 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/core/UniversalConfigSettings.java +++ b/1.12.2/src/main/java/com/example/universalconfig/core/UniversalConfigSettings.java @@ -1,10 +1,14 @@ package com.example.universalconfig.core; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; public final class UniversalConfigSettings { private Path rootDirectory; private Path defaultProfilePath; + private List profileOrder = new ArrayList(); public UniversalConfigSettings(Path rootDirectory) { this.rootDirectory = rootDirectory; @@ -25,4 +29,20 @@ public Path defaultProfilePath() { public void setDefaultProfilePath(Path defaultProfilePath) { this.defaultProfilePath = defaultProfilePath; } + + public List profileOrder() { + return Collections.unmodifiableList(profileOrder); + } + + public void setProfileOrder(List profileOrder) { + this.profileOrder = new ArrayList(); + if (profileOrder == null) { + return; + } + for (String profileKey : profileOrder) { + if (profileKey != null && !profileKey.trim().isEmpty() && !this.profileOrder.contains(profileKey)) { + this.profileOrder.add(profileKey); + } + } + } } diff --git a/1.12.2/src/main/java/com/example/universalconfig/core/ZipArchiveReader.java b/1.12.2/src/main/java/com/example/universalconfig/core/ZipArchiveReader.java index 2f39b94..d1d917d 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/core/ZipArchiveReader.java +++ b/1.12.2/src/main/java/com/example/universalconfig/core/ZipArchiveReader.java @@ -16,11 +16,25 @@ public final class ZipArchiveReader implements ProfileArchiveReader { public ZipArchiveReader(Path archivePath) throws IOException, UniversalConfigException { this.archivePath = archivePath; this.zipFile = new ZipFile(archivePath.toFile()); - FileOperationLogger.info("OPEN_ZIP", archivePath, "read"); - Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - ZipSecurity.validateRelativeEntryName(entry.getName()); + try { + FileOperationLogger.info("OPEN_ZIP", archivePath, "read"); + long totalUncompressedSize = 0; + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + ZipSecurity.validateRelativeEntryName(entry.getName()); + if (!entry.isDirectory()) { + totalUncompressedSize = ZipSecurity.validateEntrySizes( + entry.getName(), entry.getSize(), entry.getCompressedSize(), totalUncompressedSize); + } + } + } catch (UniversalConfigException | RuntimeException ex) { + try { + zipFile.close(); + } catch (IOException closeException) { + ex.addSuppressed(closeException); + } + throw ex; } } @@ -43,8 +57,14 @@ public InputStream open(String entryName) throws IOException, UniversalConfigExc @Override public List entries() { - List names = new ArrayList<>(); - zipFile.stream().filter(entry -> !entry.isDirectory()).forEach(entry -> names.add(entry.getName())); + List names = new ArrayList(); + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (!entry.isDirectory()) { + names.add(entry.getName()); + } + } return names; } diff --git a/1.12.2/src/main/java/com/example/universalconfig/core/ZipArchiveWriter.java b/1.12.2/src/main/java/com/example/universalconfig/core/ZipArchiveWriter.java index ac9c235..2cb34dd 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/core/ZipArchiveWriter.java +++ b/1.12.2/src/main/java/com/example/universalconfig/core/ZipArchiveWriter.java @@ -18,8 +18,11 @@ public static void write(Path path, Map entries) throws IOExcept Files.createDirectories(path.getParent()); FileOperationLogger.info("CREATE_DIRECTORY", path.getParent(), "zip parent"); try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(path))) { + long totalUncompressedSize = 0; for (Map.Entry entry : entries.entrySet()) { ZipSecurity.validateRelativeEntryName(entry.getKey()); + totalUncompressedSize = ZipSecurity.validateUncompressedSize( + entry.getKey(), entry.getValue().length, totalUncompressedSize); output.putNextEntry(new ZipEntry(entry.getKey())); output.write(entry.getValue()); output.closeEntry(); diff --git a/1.12.2/src/main/java/com/example/universalconfig/core/ZipSecurity.java b/1.12.2/src/main/java/com/example/universalconfig/core/ZipSecurity.java index 5aad6d6..c6d9282 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/core/ZipSecurity.java +++ b/1.12.2/src/main/java/com/example/universalconfig/core/ZipSecurity.java @@ -4,6 +4,15 @@ import java.nio.file.Paths; public final class ZipSecurity { + /** Maximum uncompressed size accepted for one profile or backup entry. */ + public static final long MAX_ENTRY_UNCOMPRESSED_BYTES = 32L * 1024L * 1024L; + + /** Maximum combined uncompressed size accepted for one archive. */ + public static final long MAX_TOTAL_UNCOMPRESSED_BYTES = 256L * 1024L * 1024L; + + /** Maximum declared expansion ratio accepted for a non-empty entry. */ + public static final long MAX_COMPRESSION_RATIO = 1_000L; + private ZipSecurity() { } @@ -44,4 +53,42 @@ public static Path safeResolve(Path destinationRoot, String entryName) throws Un } return resolved; } + + /** + * Validates central-directory size metadata before any archive payload is read. + * The returned value is the accumulated uncompressed size for the archive. + */ + public static long validateEntrySizes( + String entryName, + long uncompressedSize, + long compressedSize, + long currentTotal + ) throws UniversalConfigException { + if (uncompressedSize < 0 || compressedSize < 0) { + throw new UniversalConfigException("ZIP entry size metadata is missing: " + entryName); + } + long total = validateUncompressedSize(entryName, uncompressedSize, currentTotal); + if (uncompressedSize > 0 + && (compressedSize == 0 || uncompressedSize / compressedSize > MAX_COMPRESSION_RATIO)) { + throw new UniversalConfigException("ZIP entry compression ratio is unsafe: " + entryName); + } + return total; + } + + public static long validateUncompressedSize( + String entryName, + long uncompressedSize, + long currentTotal + ) throws UniversalConfigException { + if (uncompressedSize < 0) { + throw new UniversalConfigException("ZIP entry size is invalid: " + entryName); + } + if (uncompressedSize > MAX_ENTRY_UNCOMPRESSED_BYTES) { + throw new UniversalConfigException("ZIP entry exceeds the uncompressed size limit: " + entryName); + } + if (currentTotal < 0 || currentTotal > MAX_TOTAL_UNCOMPRESSED_BYTES - uncompressedSize) { + throw new UniversalConfigException("ZIP archive exceeds the total uncompressed size limit."); + } + return currentTotal + uncompressedSize; + } } diff --git a/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/LegacyScreens.java b/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/LegacyScreens.java index 1d9ed9f..1acc2e2 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/LegacyScreens.java +++ b/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/LegacyScreens.java @@ -46,7 +46,9 @@ public final class LegacyScreens { private static final int PROFILE_ICON_SIZE = 28; private static final int MORE_MENU_WIDTH = 132; private static final int MORE_MENU_ITEM_GAP = 2; - private static final int MORE_MENU_ITEM_COUNT = 5; + private static final int MORE_MENU_ITEM_COUNT = 6; + private static final int REORDER_BUTTON_WIDTH = 20; + private static final int REORDER_BUTTON_HEIGHT = 16; private static final int SCROLLBAR_WIDTH = 3; private static final int PANEL = 0xD0181818; private static final int PANEL_INNER = 0xB00B0B0B; @@ -131,6 +133,10 @@ public static class ProfileList extends Base implements GuiYesNoCallback { private Path deleteConfirmationPath; private boolean draggingScrollbar; private int scrollbarDragOffset; + private int draggingProfileIndex = -1; + private int dragTargetIndex = -1; + private int dragStartY; + private boolean draggingProfile; public ProfileList(GuiScreen parent) { super(parent); @@ -152,16 +158,23 @@ public void initGui() { tr("screen.universal_config.use_profile"))); buttonList.add(createButton(4, actionX + useWidth + 4, actionY, moreWidth, BUTTON_HEIGHT, tr("screen.universal_config.more"))); + // moreMenuの項目順序は main 側の ProfileListScreen に合わせる。 + // default / open_folder / rename / duplicate / backups / delete の6項目。 buttonList.add(createButton(30, moreMenuLeft(), moreMenuButtonY(0), moreMenuWidth(), BUTTON_HEIGHT, "")); buttonList.add(createButton(31, moreMenuLeft(), moreMenuButtonY(1), moreMenuWidth(), BUTTON_HEIGHT, - tr("screen.universal_config.duplicate"))); - buttonList.add(createButton(32, moreMenuLeft(), moreMenuButtonY(2), moreMenuWidth(), BUTTON_HEIGHT, tr("screen.universal_config.open_folder"))); + buttonList.add(createButton(32, moreMenuLeft(), moreMenuButtonY(2), moreMenuWidth(), BUTTON_HEIGHT, + tr("screen.universal_config.rename"))); buttonList.add(createButton(33, moreMenuLeft(), moreMenuButtonY(3), moreMenuWidth(), BUTTON_HEIGHT, - tr("screen.universal_config.backups"))); + tr("screen.universal_config.duplicate"))); buttonList.add(createButton(34, moreMenuLeft(), moreMenuButtonY(4), moreMenuWidth(), BUTTON_HEIGHT, + tr("screen.universal_config.backups"))); + buttonList.add(createButton(35, moreMenuLeft(), moreMenuButtonY(5), moreMenuWidth(), BUTTON_HEIGHT, tr("screen.universal_config.delete"))); buttonList.add(createButton(0, width - 28, 6, 20, BUTTON_HEIGHT, "×")); + // 並べ替えボタン(↑/↓)は表示行ごとに追加する。scroll 後の位置を再計算できるように + // initGui の末尾で構築し、cardWidth の右端に配置する(PR #43)。 + addReorderButtons(); if (pending) { int y = HEADER_HEIGHT + 5; buttonList.add(createButton(9, rightPanelRight() - 194, y, 92, BUTTON_HEIGHT, @@ -282,7 +295,7 @@ private void updateButtons() { for (Object value : buttonList) { GuiButton button = (GuiButton) value; if (button.id == 2 || button.id == 4) button.enabled = has; - if (button.id >= 30 && button.id <= 34) { + if (button.id >= 30 && button.id <= 35) { button.visible = moreMenuOpen; button.enabled = has; } @@ -302,6 +315,51 @@ private GuiButton findButton(int id) { return null; } + private void addReorderButtons() { + // 並べ替えボタン(ID 40=↑, 41=↓)は cardWidth の右端に配置し、 + // 表示行ごとに初期化する。scroll 変化で位置を再計算するため initGui 末尾で呼ぶ(PR #43)。 + for (int i = scroll; i < Math.min(profiles.size(), scroll + visibleRows()); i++) { + int y = listTop() + (i - scroll) * CARD_STEP; + int x = cardLeft() + cardWidth() - REORDER_BUTTON_WIDTH - 4; + GuiButton up = createButton(40, x, y + 3, REORDER_BUTTON_WIDTH, REORDER_BUTTON_HEIGHT, "↑"); + up.enabled = i > 0; + GuiButton down = createButton(41, x, y + 3 + REORDER_BUTTON_HEIGHT + 1, + REORDER_BUTTON_WIDTH, REORDER_BUTTON_HEIGHT, "↓"); + down.enabled = i < profiles.size() - 1; + buttonList.add(up); + buttonList.add(down); + } + } + + private void moveProfile(int sourceIndex, int targetIndex) { + // main 側の ProfileListScreen.moveProfile に合わせ、範囲外・同一インデックスは無視する。 + if (sourceIndex < 0 || sourceIndex >= profiles.size() + || targetIndex < 0 || targetIndex >= profiles.size() + || sourceIndex == targetIndex) { + return; + } + Path path = profiles.get(sourceIndex).path(); + try { + service().moveProfile(LegacyPlatform.gameDirectory(), path, targetIndex); + initGui(); + selectProfile(path); + } catch (Exception ex) { + fail("screen.universal_config.action_failed", ex); + } + } + + private void selectProfile(Path path) { + // 並び替え前のインデックスは再読込後には別プロフィールを指す場合がある。 + // 操作対象を維持するため、安定したファイルパスで移動元を選択し直す。 + for (int i = 0; i < profiles.size(); i++) { + if (profiles.get(i).path().equals(path)) { + selected = i; + updateButtons(); + return; + } + } + } + private int moreMenuWidth() { return Math.min(MORE_MENU_WIDTH, rightPanelRight() - rightPanelLeft() - PANEL_PADDING * 2); } @@ -354,29 +412,37 @@ protected void actionPerformed(GuiButton button) { } break; case 31: + if (path != null) Desktop.getDesktop().browse(path.getParent().toUri()); + moreMenuOpen = false; + break; + case 32: + if (path != null) { + moreMenuOpen = false; + mc.displayGuiScreen(new ProfileRename(this, path, profileName())); + } + break; + case 33: if (path != null) { service().duplicateProfile(path); moreMenuOpen = false; initGui(); } break; - case 32: - if (path != null) Desktop.getDesktop().browse(path.getParent().toUri()); - moreMenuOpen = false; - break; - case 33: + case 34: moreMenuOpen = false; mc.displayGuiScreen(new Backups(this)); break; - case 34: + case 35: if (path != null) { deleteConfirmationPath = path; moreMenuOpen = false; mc.displayGuiScreen(new GuiYesNo(this, tr("screen.universal_config.delete_confirm", profileName()), - tr("screen.universal_config.delete_warning"), 34)); + tr("screen.universal_config.delete_warning"), 35)); } break; + case 40: moveProfile(selected, selected - 1); break; + case 41: moveProfile(selected, selected + 1); break; case 9: restart(); break; case 10: service().clearPendingImport(LegacyPlatform.gameDirectory()); initGui(); break; default: break; @@ -405,7 +471,7 @@ private void restart() { @Override public void confirmClicked(boolean result, int id) { mc.displayGuiScreen(this); - if (result && id == 34 && deleteConfirmationPath != null) { + if (result && id == 35 && deleteConfirmationPath != null) { try { service().deleteProfile(LegacyPlatform.gameDirectory(), deleteConfirmationPath); initGui(); @@ -418,7 +484,13 @@ public void confirmClicked(boolean result, int id) { @Override public void handleMouseInput() { + int eventButton = Mouse.getEventButton(); + boolean leftReleased = eventButton == 0 && !Mouse.getEventButtonState(); try { super.handleMouseInput(); } catch (Exception ex) { fail("screen.universal_config.action_failed", ex); } + if (leftReleased && draggingProfileIndex >= 0) { + finishProfileDrag(Mouse.getEventX() * width / mc.displayWidth, + height - Mouse.getEventY() * height / mc.displayHeight - 1); + } int wheel = Mouse.getEventDWheel(); if (wheel != 0) { scroll += wheel < 0 ? 1 : -1; @@ -429,6 +501,8 @@ public void handleMouseInput() { @Override protected void mouseClicked(int mouseX, int mouseY, int mouseButton) { draggingScrollbar = false; + draggingProfileIndex = -1; + draggingProfile = false; if (mouseButton == 0 && hasScrollbar() && mouseX >= scrollbarLeft() - 2 && mouseX < scrollbarLeft() + SCROLLBAR_WIDTH + 2 && mouseY >= listTop() && mouseY < listBottom()) { @@ -452,15 +526,21 @@ protected void mouseClicked(int mouseX, int mouseY, int mouseButton) { moreMenuOpen = false; updateButtons(); } - try { super.mouseClicked(mouseX, mouseY, mouseButton); } catch (Exception ex) { fail("screen.universal_config.action_failed", ex); } - if (mouseX >= cardLeft() && mouseX < cardLeft() + cardWidth() - && mouseY >= listTop() && mouseY < listBottom()) { - int index = scroll + (mouseY - listTop()) / CARD_STEP; - if (index >= 0 && index < profiles.size()) { - selected = index; - updateButtons(); + if (mouseButton == 0) { + int profileIndex = profileIndexAt(mouseX, mouseY); + int reorderDelta = reorderDeltaAt(mouseX, mouseY); + if (profileIndex >= 0 && reorderDelta != 0) { + moveProfile(profileIndex, profileIndex + reorderDelta); + return; + } + if (profileIndex >= 0) { + draggingProfileIndex = profileIndex; + dragTargetIndex = profileIndex; + dragStartY = mouseY; + return; } } + try { super.mouseClicked(mouseX, mouseY, mouseButton); } catch (Exception ex) { fail("screen.universal_config.action_failed", ex); } } @Override @@ -469,6 +549,15 @@ protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, lo scrollToThumbPosition(mouseY); return; } + if (draggingProfileIndex >= 0 && clickedMouseButton == 0) { + if (!draggingProfile && Math.abs(mouseY - dragStartY) > 3) { + draggingProfile = true; + } + if (draggingProfile) { + dragTargetIndex = dropIndexAt(mouseY); + } + return; + } try { super.mouseClickMove(mouseX, mouseY, clickedMouseButton, timeSinceLastClick); } catch (Exception ex) { @@ -476,6 +565,49 @@ protected void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, lo } } + private void finishProfileDrag(int mouseX, int mouseY) { + int sourceIndex = draggingProfileIndex; + int targetIndex = dragTargetIndex; + boolean wasDragging = draggingProfile; + draggingProfileIndex = -1; + dragTargetIndex = -1; + draggingProfile = false; + if (wasDragging) { + moveProfile(sourceIndex, targetIndex); + } else if (profileIndexAt(mouseX, mouseY) == sourceIndex) { + selected = sourceIndex; + updateButtons(); + } + } + + private int profileIndexAt(int mouseX, int mouseY) { + if (mouseX < cardLeft() || mouseX >= cardLeft() + cardWidth() + || mouseY < listTop() || mouseY >= listBottom()) { + return -1; + } + int rowOffset = mouseY - listTop(); + int index = scroll + rowOffset / CARD_STEP; + return index >= 0 && index < profiles.size() && rowOffset % CARD_STEP < CARD_HEIGHT ? index : -1; + } + + private int reorderDeltaAt(int mouseX, int mouseY) { + int index = profileIndexAt(mouseX, mouseY); + if (index < 0) return 0; + int x = cardLeft() + cardWidth() - REORDER_BUTTON_WIDTH - 4; + int y = listTop() + (index - scroll) * CARD_STEP + 3; + if (mouseX >= x && mouseX < x + REORDER_BUTTON_WIDTH + && mouseY >= y && mouseY < y + REORDER_BUTTON_HEIGHT) return -1; + int downY = y + REORDER_BUTTON_HEIGHT + 1; + return mouseX >= x && mouseX < x + REORDER_BUTTON_WIDTH + && mouseY >= downY && mouseY < downY + REORDER_BUTTON_HEIGHT ? 1 : 0; + } + + private int dropIndexAt(int mouseY) { + int relativeY = mouseY - listTop() - CARD_HEIGHT / 2; + int index = scroll + relativeY / CARD_STEP; + return Math.max(0, Math.min(profiles.size() - 1, index)); + } + @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { drawDefaultBackground(); @@ -503,7 +635,7 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { int iconY = y + (CARD_HEIGHT - PROFILE_ICON_SIZE) / 2; LegacyVersionBridge.drawProfileIcon(mc, manifest.icon, iconX, iconY, PROFILE_ICON_SIZE); int textX = iconX + PROFILE_ICON_SIZE + 8; - int textWidth = cardLeft() + cardWidth() - textX - 7; + int textWidth = cardLeft() + cardWidth() - textX - REORDER_BUTTON_WIDTH - 12; if (isDefault(profiles.get(i).path())) { String marker = tr("screen.universal_config.default_marker"); int markerWidth = Math.min(font().getStringWidth(marker), textWidth); @@ -534,6 +666,11 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { moreMenuBottom(), 0xFF181818); } super.drawScreen(mouseX, mouseY, partialTicks); + if (draggingProfile && dragTargetIndex >= 0 && dragTargetIndex != draggingProfileIndex) { + int indicatorY = listTop() + (dragTargetIndex - scroll) * CARD_STEP + + (dragTargetIndex > draggingProfileIndex ? CARD_HEIGHT - 1 : -1); + drawRect(cardLeft(), indicatorY, cardLeft() + cardWidth(), indicatorY + 2, 0xFFD8E3FF); + } drawStatus(); } @@ -571,6 +708,10 @@ private void drawDetails() { drawString(font(), font().trimStringToWidth(tr("screen.universal_config.loader_summary", loader, loaderVersion), contentWidth - PROFILE_ICON_SIZE - 8), infoX, line + 15, MUTED); line += 35; + // 詳細画面に .ucp ファイル名を表示し、複数プロファイルで実体を区別できるようにする(PR #43)。 + String fileName = profiles.get(selected).path().getFileName().toString(); + drawString(font(), font().trimStringToWidth(fileName, contentWidth), innerX, line, MUTED); + line += 12; String description = manifest.description == null || manifest.description.trim().isEmpty() ? tr("screen.universal_config.description_none") : manifest.description; for (Object value : font().listFormattedStringToWidth(description, contentWidth)) { @@ -581,18 +722,24 @@ private void drawDetails() { } if (manifest.includes != null) { line += 5; - drawRect(innerX, line, innerX + contentWidth, line + 1, DIVIDER); - line += 8; - drawString(font(), tr("screen.universal_config.included_settings"), innerX, line, TEXT); - line += 14; - line = drawIncluded(manifest.includes.keybinds, "screen.universal_config.target_keybinds", innerX, line); - line = drawIncluded(manifest.includes.clientOptions, "screen.universal_config.target_client", innerX, line); - drawIncluded(manifest.includes.modConfigs, "screen.universal_config.target_mods", innerX, line); + if (line + font().FONT_HEIGHT < bottom) { + drawRect(innerX, line, innerX + contentWidth, line + 1, DIVIDER); + line += 8; + drawString(font(), tr("screen.universal_config.included_settings"), innerX, line, TEXT); + line += 14; + line = drawIncluded(manifest.includes.keybinds, "screen.universal_config.target_keybinds", + innerX, line, bottom); + line = drawIncluded(manifest.includes.clientOptions, "screen.universal_config.target_client", + innerX, line, bottom); + drawIncluded(manifest.includes.modConfigs, "screen.universal_config.target_mods", + innerX, line, bottom); + } } } - private int drawIncluded(boolean included, String key, int x, int y) { - if (included) { + private int drawIncluded(boolean included, String key, int x, int y, int bottom) { + // 小さいウィンドウでは詳細欄の下に操作ボタンがある。内容をボタン領域へ描画しない。 + if (included && y + font().FONT_HEIGHT <= bottom) { drawString(font(), "✓", x, y, 0x55FF55); drawString(font(), tr(key), x + 14, y, TEXT); return y + 14; @@ -610,12 +757,96 @@ private String safe(String value) { } } + private static final class ProfileRename extends Base { + // main 側の ProfileRenameScreen を旧Forgeへ移植した画面。GuiTextField で新しい名前を入力し、 + // ProfileService.renameProfile でマニフェストを更新する(PR #43)。 + private static final int FORM_WIDTH = 300; + private static final int FIELD_Y = 64; + private final Path profilePath; + private GuiTextField nameField; + + private ProfileRename(GuiScreen parent, Path profilePath, String initialName) { + super(parent); + this.profilePath = profilePath; + } + + @Override + public void initGui() { + Keyboard.enableRepeatEvents(true); + buttonList.clear(); + int left = (width - FORM_WIDTH) / 2; + nameField = LegacyVersionBridge.textField(100, font(), left, FIELD_Y, FORM_WIDTH, 20); + nameField.setMaxStringLength(128); + // 親画面から渡された名前を初期値とし、空の場合はそのままにする。 + String initial = ""; + try { + ProfileManifest manifest = service().readManifest(profilePath); + initial = manifest.name == null ? "" : manifest.name; + } catch (Exception ignored) { + // マニフェスト読み込み失敗時は空欄から始め、ユーザーが入力できるようにする。 + } + nameField.setText(initial); + buttonList.add(createButton(1, width / 2 - 104, height - 32, 100, BUTTON_HEIGHT, + tr("screen.universal_config.rename"))); + buttonList.add(createButton(0, width / 2 + 4, height - 32, 100, BUTTON_HEIGHT, + tr("screen.universal_config.back"))); + } + + private void rename() { + String value = nameField.getText(); + if (value == null || value.trim().isEmpty()) { + status = tr("screen.universal_config.profile_name_required"); + return; + } + try { + service().renameProfile(profilePath, value); + back(); + } catch (Exception ex) { + fail("screen.universal_config.rename_failed", ex); + } + } + + @Override + public void updateScreen() { + nameField.updateCursorCounter(); + } + + @Override + protected void actionPerformed(GuiButton button) { + if (button.id == 0) back(); + if (button.id == 1) rename(); + } + + @Override + protected void keyTyped(char typedChar, int keyCode) { + if (keyCode == Keyboard.KEY_ESCAPE) { back(); return; } + nameField.textboxKeyTyped(typedChar, keyCode); + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) { + try { super.mouseClicked(mouseX, mouseY, mouseButton); } catch (Exception ex) { fail("screen.universal_config.action_failed", ex); } + nameField.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + drawDefaultBackground(); + drawCenteredString(font(), tr("screen.universal_config.profile_rename_title"), width / 2, 18, TEXT); + int left = (width - FORM_WIDTH) / 2; + drawString(font(), tr("screen.universal_config.profile_rename_label"), left, 44, MUTED); + nameField.drawTextBox(); + super.drawScreen(mouseX, mouseY, partialTicks); + drawStatus(); + } + } + private static final class ProfileCreate extends Base { private GuiTextField name; private GuiTextField description; private boolean keybinds = true; private boolean client = true; - private boolean mods = true; + private boolean mods = false; private int iconIndex; private static final String[] ICONS = {ProfileIcon.GRASS_BLOCK, ProfileIcon.CRAFTING_TABLE, ProfileIcon.BOOKSHELF, ProfileIcon.COBBLESTONE, ProfileIcon.TNT, ProfileIcon.CHEST, ProfileIcon.FURNACE, ProfileIcon.DIAMOND_BLOCK}; diff --git a/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/LegacyTitleButtonHandler.java b/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/LegacyTitleButtonHandler.java index 747439c..e8d802f 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/LegacyTitleButtonHandler.java +++ b/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/LegacyTitleButtonHandler.java @@ -7,20 +7,46 @@ import net.minecraft.util.ResourceLocation; import net.minecraftforge.client.event.GuiScreenEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.FMLCommonHandler; import org.lwjgl.opengl.GL11; /** Adds a responsive lower-left main-menu entry while keeping Forge's credit area unobstructed. */ public final class LegacyTitleButtonHandler { private static final int BUTTON_ID = 0x5543; + private static final int BUTTON_SIZE = 20; + private static final int BUTTON_MARGIN = 4; + // バニラの左下システム文字列(バージョン表記など)の描画位置を基準にする。 + // 1.12.2のGuiMainMenuは height-10 の位置に文字列を描画するため、その上に最小限の間隔を確保する。 + private static final int SYSTEM_TEXT_BOTTOM_OFFSET = 10; + private static final int SYSTEM_TEXT_GAP = 2; + // title_screen_button.png の実寸。画像を差し替える場合は描画APIへ渡す実寸・UV領域と + // この定数を必ず一致させる。ボタンの位置・サイズ・描画先サイズは変更しない。 + private static final int ICON_TEXTURE_WIDTH = 15; + private static final int ICON_TEXTURE_HEIGHT = 15; @SubscribeEvent public void onInitGui(GuiScreenEvent.InitGuiEvent.Post event) { if (event.getGui() instanceof GuiMainMenu) { - event.getButtonList().add(new TitleIconButton(BUTTON_ID, 4, - Math.max(4, event.getGui().height - 70))); + // 画面初期化のたびにイベントが呼ばれるため、既存ボタンを除去してから追加し直す。 + // これによりタイトル画面の再初期化でボタンが重複登録されるのを防ぐ(PR #43)。 + event.getButtonList().removeIf(button -> button instanceof TitleIconButton); + int y = titleButtonY(event.getGui().height); + event.getButtonList().add(new TitleIconButton(BUTTON_ID, BUTTON_MARGIN, y)); } } + // バニラの左下システム文字列の上にボタンを配置する。 + // 固定の高さオフセットではなく文字列の描画位置から計算することで、画面サイズに応じた配置になる。 + private static int titleButtonY(int screenHeight) { + int systemTextY = screenHeight - SYSTEM_TEXT_BOTTOM_OFFSET; + int brandingLines = FMLCommonHandler.instance().getBrandings(true).size(); + int brandingStep = Minecraft.getMinecraft().fontRenderer.FONT_HEIGHT + 1; + // Forge places branding line 0 at systemTextY and each additional line one step above it. Anchoring the + // button bottom to the top line's Y removes the artificial blank row without overlapping any glyphs. + int topBrandingY = systemTextY - Math.max(0, brandingLines - 1) * brandingStep; + return Math.max(BUTTON_MARGIN, topBrandingY - BUTTON_SIZE - SYSTEM_TEXT_GAP); + } + @SubscribeEvent public void onAction(GuiScreenEvent.ActionPerformedEvent.Pre event) { if (event.getGui() instanceof GuiMainMenu && event.getButton() instanceof TitleIconButton @@ -47,7 +73,8 @@ public void drawButton(Minecraft minecraft, int mouseX, int mouseY, float partia GL11.glEnable(GL11.GL_BLEND); GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); Gui.drawScaledCustomSizeModalRect(x + 2, y + 2, - 0.0F, 0.0F, 128, 128, 16, 16, 128.0F, 128.0F); + 0.0F, 0.0F, ICON_TEXTURE_WIDTH, ICON_TEXTURE_HEIGHT, 16, 16, + (float) ICON_TEXTURE_WIDTH, (float) ICON_TEXTURE_HEIGHT); GL11.glDisable(GL11.GL_BLEND); } } diff --git a/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/UniversalConfigLegacyMod.java b/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/UniversalConfigLegacyMod.java index b11c591..c2eaa67 100644 --- a/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/UniversalConfigLegacyMod.java +++ b/1.12.2/src/main/java/com/example/universalconfig/forgelegacy/UniversalConfigLegacyMod.java @@ -7,24 +7,17 @@ import com.example.universalconfig.core.UniversalConfigFormat; import com.example.universalconfig.core.UniversalConfigPaths; import com.example.universalconfig.core.UniversalConfigSettings; -import net.minecraft.client.Minecraft; -import net.minecraft.client.settings.KeyBinding; -import net.minecraftforge.fml.client.registry.ClientRegistry; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; -import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; -import net.minecraftforge.fml.common.gameevent.TickEvent; -import org.lwjgl.input.Keyboard; import java.nio.file.Path; -@Mod(modid = UniversalConfigLegacyMod.MOD_ID, name = "Universal Config", version = "1.0.0", +@Mod(modid = UniversalConfigLegacyMod.MOD_ID, name = "Universal Config", version = "1.1.0", acceptedMinecraftVersions = "[1.12.2]", clientSideOnly = true, guiFactory = "com.example.universalconfig.forgelegacy.UniversalConfigGuiFactory") public final class UniversalConfigLegacyMod { public static final String MOD_ID = UniversalConfigFormat.MOD_ID; - private static KeyBinding openKey; @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { @@ -56,9 +49,9 @@ public void preInit(FMLPreInitializationEvent event) { @Mod.EventHandler public void init(FMLInitializationEvent event) { - openKey = new KeyBinding("key.universal_config.open", Keyboard.KEY_U, "category.universal_config"); - ClientRegistry.registerKeyBinding(openKey); - net.minecraftforge.fml.common.FMLCommonHandler.instance().bus().register(this); + // 設定画面はタイトル画面のボタンから開けるため、キーバインドは登録しない(Issue #35)。 + // ゲーム中に頻繁に開く用途ではなく、未割り当てのキーがキー設定一覧に残ると混乱を招く。 + // 過去に登録していたキーが options.txt に残っていても、未登録の KeyBinding は無視される。 net.minecraftforge.common.MinecraftForge.EVENT_BUS.register(new LegacyTitleButtonHandler()); try { // Always normalize the loaded locale. A profile from a newer Minecraft version can leave a lowercase @@ -68,12 +61,4 @@ public void init(FMLInitializationEvent event) { FileOperationLogger.failure("STARTUP_RELOAD_OPTIONS", LegacyPlatform.gameDirectory(), "failed", ex); } } - - @SubscribeEvent - public void onClientTick(TickEvent.ClientTickEvent event) { - if (event.phase == TickEvent.Phase.END && openKey != null && openKey.isPressed()) { - Minecraft minecraft = Minecraft.getMinecraft(); - minecraft.displayGuiScreen(new LegacyScreens.ProfileList(minecraft.currentScreen)); - } - } } diff --git a/1.12.2/src/main/resources/assets/universal_config/icon.png b/1.12.2/src/main/resources/assets/universal_config/icon.png index 2ed8a92fec6b8755fe43bfe61abb7edfd2810058..d3f6dd6b91e71900963c72f53847683709432357 100644 GIT binary patch literal 3907 zcmc&%eNvow#TSg9~^OvxY)AZn$QAJa}~ltUa7VZ!e9<5V8W zovg%QZXw7x0nTV?!eBShW557$3d)2#KVXgt6E+y*?(TL+Ux#zFGVjrQ&i&_}o%i{@ z&-*;@@7ewp%cBDPCi?*Z0;1nqwh90r*!wcalVAKB>o@?g{GyjFeJ5SsdnNzihv%=t z*SRexVS(S)zRkkT1+ECTn&-U?us-k{c!uY*uR~K5l0-m0+DkA>G3vZWY z68+d>ZAJCB;p@%s@_00|@?&`PSBe-Qve!VLS04tBNf+?QGZ1TiCW7gazz3XVE&=c) z8WT+902=tfcRE;-4Sd1pfiM8C{=<*-3wdDbwQwF%XdevcMYQ*da)UPI?wq9ka?1ZJ zI2LttUJ!VeJacKNz-HheM~q$bc{Q_bD<(7q{MQqXEjYHOM@%h(2aX7;oT|ek;A$rO>DbEtw z$tj${%e^%1L8m7+smC=1ul||#9|n1klaH0eDGM#V{iP7T!1k4q-!WI!g0qor%A8){Ad`t~3v_-o=pOJSq*Qn2w4!rWj5tv&Ro%b>I4iN=C4nwY-6pD(M>?Sa zf1mR|i9PXoQX+O2g4r(Ef4(_@ruvo%ZNTXXFp z9R4@B(E)S-bASUq&B$%%Xvemd!4KkbDU7LhW05x*=(hGXbZkP-PzEWL8pt zwZ?6+5pL&2NoYAAcd28n5K)PqIGEztX=HroW`A#tn1FKlJ!AiYII_@(BRWpPoC{?D=}Q_B)c1N?MTA0oxw5IHH4=k zSQ#EGLrR6|1SzIok>t`;itP3~@PMWE;t1JkPOA=bmTRP!dr($(dl6$6H5+hoJPYVE zTtik%;9;%Qy_i>XsDL1L-xN_I5pQ*E#~L&w+tfs5|eO;)7fSC ztOeiArE*dvVGd@&2EKH|`-G>XVeib0yb&iuzn!Xv?vC1wTur9p4b`|rOpwEgYE$QW z2wEyNUT!wfW5Wct80)qiLk|<{VZ74*sdLLOqclde%lMf(jYp*v{Go`T+my{JMNIAq zF=6b|j09pgMua;+XJ3PgW#AN6N!?nF2d~Uw#QEGsDdQ5!wGUQnJZaprgD2HeQepA58-S*7LEWf2rsBLk$sy=qO`HLu9B>F`HTLDJiEiJfG zp*VGJ5Uao8-D3xgwQ|0@KiI&zH0LP4WHrhrSWU!yYH%0w5c-{$4>Y%71>K>AqtWSm z%a(MDiynErS!Qvf^iIA3Mf1;4k`ELrMrZ{IQbD*R?DYh!gVy5krYI#lwD_`p*JTGL zTR@owi^%+83G5jza z)%MgwR0Uf&5xdn*nn8pmk|+{i-O_rw;7Pfll(MVy5#`QDkf4!cSrYh}(J(V9wNhRb zmC!YyqKJ0a4-$pTGJQQY+5okA;nn|5ShVP@--FOc?|y@ ey^t`mp8%E_L+frFFSL3;M8COw*@=jadwv62nl3#6 literal 32658 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
    h&$kvWgx;dyg0LwhM6vbM&`ii5~}={w9ec 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 literal 24132 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 TSH$y=00000NkvXXu0mjfZS@u* diff --git a/1.12.2/src/main/resources/mcmod.info b/1.12.2/src/main/resources/mcmod.info index 5ba87e4..e2bf42a 100644 --- a/1.12.2/src/main/resources/mcmod.info +++ b/1.12.2/src/main/resources/mcmod.info @@ -3,7 +3,7 @@ "modid": "universal_config", "name": "Universal Config", "description": "Share Minecraft keybinds, client settings, and mod configuration profiles across Minecraft instances.", - "version": "1.0.0-mc1.12.2-forge", + "version": "1.1.0-mc1.12.2-forge", "mcversion": "1.12.2", "url": "https://modrinth.com/project/universal-config", "updateUrl": "", diff --git a/1.12.2/src/main/resources/universal_config.png b/1.12.2/src/main/resources/universal_config.png index 2ed8a92fec6b8755fe43bfe61abb7edfd2810058..d3f6dd6b91e71900963c72f53847683709432357 100644 GIT binary patch literal 3907 zcmc&%eNvow#TSg9~^OvxY)AZn$QAJa}~ltUa7VZ!e9<5V8W zovg%QZXw7x0nTV?!eBShW557$3d)2#KVXgt6E+y*?(TL+Ux#zFGVjrQ&i&_}o%i{@ z&-*;@@7ewp%cBDPCi?*Z0;1nqwh90r*!wcalVAKB>o@?g{GyjFeJ5SsdnNzihv%=t z*SRexVS(S)zRkkT1+ECTn&-U?us-k{c!uY*uR~K5l0-m0+DkA>G3vZWY z68+d>ZAJCB;p@%s@_00|@?&`PSBe-Qve!VLS04tBNf+?QGZ1TiCW7gazz3XVE&=c) z8WT+902=tfcRE;-4Sd1pfiM8C{=<*-3wdDbwQwF%XdevcMYQ*da)UPI?wq9ka?1ZJ zI2LttUJ!VeJacKNz-HheM~q$bc{Q_bD<(7q{MQqXEjYHOM@%h(2aX7;oT|ek;A$rO>DbEtw z$tj${%e^%1L8m7+smC=1ul||#9|n1klaH0eDGM#V{iP7T!1k4q-!WI!g0qor%A8){Ad`t~3v_-o=pOJSq*Qn2w4!rWj5tv&Ro%b>I4iN=C4nwY-6pD(M>?Sa zf1mR|i9PXoQX+O2g4r(Ef4(_@ruvo%ZNTXXFp z9R4@B(E)S-bASUq&B$%%Xvemd!4KkbDU7LhW05x*=(hGXbZkP-PzEWL8pt zwZ?6+5pL&2NoYAAcd28n5K)PqIGEztX=HroW`A#tn1FKlJ!AiYII_@(BRWpPoC{?D=}Q_B)c1N?MTA0oxw5IHH4=k zSQ#EGLrR6|1SzIok>t`;itP3~@PMWE;t1JkPOA=bmTRP!dr($(dl6$6H5+hoJPYVE zTtik%;9;%Qy_i>XsDL1L-xN_I5pQ*E#~L&w+tfs5|eO;)7fSC ztOeiArE*dvVGd@&2EKH|`-G>XVeib0yb&iuzn!Xv?vC1wTur9p4b`|rOpwEgYE$QW z2wEyNUT!wfW5Wct80)qiLk|<{VZ74*sdLLOqclde%lMf(jYp*v{Go`T+my{JMNIAq zF=6b|j09pgMua;+XJ3PgW#AN6N!?nF2d~Uw#QEGsDdQ5!wGUQnJZaprgD2HeQepA58-S*7LEWf2rsBLk$sy=qO`HLu9B>F`HTLDJiEiJfG zp*VGJ5Uao8-D3xgwQ|0@KiI&zH0LP4WHrhrSWU!yYH%0w5c-{$4>Y%71>K>AqtWSm z%a(MDiynErS!Qvf^iIA3Mf1;4k`ELrMrZ{IQbD*R?DYh!gVy5krYI#lwD_`p*JTGL zTR@owi^%+83G5jza z)%MgwR0Uf&5xdn*nn8pmk|+{i-O_rw;7Pfll(MVy5#`QDkf4!cSrYh}(J(V9wNhRb zmC!YyqKJ0a4-$pTGJQQY+5okA;nn|5ShVP@--FOc?|y@ ey^t`mp8%E_L+frFFSL3;M8COw*@=jadwv62nl3#6 literal 32658 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
      h&$kvWgx;dyg0LwhM6vbM&`ii5~}={w9ec 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 + scheduleRestartAfterCurrentProcessExit(currentWorkingDirectory(), currentProcessArguments()); + } + + /** + * ゲームプロセスが公開している起動引数を、ローダーが解決した引数の代替として返す。 + * Java 9 以降の ProcessHandle API が使える環境では OS が引数境界を保持したまま公開し、 + * Java 8 環境では ProcessDiscovery が OS メタデータから読み取る。引数は再起動先へそのまま渡すため、 + * ローダー独自の引数解決がなくても安全に現在の Java 起動を再現できる。 + */ + public static List currentProcessArguments() { + try { + List arguments = ProcessDiscovery.current().arguments(); + return arguments == null ? Collections.emptyList() : arguments; + } catch (RuntimeException ignored) { + return Collections.emptyList(); + } } /** @@ -38,7 +53,7 @@ public static void scheduleRestartAfterCurrentProcessExit() throws UniversalConf */ public static void scheduleRestartAfterCurrentProcessExit(Path workingDirectory) throws UniversalConfigException { - scheduleRestartAfterCurrentProcessExit(workingDirectory, Collections.emptyList()); + scheduleRestartAfterCurrentProcessExit(workingDirectory, currentProcessArguments()); } /** @@ -52,12 +67,12 @@ public static void scheduleRestartAfterCurrentProcessExit( Path helperClasspath ) throws UniversalConfigException { scheduleRestartAfterCurrentProcessExit( - workingDirectory, Collections.emptyList(), helperClasspath); + workingDirectory, currentProcessArguments(), helperClasspath); } /** - * Schedules the current Minecraft process to be replaced, using loader-provided arguments when the operating - * system does not expose the current process arguments through its process metadata API. + * Schedules the current Minecraft process to be replaced. Unsupported launchers use the loader-resolved Java + * arguments supplied by the loader adapter instead of reconstructing a command line from process metadata. */ public static void scheduleRestartAfterCurrentProcessExit( Path workingDirectory, @@ -66,7 +81,7 @@ public static void scheduleRestartAfterCurrentProcessExit( scheduleRestartAfterCurrentProcessExit(workingDirectory, loaderResolvedArguments, null); } - private static void scheduleRestartAfterCurrentProcessExit( + public static void scheduleRestartAfterCurrentProcessExit( Path workingDirectory, List loaderResolvedArguments, Path helperClasspath @@ -78,7 +93,6 @@ private static void scheduleRestartAfterCurrentProcessExit( } Path normalizedWorkingDirectory = normalizeWorkingDirectory(workingDirectory); List ancestors = ancestorCommands(current); - Optional> currentArguments = currentJavaArguments(current.arguments(), loaderResolvedArguments); // Modrinth's documented launch URL requires a database-only internal ID that is not inherited by the game. // Guessing it from the folder name could launch the wrong profile, so only self-identifying launchers are used. Optional replacementCandidate = prismFamilyLauncherCommand( @@ -86,13 +100,9 @@ private static void scheduleRestartAfterCurrentProcessExit( if (!replacementCandidate.isPresent()) { replacementCandidate = atLauncherCommand(normalizedWorkingDirectory, ancestors); } - if (!replacementCandidate.isPresent()) { - replacementCandidate = gdLauncherCommand( - normalizedWorkingDirectory, ancestors, executable, currentArguments); - } LaunchCommand replacement = replacementCandidate.isPresent() ? replacementCandidate.get() - : currentArguments.isPresent() ? new LaunchCommand(executable, currentArguments.get()) : null; + : unsupportedLauncherCommand(executable, loaderResolvedArguments).orElse(null); if (replacement == null) { throw new UniversalConfigException("Could not determine how to restart this launcher instance."); } @@ -125,21 +135,75 @@ public static List buildJavaLaunchArguments( } } - private static Optional> currentJavaArguments( - List processArguments, + /** + * Builds the default restart command for a launcher without a dedicated integration. + * + *

      The launcher is intentionally not inspected here. Every launcher that is not recognized by a dedicated + * detector follows this path, including GDLauncher and the official launcher.

      + */ + static Optional unsupportedLauncherCommand( + String currentExecutable, List loaderResolvedArguments - ) throws UniversalConfigException { + ) { + if (!isJavaExecutable(currentExecutable)) { + return Optional.empty(); + } + return validJavaLaunchArguments(loaderResolvedArguments) + .map(new java.util.function.Function, LaunchCommand>() { + @Override + public LaunchCommand apply(List arguments) { + return new LaunchCommand(currentExecutable, arguments); + } + }); + } + + private static Optional> validJavaLaunchArguments(List arguments) { + if (arguments == null || arguments.isEmpty()) { + return Optional.empty(); + } try { - if (processArguments != null && !processArguments.isEmpty()) { - return Optional.of(immutableCopy(processArguments)); + List copied = immutableCopy(arguments); + for (String value : copied) { + if (value == null || value.trim().isEmpty()) return Optional.empty(); } - if (loaderResolvedArguments == null || loaderResolvedArguments.isEmpty()) { - return Optional.empty(); + for (int index = 0; index + 2 < copied.size(); index++) { + if (("-cp".equals(copied.get(index)) || "-classpath".equals(copied.get(index))) + && !copied.get(index + 1).trim().isEmpty()) { + return hasMainClassAfter(copied, index + 2) ? Optional.of(copied) : Optional.empty(); + } } - return Optional.of(immutableCopy(loaderResolvedArguments)); + return Optional.empty(); } catch (RuntimeException ex) { - throw new UniversalConfigException("Could not determine the current Java arguments.", ex); + return Optional.empty(); + } + } + + private static boolean hasMainClassAfter(List arguments, int startIndex) { + int index = startIndex; + while (index < arguments.size()) { + String value = arguments.get(index); + if (!value.startsWith("-")) return true; + if (isInlineJvmOption(value)) { + index++; + } else if (isJvmOptionWithSeparateValue(value) && index + 1 < arguments.size()) { + index += 2; + } else { + return false; + } } + return false; + } + + private static boolean isInlineJvmOption(String value) { + return value.startsWith("-D") || value.startsWith("-X") + || (value.startsWith("--") && value.indexOf('=') > 2); + } + + private static boolean isJvmOptionWithSeparateValue(String value) { + return "--add-modules".equals(value) || "--add-exports".equals(value) + || "--add-opens".equals(value) || "--add-reads".equals(value) + || "--limit-modules".equals(value) || "-p".equals(value) || "--module-path".equals(value) + || "--upgrade-module-path".equals(value) || "--patch-module".equals(value); } private static void scheduleJavaHelper( @@ -348,42 +412,6 @@ static Optional atLauncherCommand( return Optional.empty(); } - static Optional gdLauncherCommand( - Path workingDirectory, - List ancestorCommands, - String currentExecutable, - Optional> currentArguments - ) { - if (!isGdLauncherGameDirectory(workingDirectory) - || !ancestorCommands.stream().anyMatch(command -> isGdLauncherExecutable(command.executable())) - || !ancestorCommands.stream().anyMatch(command -> isGdCoreModuleExecutable(command.executable())) - || !isJavaExecutable(currentExecutable)) { - return Optional.empty(); - } - - // Carbon does not provide a stable command-line launch interface for an instance. The core module supervises - // the Java child, so reusing the already resolved Java command is the only way to preserve the selected - // loader, natives, assets, account, and instance-specific arguments without guessing launcher internals. - return currentArguments.map(arguments -> new LaunchCommand(currentExecutable, arguments)); - } - - private static boolean isGdLauncherGameDirectory(Path workingDirectory) { - try { - Path gameDirectory = workingDirectory.toAbsolutePath().normalize(); - Path instanceDirectory = gameDirectory.getParent(); - Path instancesDirectory = instanceDirectory == null ? null : instanceDirectory.getParent(); - return gameDirectory.getFileName() != null - && gameDirectory.getFileName().toString().equalsIgnoreCase("instance") - && instanceDirectory != null - && Files.isRegularFile(instanceDirectory.resolve("instance.json")) - && instancesDirectory != null - && instancesDirectory.getFileName() != null - && instancesDirectory.getFileName().toString().equalsIgnoreCase("instances"); - } catch (RuntimeException ex) { - return false; - } - } - private static Optional> atLauncherJarArguments( List ancestorArguments, List launchArguments @@ -575,16 +603,6 @@ private static boolean isAtLauncherExecutable(String command) { return fileName.equalsIgnoreCase("atlauncher.exe") || fileName.equalsIgnoreCase("atlauncher"); } - private static boolean isGdLauncherExecutable(String command) { - String fileName = fileName(command); - return fileName.equalsIgnoreCase("gdlauncher.exe") || fileName.equalsIgnoreCase("gdlauncher"); - } - - private static boolean isGdCoreModuleExecutable(String command) { - String fileName = fileName(command); - return fileName.equalsIgnoreCase("core_module.exe") || fileName.equalsIgnoreCase("core_module"); - } - private static boolean isJavaExecutable(String command) { String fileName = fileName(command); return fileName.equalsIgnoreCase("java.exe") diff --git a/1.16.5/src/main/java/com/example/universalconfig/core/FileOperationLogger.java b/1.16.5/src/main/java/com/example/universalconfig/core/FileOperationLogger.java index 8db6802..9be027b 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/core/FileOperationLogger.java +++ b/1.16.5/src/main/java/com/example/universalconfig/core/FileOperationLogger.java @@ -7,14 +7,29 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.regex.Pattern; public final class FileOperationLogger { + private static final Pattern ABSOLUTE_WINDOWS_PATH = Pattern.compile( + "(?i)(?])/(?:[^\\s\\t,;:/]+/)*[^\\s\\t,;:/]+" + ); private static final String LAUNCH_ID = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss-SSS") .format(java.time.LocalDateTime.now()) + "-pid" + currentProcessId(); private static Path configuredRoot; + private static Path instanceRoot; + private static Path userHomeRoot; + private static Path appDataRoot; private static Path launchLogFile; private static Path latestLogFile; @@ -22,11 +37,23 @@ private FileOperationLogger() { } public static synchronized void configure(UniversalConfigSettings settings) { + configure(settings, null); + } + + public static synchronized void configure(UniversalConfigSettings settings, Path minecraftRunDirectory) { Path root = settings.rootDirectory().toAbsolutePath().normalize(); - if (root.equals(configuredRoot) && launchLogFile != null && latestLogFile != null) { + Path instance = normalizeOrNull(minecraftRunDirectory); + if (root.equals(configuredRoot) + && equalsPath(instance, instanceRoot) + && launchLogFile != null + && latestLogFile != null) { return; } configuredRoot = root; + instanceRoot = instance; + userHomeRoot = normalizeOrNull(Paths.get(System.getProperty("user.home", "."))); + String appData = System.getenv("APPDATA"); + appDataRoot = appData == null || appData.trim().isEmpty() ? null : normalizeOrNull(Paths.get(appData)); Path logsRoot = root.resolve(UniversalConfigFormat.LOGS_DIRECTORY_NAME); launchLogFile = logsRoot.resolve(UniversalConfigFormat.LAUNCH_LOGS_DIRECTORY_NAME) .resolve(UniversalConfigFormat.LAUNCH_LOG_FILE_PREFIX + LAUNCH_ID + ".log"); @@ -70,12 +97,12 @@ private static synchronized void write(String level, String operation, Path path .append(LAUNCH_ID).append('\t') .append(level).append('\t') .append(operation).append('\t') - .append(path == null ? "-" : path.toAbsolutePath().normalize()).append('\t') - .append(detail == null ? "" : detail.replace('\n', ' ').replace('\r', ' ')); + .append(sanitizePath(path)).append('\t') + .append(sanitizeText(detail)); if (throwable != null) { StringWriter writer = new StringWriter(); throwable.printStackTrace(new PrintWriter(writer)); - line.append('\t').append(writer.toString().replace('\n', ' ').replace('\r', ' ')); + line.append('\t').append(sanitizeText(writer.toString())); } line.append(System.lineSeparator()); byte[] encoded = line.toString().getBytes(StandardCharsets.UTF_8); @@ -88,6 +115,112 @@ private static synchronized void write(String level, String operation, Path path } } + static synchronized String sanitizePath(Path path) { + if (path == null) { + return "-"; + } + Path normalized = normalizeOrNull(path); + if (normalized == null) { + return ""; + } + List labels = new ArrayList(); + labels.add(new PathLabel(instanceRoot, "")); + labels.add(new PathLabel(configuredRoot, "")); + labels.add(new PathLabel(appDataRoot, "")); + labels.add(new PathLabel(userHomeRoot, "")); + List filtered = new ArrayList(); + for (PathLabel label : labels) { + if (label.root() != null) { + filtered.add(label); + } + } + // 深いパスほど先に一致させる。ファイル名キーは相対パス全体を含むため、 + // ルートが他のルートの接頭辞になる場合でもより具体的なラベルが優先される。 + Collections.sort(filtered, new Comparator() { + @Override + public int compare(PathLabel left, PathLabel right) { + return Integer.compare(right.root().getNameCount(), left.root().getNameCount()); + } + }); + for (PathLabel label : filtered) { + if (normalized.startsWith(label.root())) { + Path relative = label.root().relativize(normalized); + return relative.getNameCount() == 0 + ? label.label() + : label.label() + "/" + relative.toString().replace('\\', '/'); + } + } + Path fileName = normalized.getFileName(); + return fileName == null ? "" : "/" + fileName; + } + + static synchronized String sanitizeText(String value) { + if (value == null || value.trim().isEmpty()) { + return value == null ? "" : value; + } + String sanitized = value.replace('\n', ' ').replace('\r', ' '); + List labels = new ArrayList(); + labels.add(new PathLabel(instanceRoot, "")); + labels.add(new PathLabel(configuredRoot, "")); + labels.add(new PathLabel(appDataRoot, "")); + labels.add(new PathLabel(userHomeRoot, "")); + List filtered = new ArrayList(); + for (PathLabel label : labels) { + if (label.root() != null) { + filtered.add(label); + } + } + // 長いルート文字列ほど先に置換する。短いルートが先だと、共通接頭辞を持つ深いパスの + // 一部だけが置換され、残った相対部分が絶対パス正規表現に誤判定される可能性がある。 + Collections.sort(filtered, new Comparator() { + @Override + public int compare(PathLabel left, PathLabel right) { + return Integer.compare(right.root().toString().length(), left.root().toString().length()); + } + }); + for (PathLabel label : filtered) { + String nativeRoot = label.root().toString(); + sanitized = sanitized.replace(nativeRoot, label.label()) + .replace(nativeRoot.replace('\\', '/'), label.label()); + } + return ABSOLUTE_UNIX_PATH.matcher( + ABSOLUTE_WINDOWS_PATH.matcher(sanitized).replaceAll("") + ).replaceAll("").replace('\\', '/'); + } + + private static Path normalizeOrNull(Path path) { + if (path == null) { + return null; + } + try { + return path.toAbsolutePath().normalize(); + } catch (RuntimeException ex) { + return null; + } + } + + private static boolean equalsPath(Path left, Path right) { + return left == null ? right == null : left.equals(right); + } + + static final class PathLabel { + private final Path root; + private final String label; + + PathLabel(Path root, String label) { + this.root = root; + this.label = label; + } + + Path root() { + return root; + } + + String label() { + return label; + } + } + private static long currentProcessId() { String runtimeName = ManagementFactory.getRuntimeMXBean().getName(); int separator = runtimeName.indexOf('@'); diff --git a/1.16.5/src/main/java/com/example/universalconfig/core/ProfileCreateOptions.java b/1.16.5/src/main/java/com/example/universalconfig/core/ProfileCreateOptions.java index ffa7fc9..591bb34 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/core/ProfileCreateOptions.java +++ b/1.16.5/src/main/java/com/example/universalconfig/core/ProfileCreateOptions.java @@ -7,6 +7,7 @@ public final class ProfileCreateOptions { public String name; public String description; public String icon = ProfileIcon.GRASS_BLOCK; + // 画面の初期選択はUI側で管理する。非UIの呼び出し元では従来どおり全項目を保存対象にする。 public boolean includeKeybinds = true; public boolean includeClientOptions = true; public boolean includeModConfigs = true; diff --git a/1.16.5/src/main/java/com/example/universalconfig/core/ProfileService.java b/1.16.5/src/main/java/com/example/universalconfig/core/ProfileService.java index a26512c..e786749 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/core/ProfileService.java +++ b/1.16.5/src/main/java/com/example/universalconfig/core/ProfileService.java @@ -19,6 +19,8 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -172,14 +174,145 @@ public List listProfiles() throws UniversalConfigException { } } summaries.sort(Comparator.comparing((ProfileSummary summary) -> safeUpdatedAt(summary.manifest())).reversed()); - FileOperationLogger.info("LIST_PROFILES", profiles, "count=" + summaries.size()); - return summaries; + List ordered = applyProfileOrder(summaries); + FileOperationLogger.info("LIST_PROFILES", profiles, "count=" + ordered.size()); + return ordered; } catch (IOException ex) { FileOperationLogger.failure("LIST_PROFILES", profiles, "failed", ex); throw new UniversalConfigException("Failed to list profiles.", ex); } } + public void renameProfile(Path profilePath, String name) throws UniversalConfigException { + Path normalized = validateProfilePath(profilePath); + String requestedName = name == null ? "" : name.trim(); + if (requestedName.trim().isEmpty()) { + throw new UniversalConfigException("Profile name is required."); + } + if (requestedName.length() > 128) { + throw new UniversalConfigException("Profile name is too long."); + } + + try (ProfileDirectoryLock ignored = lockProfileDirectory()) { + ProfileManifest manifest = readManifest(normalized); + String uniqueName = uniqueProfileName(requestedName, normalized); + if (Objects.equals(manifest.name, uniqueName)) { + return; + } + rewriteProfileName(normalized, uniqueName); + FileOperationLogger.info("RENAME_PROFILE", normalized, + "from=" + manifest.name + " to=" + uniqueName); + } + } + + public void moveProfile(Path instancePath, Path profilePath, int targetIndex) throws UniversalConfigException { + Path normalized = validateProfilePath(profilePath); + try (ProfileDirectoryLock ignored = lockProfileDirectory()) { + List summaries = listProfiles(); + int currentIndex = indexOfProfile(summaries, normalized); + if (currentIndex < 0) { + throw new UniversalConfigException("The selected profile is unavailable."); + } + if (targetIndex < 0 || targetIndex >= summaries.size()) { + throw new UniversalConfigException("The selected profile position is invalid."); + } + if (currentIndex == targetIndex) { + return; + } + + List order = new ArrayList(); + for (ProfileSummary summary : summaries) { + String key = profileOrderKey(summary); + if (key != null) { + order.add(key); + } + } + String movedKey = profileOrderKey(summaries.get(currentIndex)); + order.remove(movedKey); + order.add(targetIndex, movedKey); + settings.setProfileOrder(order); + UniversalConfigPaths.saveSettings(instancePath, settings); + FileOperationLogger.info("MOVE_PROFILE", normalized, "targetIndex=" + targetIndex); + } + } + + private List applyProfileOrder(List summaries) { + List configuredOrder = settings.profileOrder(); + if (configuredOrder.isEmpty()) { + return summaries; + } + + Map byKey = new HashMap(); + for (ProfileSummary summary : summaries) { + String key = profileOrderKey(summary); + if (key != null) { + byKey.putIfAbsent(key, summary); + } + } + + // 過去バージョンで id ベースのキーを保存済みの設定との互換性のため、id も補助インデックスとして残す。 + Map byLegacyIdKey = new HashMap(); + for (ProfileSummary summary : summaries) { + String legacyIdKey = legacyProfileOrderKey(summary); + if (legacyIdKey != null) { + byLegacyIdKey.putIfAbsent(legacyIdKey, summary); + } + } + + List ordered = new ArrayList(summaries.size()); + Set included = new HashSet(); + for (String key : configuredOrder) { + // ファイル名キーを優先し、無ければ旧 id キーで解決する。 + ProfileSummary summary = byKey.get(key); + if (summary == null) { + summary = byLegacyIdKey.get(key); + } + if (summary != null) { + String fileKey = profileOrderKey(summary); + if (fileKey != null && included.add(fileKey)) { + ordered.add(summary); + } + } + } + for (ProfileSummary summary : summaries) { + String key = profileOrderKey(summary); + if (key == null || included.add(key)) { + ordered.add(summary); + } + } + return ordered; + } + + private int indexOfProfile(List summaries, Path profilePath) { + for (int index = 0; index < summaries.size(); index++) { + if (summaries.get(index).path().toAbsolutePath().normalize().equals(profilePath)) { + return index; + } + } + return -1; + } + + private String profileOrderKey(ProfileSummary summary) { + if (summary == null) { + return null; + } + // ファイル名を並び順キーに使う。manifest.id は複製が元 id を保持するため一意性が保証されず、 + // id ベースの重複排除で複製が一覧から消える問題がある(PR #43 P1)。 + // rename や duplicate でもファイル名は変化しないため、ファイル名は安定した識別子になる。 + Path fileName = summary.path() == null ? null : summary.path().getFileName(); + return fileName == null ? null : "file:" + fileName; + } + + // 過去バージョンで settings.profileOrder に id ベースキーを保存していた設定との互換用。 + // 新規保存では使わない。applyProfileOrder のフォールバック解決でのみ参照する。 + private String legacyProfileOrderKey(ProfileSummary summary) { + if (summary == null || summary.manifest() == null) { + return null; + } + String id = summary.manifest().id; + return id == null || id.trim().isEmpty() ? null : "id:" + id; + } + public List listBackups() throws UniversalConfigException { Path backups = UniversalConfigPaths.backupsDirectory(settings); FileOperationLogger.info("LIST_BACKUPS", backups, "start"); diff --git a/1.16.5/src/main/java/com/example/universalconfig/core/UniversalConfigPaths.java b/1.16.5/src/main/java/com/example/universalconfig/core/UniversalConfigPaths.java index ef2767a..a76b4a4 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/core/UniversalConfigPaths.java +++ b/1.16.5/src/main/java/com/example/universalconfig/core/UniversalConfigPaths.java @@ -12,6 +12,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Locale; +import java.util.List; public final class UniversalConfigPaths { private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); @@ -40,14 +41,16 @@ public static UniversalConfigSettings loadOrCreateSettings(Path minecraftRunDire UniversalConfigSettings settings = new UniversalConfigSettings(Paths.get(dto.rootDirectory)); boolean invalidDefaultProfilePath = hasInvalidDefaultProfilePath(dto.defaultProfilePath); settings.setDefaultProfilePath(parseDefaultProfilePath(dto.defaultProfilePath)); + settings.setProfileOrder(dto.profileOrder); ensureDirectories(settings); - FileOperationLogger.configure(settings); + FileOperationLogger.configure(settings, minecraftRunDirectory); Path sharedSettingsFile = rootSettingsFile(settings); if (Files.isRegularFile(sharedSettingsFile)) { SettingsDto sharedDto = readSettingsDto(sharedSettingsFile); invalidDefaultProfilePath = hasInvalidDefaultProfilePath(sharedDto == null ? null : sharedDto.defaultProfilePath); settings.setDefaultProfilePath(parseDefaultProfilePath( sharedDto == null ? null : sharedDto.defaultProfilePath)); + settings.setProfileOrder(sharedDto == null ? null : sharedDto.profileOrder); } else { // Older versions stored the default only inside one instance. Migrate it to the shared root. saveRootSettings(settings); @@ -65,12 +68,13 @@ public static UniversalConfigSettings loadOrCreateSettings(Path minecraftRunDire UniversalConfigSettings settings = new UniversalConfigSettings(defaultRootDirectory()); ensureDirectories(settings); - FileOperationLogger.configure(settings); + FileOperationLogger.configure(settings, minecraftRunDirectory); Path sharedSettingsFile = rootSettingsFile(settings); if (Files.isRegularFile(sharedSettingsFile)) { SettingsDto sharedDto = readSettingsDto(sharedSettingsFile); settings.setDefaultProfilePath(parseDefaultProfilePath( sharedDto == null ? null : sharedDto.defaultProfilePath)); + settings.setProfileOrder(sharedDto == null ? null : sharedDto.profileOrder); } saveSettings(minecraftRunDirectory, settings); return settings; @@ -85,6 +89,7 @@ public static void saveSettings(Path minecraftRunDirectory, UniversalConfigSetti dto.defaultProfilePath = settings.defaultProfilePath() == null ? null : settings.defaultProfilePath().toAbsolutePath().normalize().toString(); + dto.profileOrder = settings.profileOrder(); try (Writer writer = Files.newBufferedWriter(localSettings, StandardCharsets.UTF_8)) { GSON.toJson(dto, writer); } @@ -160,6 +165,7 @@ public static String safeFileSlug(String value) { private static final class SettingsDto { String rootDirectory; String defaultProfilePath; + List profileOrder; } private static SettingsDto readSettingsDto(Path path) throws UniversalConfigException { @@ -179,6 +185,7 @@ private static void saveRootSettings(UniversalConfigSettings settings) throws Un dto.defaultProfilePath = settings.defaultProfilePath() == null ? null : settings.defaultProfilePath().toAbsolutePath().normalize().toString(); + dto.profileOrder = settings.profileOrder(); try (Writer writer = Files.newBufferedWriter(rootSettings, StandardCharsets.UTF_8)) { GSON.toJson(dto, writer); } diff --git a/1.16.5/src/main/java/com/example/universalconfig/core/UniversalConfigSettings.java b/1.16.5/src/main/java/com/example/universalconfig/core/UniversalConfigSettings.java index 174087c..0e94647 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/core/UniversalConfigSettings.java +++ b/1.16.5/src/main/java/com/example/universalconfig/core/UniversalConfigSettings.java @@ -1,10 +1,14 @@ package com.example.universalconfig.core; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; public final class UniversalConfigSettings { private Path rootDirectory; private Path defaultProfilePath; + private List profileOrder = new ArrayList(); public UniversalConfigSettings(Path rootDirectory) { this.rootDirectory = rootDirectory; @@ -25,4 +29,20 @@ public Path defaultProfilePath() { public void setDefaultProfilePath(Path defaultProfilePath) { this.defaultProfilePath = defaultProfilePath; } + + public List profileOrder() { + return Collections.unmodifiableList(profileOrder); + } + + public void setProfileOrder(List profileOrder) { + this.profileOrder = new ArrayList(); + if (profileOrder == null) { + return; + } + for (String profileKey : profileOrder) { + if (profileKey != null && !profileKey.trim().isEmpty() && !this.profileOrder.contains(profileKey)) { + this.profileOrder.add(profileKey); + } + } + } } diff --git a/1.16.5/src/main/java/com/example/universalconfig/core/ZipArchiveReader.java b/1.16.5/src/main/java/com/example/universalconfig/core/ZipArchiveReader.java index 2f39b94..d1d917d 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/core/ZipArchiveReader.java +++ b/1.16.5/src/main/java/com/example/universalconfig/core/ZipArchiveReader.java @@ -16,11 +16,25 @@ public final class ZipArchiveReader implements ProfileArchiveReader { public ZipArchiveReader(Path archivePath) throws IOException, UniversalConfigException { this.archivePath = archivePath; this.zipFile = new ZipFile(archivePath.toFile()); - FileOperationLogger.info("OPEN_ZIP", archivePath, "read"); - Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - ZipSecurity.validateRelativeEntryName(entry.getName()); + try { + FileOperationLogger.info("OPEN_ZIP", archivePath, "read"); + long totalUncompressedSize = 0; + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + ZipSecurity.validateRelativeEntryName(entry.getName()); + if (!entry.isDirectory()) { + totalUncompressedSize = ZipSecurity.validateEntrySizes( + entry.getName(), entry.getSize(), entry.getCompressedSize(), totalUncompressedSize); + } + } + } catch (UniversalConfigException | RuntimeException ex) { + try { + zipFile.close(); + } catch (IOException closeException) { + ex.addSuppressed(closeException); + } + throw ex; } } @@ -43,8 +57,14 @@ public InputStream open(String entryName) throws IOException, UniversalConfigExc @Override public List entries() { - List names = new ArrayList<>(); - zipFile.stream().filter(entry -> !entry.isDirectory()).forEach(entry -> names.add(entry.getName())); + List names = new ArrayList(); + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + if (!entry.isDirectory()) { + names.add(entry.getName()); + } + } return names; } diff --git a/1.16.5/src/main/java/com/example/universalconfig/core/ZipArchiveWriter.java b/1.16.5/src/main/java/com/example/universalconfig/core/ZipArchiveWriter.java index ac9c235..2cb34dd 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/core/ZipArchiveWriter.java +++ b/1.16.5/src/main/java/com/example/universalconfig/core/ZipArchiveWriter.java @@ -18,8 +18,11 @@ public static void write(Path path, Map entries) throws IOExcept Files.createDirectories(path.getParent()); FileOperationLogger.info("CREATE_DIRECTORY", path.getParent(), "zip parent"); try (ZipOutputStream output = new ZipOutputStream(Files.newOutputStream(path))) { + long totalUncompressedSize = 0; for (Map.Entry entry : entries.entrySet()) { ZipSecurity.validateRelativeEntryName(entry.getKey()); + totalUncompressedSize = ZipSecurity.validateUncompressedSize( + entry.getKey(), entry.getValue().length, totalUncompressedSize); output.putNextEntry(new ZipEntry(entry.getKey())); output.write(entry.getValue()); output.closeEntry(); diff --git a/1.16.5/src/main/java/com/example/universalconfig/core/ZipSecurity.java b/1.16.5/src/main/java/com/example/universalconfig/core/ZipSecurity.java index 5aad6d6..c6d9282 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/core/ZipSecurity.java +++ b/1.16.5/src/main/java/com/example/universalconfig/core/ZipSecurity.java @@ -4,6 +4,15 @@ import java.nio.file.Paths; public final class ZipSecurity { + /** Maximum uncompressed size accepted for one profile or backup entry. */ + public static final long MAX_ENTRY_UNCOMPRESSED_BYTES = 32L * 1024L * 1024L; + + /** Maximum combined uncompressed size accepted for one archive. */ + public static final long MAX_TOTAL_UNCOMPRESSED_BYTES = 256L * 1024L * 1024L; + + /** Maximum declared expansion ratio accepted for a non-empty entry. */ + public static final long MAX_COMPRESSION_RATIO = 1_000L; + private ZipSecurity() { } @@ -44,4 +53,42 @@ public static Path safeResolve(Path destinationRoot, String entryName) throws Un } return resolved; } + + /** + * Validates central-directory size metadata before any archive payload is read. + * The returned value is the accumulated uncompressed size for the archive. + */ + public static long validateEntrySizes( + String entryName, + long uncompressedSize, + long compressedSize, + long currentTotal + ) throws UniversalConfigException { + if (uncompressedSize < 0 || compressedSize < 0) { + throw new UniversalConfigException("ZIP entry size metadata is missing: " + entryName); + } + long total = validateUncompressedSize(entryName, uncompressedSize, currentTotal); + if (uncompressedSize > 0 + && (compressedSize == 0 || uncompressedSize / compressedSize > MAX_COMPRESSION_RATIO)) { + throw new UniversalConfigException("ZIP entry compression ratio is unsafe: " + entryName); + } + return total; + } + + public static long validateUncompressedSize( + String entryName, + long uncompressedSize, + long currentTotal + ) throws UniversalConfigException { + if (uncompressedSize < 0) { + throw new UniversalConfigException("ZIP entry size is invalid: " + entryName); + } + if (uncompressedSize > MAX_ENTRY_UNCOMPRESSED_BYTES) { + throw new UniversalConfigException("ZIP entry exceeds the uncompressed size limit: " + entryName); + } + if (currentTotal < 0 || currentTotal > MAX_TOTAL_UNCOMPRESSED_BYTES - uncompressedSize) { + throw new UniversalConfigException("ZIP archive exceeds the total uncompressed size limit."); + } + return currentTotal + uncompressedSize; + } } diff --git a/1.16.5/src/main/java/com/example/universalconfig/forge/ForgeRestartService.java b/1.16.5/src/main/java/com/example/universalconfig/forge/ForgeRestartService.java index a0c1aca..682e4c0 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/forge/ForgeRestartService.java +++ b/1.16.5/src/main/java/com/example/universalconfig/forge/ForgeRestartService.java @@ -2,11 +2,10 @@ import com.example.universalconfig.core.CurrentProcessRestartService; import com.example.universalconfig.core.UniversalConfigException; -import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.loading.FMLPaths; -import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; +import net.minecraftforge.fml.ModList; -import java.nio.file.Path; +import java.util.List; public final class ForgeRestartService { private ForgeRestartService() { @@ -14,13 +13,15 @@ private ForgeRestartService() { public static void scheduleRestartAfterCurrentProcessExit() throws UniversalConfigException { try { - ModFileInfo modFileInfo = ModList.get().getModFileById(UniversalConfigMod.MOD_ID); - if (modFileInfo == null || modFileInfo.getFile() == null) { - throw new UniversalConfigException("Could not locate the Universal Config Forge mod file."); - } - Path modFile = modFileInfo.getFile().getFilePath(); + // Forge does not expose FabricLoader-style launch arguments. Reuse the already resolved current Java + // arguments, as the v1.0 Forge builds did, and let the shared service validate the launch shape. + List javaArguments = CurrentProcessRestartService.currentProcessArguments(); + // ModLauncherのunion CodeSourceは独立Javaプロセスのclasspathとして使えない。 + // Forgeが検出済みの物理ModFileを渡し、RestartHelperを配布Jarから確実に起動する。 + java.nio.file.Path helperClasspath = ModList.get().getModFileById("universal_config") + .getFile().getFilePath(); CurrentProcessRestartService.scheduleRestartAfterCurrentProcessExit( - FMLPaths.GAMEDIR.get(), modFile); + FMLPaths.GAMEDIR.get(), javaArguments, helperClasspath); } catch (UniversalConfigException ex) { throw ex; } catch (RuntimeException ex) { diff --git a/1.16.5/src/main/java/com/example/universalconfig/forge/UniversalConfigMod.java b/1.16.5/src/main/java/com/example/universalconfig/forge/UniversalConfigMod.java index a20234b..fee95d1 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/forge/UniversalConfigMod.java +++ b/1.16.5/src/main/java/com/example/universalconfig/forge/UniversalConfigMod.java @@ -8,11 +8,8 @@ import com.example.universalconfig.core.UniversalConfigPaths; import com.example.universalconfig.core.UniversalConfigSettings; import com.example.universalconfig.forge.screen.ProfileListScreen; -import net.minecraft.client.util.InputMappings; -import net.minecraft.client.settings.KeyBinding; import net.minecraft.client.Minecraft; import com.mojang.blaze3d.matrix.MatrixStack; -import com.mojang.blaze3d.systems.RenderSystem; import net.minecraft.client.gui.widget.button.Button; import net.minecraft.client.gui.AbstractGui; import net.minecraft.client.gui.screen.MainMenuScreen; @@ -22,11 +19,9 @@ import net.minecraft.util.text.IFormattableTextComponent; import net.minecraft.util.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.fml.client.registry.ClientRegistry; import net.minecraftforge.client.event.GuiScreenEvent; import net.minecraftforge.fml.ExtensionPoint; import net.minecraftforge.common.MinecraftForge; -import net.minecraftforge.event.TickEvent; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.DistExecutor; @@ -35,7 +30,7 @@ import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import net.minecraftforge.fml.loading.FMLPaths; -import org.lwjgl.glfw.GLFW; +import net.minecraftforge.fml.BrandingControl; import java.nio.file.Path; @@ -47,9 +42,13 @@ public final class UniversalConfigMod { private static final int TITLE_SCREEN_BUTTON_SIZE = 20; private static final int TITLE_SCREEN_ICON_PADDING = 3; private static final int TITLE_SCREEN_BUTTON_MARGIN = 4; - private static final int TITLE_SCREEN_BOTTOM_BRANDING_CLEARANCE = 54; + private static final int TITLE_SCREEN_SYSTEM_TEXT_BOTTOM_OFFSET = 10; + private static final int TITLE_SCREEN_SYSTEM_TEXT_GAP = 2; + // title_screen_button.png の実寸。画像を差し替える場合は描画APIへ渡す実寸・UV領域と + // この定数を必ず一致させる。ボタンの位置・サイズ・描画先サイズは変更しない。 + private static final int TITLE_SCREEN_ICON_TEXTURE_WIDTH = 15; + private static final int TITLE_SCREEN_ICON_TEXTURE_HEIGHT = 15; - private static KeyBinding openKey; private boolean pendingImportLogged; private boolean startupChangedOptions; @@ -69,18 +68,7 @@ public static Minecraft client() { return Minecraft.getInstance(); } - private void registerKeyBindings() { - openKey = new KeyBinding( - "key.universal_config.open", - InputMappings.Type.KEYSYM, - GLFW.GLFW_KEY_UNKNOWN, - "category.universal_config" - ); - ClientRegistry.registerKeyBinding(openKey); - } - private void clientSetup(FMLClientSetupEvent event) { - registerKeyBindings(); if (!startupChangedOptions) { return; } @@ -94,19 +82,6 @@ private void clientSetup(FMLClientSetupEvent event) { }); } - @SubscribeEvent - public void onClientTick(TickEvent.ClientTickEvent event) { - if (event.phase != TickEvent.Phase.END || openKey == null) { - return; - } - Minecraft minecraft = Minecraft.getInstance(); - while (openKey.consumeClick()) { - if (minecraft.screen == null || minecraft.screen instanceof MainMenuScreen) { - minecraft.setScreen(new ProfileListScreen(minecraft.screen)); - } - } - } - @SubscribeEvent public void onScreenInit(GuiScreenEvent.InitGuiEvent.Post event) { if (!(event.getGui() instanceof MainMenuScreen)) { @@ -115,13 +90,32 @@ public void onScreenInit(GuiScreenEvent.InitGuiEvent.Post event) { Minecraft minecraft = Minecraft.getInstance(); reloadStartupOptionsAtTitleScreen(); logPendingImportStateOnce(); + // Forge can re-run screen initialization after resize or resource reload. Reusing the same screen must not + // accumulate overlapping icon buttons. + for (net.minecraft.client.gui.widget.Widget widget : event.getWidgetList()) { + if (widget instanceof IconButton) { + return; + } + } ITextComponent narration = new TranslationTextComponent("button.universal_config.open"); - int y = event.getGui().height - TITLE_SCREEN_BUTTON_SIZE - TITLE_SCREEN_BOTTOM_BRANDING_CLEARANCE; + int y = titleScreenButtonY(minecraft, event.getGui().height); IconButton button = new IconButton(TITLE_SCREEN_BUTTON_MARGIN, y, ignored -> minecraft.setScreen(new ProfileListScreen(event.getGui())), narration); event.addWidget(button); } + private static int titleScreenButtonY(Minecraft minecraft, int screenHeight) { + int systemTextY = screenHeight - TITLE_SCREEN_SYSTEM_TEXT_BOTTOM_OFFSET; + int[] brandingLines = {0}; + BrandingControl.forEachLine(true, false, (index, line) -> brandingLines[0]++); + int brandingStep = minecraft.font.lineHeight + 1; + // Forge draws line 0 at systemTextY. Only the intervals between lines belong above it; reserving one full + // extra line creates a conspicuous blank row between the icon and the highest branding line. + int topBrandingY = systemTextY - Math.max(0, brandingLines[0] - 1) * brandingStep; + return Math.max(TITLE_SCREEN_BUTTON_MARGIN, + topBrandingY - TITLE_SCREEN_BUTTON_SIZE - TITLE_SCREEN_SYSTEM_TEXT_GAP); + } + private void reloadStartupOptionsAtTitleScreen() { if (!startupChangedOptions) { return; @@ -212,7 +206,9 @@ public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float par x + TITLE_SCREEN_ICON_PADDING, y + TITLE_SCREEN_ICON_PADDING, iconSize, iconSize, - 0.0F, 0.0F, 128, 128, 128, 128); + 0.0F, 0.0F, + TITLE_SCREEN_ICON_TEXTURE_WIDTH, TITLE_SCREEN_ICON_TEXTURE_HEIGHT, + TITLE_SCREEN_ICON_TEXTURE_WIDTH, TITLE_SCREEN_ICON_TEXTURE_HEIGHT); } @Override diff --git a/1.16.5/src/main/java/com/example/universalconfig/forge/screen/Button.java b/1.16.5/src/main/java/com/example/universalconfig/forge/screen/Button.java index 6cce16e..b8c56de 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/forge/screen/Button.java +++ b/1.16.5/src/main/java/com/example/universalconfig/forge/screen/Button.java @@ -7,10 +7,16 @@ import net.minecraft.util.text.ITextComponent; /** - * 1.20で導入されたButton.Builder相当を1.19系へ提供する。 - * 画面コードのレイアウト計算を世代間で揃え、狭い画面での配置差を防ぐため、この層だけでAPI差を吸収する。 + * 1.20で導入されたButton.Builderとnine-slice描画相当を1.16系へ提供する。 + * 画面コードのレイアウトとボタンの縁・長文表示を世代間で揃えるため、この層だけでAPI差を吸収する。 */ class Button extends net.minecraft.client.gui.widget.button.Button { + private static final int SOURCE_WIDTH = 200; + private static final int SOURCE_HEIGHT = 20; + private static final int HORIZONTAL_BORDER = 20; + private static final int VERTICAL_BORDER = 4; + private static final int TEXT_MARGIN = 2; + protected Button(int x, int y, int width, int height, ITextComponent message, IPressable onPress) { super(x, y, width, height, message, onPress); } @@ -24,25 +30,95 @@ public void renderButton(MatrixStack graphics, int mouseX, int mouseY, float par Minecraft minecraft = Minecraft.getInstance(); minecraft.getTextureManager().bind(WIDGETS_LOCATION); RenderSystem.color4f(1.0F, 1.0F, 1.0F, alpha); - int state = getYImage(isHovered()); + // 1.20.1と同様、マウスだけでなくキーボードフォーカスもホバー表示にする。 + int state = getYImage(isHovered() || isFocused()); RenderSystem.enableBlend(); RenderSystem.defaultBlendFunc(); RenderSystem.enableDepthTest(); - // Vanilla 1.16 copies `width / 2` pixels from each half of a 200px source. Wider responsive buttons therefore - // read outside the widget texture and split into disconnected bars. Stretch the two valid 100px halves instead. - int leftWidth = width / 2; - int rightWidth = width - leftWidth; int sourceY = 46 + state * 20; - AbstractGui.blit(graphics, x, y, leftWidth, height, - 0.0F, sourceY, 100, 20, 256, 256); - AbstractGui.blit(graphics, x + leftWidth, y, rightWidth, height, - 100.0F, sourceY, 100, 20, 256, 256); + drawNineSlicedBackground(graphics, sourceY); renderBg(graphics, minecraft, mouseX, mouseY); int textColor = getFGColor() | ((int) Math.ceil(alpha * 255.0F) << 24); - AbstractGui.drawCenteredString(graphics, minecraft.font, getMessage(), - x + width / 2, y + (height - 8) / 2, textColor); + drawScrollingText(graphics, minecraft, textColor); + } + + private void drawNineSlicedBackground(MatrixStack graphics, int sourceY) { + int left = Math.min(HORIZONTAL_BORDER, width / 2); + int right = Math.min(HORIZONTAL_BORDER, width / 2); + int top = Math.min(VERTICAL_BORDER, height / 2); + int bottom = Math.min(VERTICAL_BORDER, height / 2); + int centerWidth = width - left - right; + int centerHeight = height - top - bottom; + int sourceCenterWidth = SOURCE_WIDTH - HORIZONTAL_BORDER * 2; + int sourceCenterHeight = SOURCE_HEIGHT - VERTICAL_BORDER * 2; + + blitTile(graphics, x, y, left, top, 0, sourceY, left, top); + blitTiled(graphics, x + left, y, centerWidth, top, + HORIZONTAL_BORDER, sourceY, sourceCenterWidth, top); + blitTile(graphics, x + width - right, y, right, top, + SOURCE_WIDTH - right, sourceY, right, top); + blitTiled(graphics, x, y + top, left, centerHeight, + 0, sourceY + VERTICAL_BORDER, left, sourceCenterHeight); + blitTiled(graphics, x + left, y + top, centerWidth, centerHeight, + HORIZONTAL_BORDER, sourceY + VERTICAL_BORDER, sourceCenterWidth, sourceCenterHeight); + blitTiled(graphics, x + width - right, y + top, right, centerHeight, + SOURCE_WIDTH - right, sourceY + VERTICAL_BORDER, right, sourceCenterHeight); + blitTile(graphics, x, y + height - bottom, left, bottom, + 0, sourceY + SOURCE_HEIGHT - bottom, left, bottom); + blitTiled(graphics, x + left, y + height - bottom, centerWidth, bottom, + HORIZONTAL_BORDER, sourceY + SOURCE_HEIGHT - bottom, sourceCenterWidth, bottom); + blitTile(graphics, x + width - right, y + height - bottom, right, bottom, + SOURCE_WIDTH - right, sourceY + SOURCE_HEIGHT - bottom, right, bottom); + } + + private static void blitTiled(MatrixStack graphics, int destinationX, int destinationY, + int destinationWidth, int destinationHeight, + int sourceX, int sourceY, int tileWidth, int tileHeight) { + for (int offsetY = 0; offsetY < destinationHeight; offsetY += tileHeight) { + int drawHeight = Math.min(tileHeight, destinationHeight - offsetY); + for (int offsetX = 0; offsetX < destinationWidth; offsetX += tileWidth) { + int drawWidth = Math.min(tileWidth, destinationWidth - offsetX); + blitTile(graphics, destinationX + offsetX, destinationY + offsetY, + drawWidth, drawHeight, sourceX, sourceY, drawWidth, drawHeight); + } + } + } + + private static void blitTile(MatrixStack graphics, int destinationX, int destinationY, + int destinationWidth, int destinationHeight, + int sourceX, int sourceY, int sourceWidth, int sourceHeight) { + if (destinationWidth <= 0 || destinationHeight <= 0) { + return; + } + AbstractGui.blit(graphics, destinationX, destinationY, destinationWidth, destinationHeight, + sourceX, sourceY, sourceWidth, sourceHeight, 256, 256); + } + + private void drawScrollingText(MatrixStack graphics, Minecraft minecraft, int color) { + int left = x + TEXT_MARGIN; + int right = x + width - TEXT_MARGIN; + int textY = y + (height - 9) / 2 + 1; + int textWidth = minecraft.font.width(getMessage()); + int availableWidth = right - left; + if (textWidth <= availableWidth) { + AbstractGui.drawCenteredString(graphics, minecraft.font, getMessage(), x + width / 2, textY, color); + return; + } + + // 1.20.1と同じ往復式で長い翻訳をスクロールし、ボタン外へ文字を漏らさない。 + int overflow = textWidth - availableWidth; + double duration = Math.max(overflow * 0.5D, 3.0D); + double time = System.currentTimeMillis() / 1000.0D; + double progress = Math.sin((Math.PI / 2.0D) * Math.cos(2.0D * Math.PI * time / duration)) / 2.0D + 0.5D; + int offset = (int) (progress * overflow); + ScreenUtil.enableScissor(left, y, right, y + height); + try { + AbstractGui.drawString(graphics, minecraft.font, getMessage(), left - offset, textY, color); + } finally { + ScreenUtil.disableScissor(); + } } static final class Builder { diff --git a/1.16.5/src/main/java/com/example/universalconfig/forge/screen/ProfileCreateScreen.java b/1.16.5/src/main/java/com/example/universalconfig/forge/screen/ProfileCreateScreen.java index 871b9cc..1677335 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/forge/screen/ProfileCreateScreen.java +++ b/1.16.5/src/main/java/com/example/universalconfig/forge/screen/ProfileCreateScreen.java @@ -50,7 +50,7 @@ protected void init() { String currentDescription = descriptionField == null ? "" : descriptionField.getValue(); boolean includeKeybinds = keybindsCheckboxButton == null || keybindsCheckboxButton.selected(); boolean includeClientOptions = clientOptionsCheckboxButton == null || clientOptionsCheckboxButton.selected(); - boolean includeModConfigs = modConfigsCheckboxButton == null || modConfigsCheckboxButton.selected(); + boolean includeModConfigs = modConfigsCheckboxButton != null && modConfigsCheckboxButton.selected(); int formLeft = width / 2 - FORM_LEFT_OFFSET; addButton(new BlockIconButton(formLeft, NAME_FIELD_Y, ICON_BUTTON_SIZE, ICON_BUTTON_SIZE, new TranslationTextComponent("screen.universal_config.profile_icon_change", iconLabel(selectedIconId)), diff --git a/1.16.5/src/main/java/com/example/universalconfig/forge/screen/ProfileListScreen.java b/1.16.5/src/main/java/com/example/universalconfig/forge/screen/ProfileListScreen.java index d025140..6caa611 100644 --- a/1.16.5/src/main/java/com/example/universalconfig/forge/screen/ProfileListScreen.java +++ b/1.16.5/src/main/java/com/example/universalconfig/forge/screen/ProfileListScreen.java @@ -55,10 +55,12 @@ public final class ProfileListScreen extends Screen { private static final int DETAIL_LINE_HEIGHT = 12; private static final int PROFILE_ICON_SIZE = 28; private static final int SCROLLBAR_WIDTH = 3; + private static final int REORDER_BUTTON_WIDTH = 16; + private static final int REORDER_BUTTON_HEIGHT = 16; private static final int MORE_BUTTON_WIDTH = 34; private static final int MENU_WIDTH = 132; private static final int MENU_ITEM_GAP = 2; - private static final int MENU_ITEM_COUNT = 5; + private static final int MENU_ITEM_COUNT = 6; private static final int MENU_HEIGHT = MENU_ITEM_COUNT * BUTTON_HEIGHT + (MENU_ITEM_COUNT - 1) * MENU_ITEM_GAP + 8; private static final int CLOSE_BUTTON_SIZE = 20; private static final String SAFE_DATE_PATTERN = "yyyy/MM/dd HH:mm"; @@ -86,12 +88,17 @@ public final class ProfileListScreen extends Screen { private Path defaultProfilePath; private ITextComponent status = StringTextComponent.EMPTY; private final List